Kelp-Space 是記錄一些生活雜事的Blog
如有任何程式設計的問題歡迎到 飛特技術論壇 討論

2008-10-25

Sample Programs Using Asynchronous Sockets

Now that you have studied all the pieces, it’s time to put them together and create a set of real Windows asynchronous network programs. The following two programs recreate the SimpleTcpSrvr and SimpleTcpClient programs (Listings 5. 1 and 5.2, respectively) introduced in Chapter 5, "Connection-Oriented Sockets," but this time they use asynchronous network methods.

The Client Program
The AsyncTcpClient.cs program (Listing 8.2) uses the .NET Windows Forms library to create a Windows GUI environment for a simple TCP client. In a Windows environment, it is important that the program respond to Windows events from the user, as well as from the network


Listing 8.2: The AsyncTcpClient.cs program 複製程式碼(Copy to clipboard)
using System;
using System.Drawing;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
class AsyncTcpClient Form:
{
  private TextBox newText;
  private TextBox conStatus;
  private ListBox results;
  private Socket client;
  private byte[] data = new byte[1024];
  private int size = 1024;
  public AsyncTcpClient()
  {
   Text = "Asynchronous TCP Client";
   Size = new Size(400, 380);
   
   Label label1 = new Label();
   label1.Parent = this;
   label1.Text = "Enter text string:";
   label1.AutoSize = true;
   label1.Location = new Point(10, 30);
   newText = new TextBox();
   newText.Parent = this;
   newText.Size = new Size(200, 2 * Font.Height);
   newText.Location = new Point(10, 55);
   results = new ListBox();
   results.Parent = this;
   results.Location = new Point(10, 85);
   results.Size = new Size(360, 18 * Font.Height);
   Label label2 = new Label();
   label2.Parent = this;
   label2.Text = "Connection Status:";
   label2.AutoSize = true;
   label2.Location = new Point(10, 330);
   conStatus = new TextBox();
   conStatus.Parent = this;
   conStatus.Text = "Disconnected";
   conStatus.Size = new Size(200, 2 * Font.Height);
   conStatus.Location = new Point(110, 325);
   Button sendit = new Button();
   sendit.Parent = this;
   sendit.Text = "Send";
   sendit.Location = new Point(220,52);
   sendit.Size = new Size(5 * Font.Height, 2 * Font.Height);
   sendit.Click += new EventHandler(ButtonSendOnClick);
   Button connect = new Button();
   connect.Parent = this;
   connect.Text = "Connect";
   connect.Location = new Point(295, 20);
   connect.Size = new Size(6 * Font.Height, 2 * Font.Height);
   connect.Click += new EventHandler(ButtonConnectOnClick);
   Button discon = new Button();
   discon.Parent = this;
   discon.Text = "Disconnect";
   discon.Location = new Point(295,52);
   discon.Size = new Size(6 * Font.Height, 2 * Font.Height);
   discon.Click += new EventHandler(ButtonDisconOnClick);
  }
  void ButtonConnectOnClick(object obj, EventArgs ea)
  {
   conStatus.Text = "Connecting...";
   Socket newsock = new Socket(AddressFamily.InterNetwork,
              SocketType.Stream, ProtocolType.Tcp);
   IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
   newsock.BeginConnect(iep, new AsyncCallback(Connected), newsock);
  }
  void ButtonSendOnClick(object obj, EventArgs ea)
  {
   byte[] message = Encoding.ASCII.GetBytes(newText.Text);
   newText.Clear();
   client.BeginSend(message, 0, message.Length, SocketFlags.None,
          new AsyncCallback(SendData), client);
  }
  void ButtonDisconOnClick(object obj, EventArgs ea)
  {
   client.Close();
   conStatus.Text = "Disconnected";
  }
  void Connected(IAsyncResult iar)
  {
   client = (Socket)iar.AsyncState;
   try
   {
     client.EndConnect(iar);
     conStatus.Text = "Connected to: " + client.RemoteEndPoint.ToString();
     client.BeginReceive(data, 0, size, SocketFlags.None,
            new AsyncCallback(ReceiveData), client);
   } catch (SocketException)
   {
     conStatus.Text = "Error connecting";
   }
  }
  void ReceiveData(IAsyncResult iar)
  {
   Socket remote = (Socket)iar.AsyncState;
   int recv = remote.EndReceive(iar);
   string stringData = Encoding.ASCII.GetString(data, 0, recv);
   results.Items.Add(stringData);
  }
  void SendData(IAsyncResult iar)
  {
   Socket remote = (Socket)iar.AsyncState;
   int sent = remote.EndSend(iar);
   remote.BeginReceive(data, 0, size, SocketFlags.None,
          new AsyncCallback(ReceiveData), remote);
  }
  public static void Main()
  {
   Application.Run(new AsyncTcpClient());
  }
}

The class constructor creates the window objects for the program. For this simple client program, the following objects are created:

A Label object and a TextBox object; these allow the customer to input text messages to send to the remote host

A ListBox object to display the messages returned by the remote host

A Label object and a TextBox object to display the connection status of the client program

Three Button objects; one for establishing a new TCP connection to a remote host, one for sending the text message and receiving a reply, and one for disconnecting the TCP session.

Each object is placed using the Point class and sized using the Size class; both of these classes are from the System.Drawing namespace. You can experiment with these values to get a feel for manually locating the objects on the Window form.

Note  If you have one of the Microsoft Visual products, such as Visual Studio or Visual C#, you can place all the Window objects using the graphical editing environment and allow the Form code to be generated automatically for you.


The Client Program Flow
You may notice that there is no network programming code at all in the class constructor. All of the network functions happen within EventHandler() methods. To get the AsyncTcpClient program to perform the same way as the original SimpleTcpClient program, you must first determine how to link the program protocol with the asynchronous events that will be triggered in the program. Then you must program them into the Windows event code methods. The diagram in Figure 8.3 shows each of the required methods and how they interact with the Windows event code. The following paragraphs describe the steps accomplished by the sample client program: connecting, receiving, sending, and disconnecting.



Connecting
As seen in the figure, when the customer clicks the Connect button, the method used to start the connection is performed. In the program code, this is the ButtonConnectOnClick() method:

ButtonConnectOnClick() 複製程式碼(Copy to clipboard)
void ButtonConnectOnClick(object obj, EventArgs ea)
{
  conStatus.Text = "Connecting...";
  Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
   ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
  newsock.BeginConnect(iep, new AsyncCallback(Connected), newsock);
}

The ButtonConnectOnClick() method creates a new Socket object for the communication. Then it starts the BeginConnect() method with the address information of the remote host, the name of the associated AsyncCallback method, and the newly created Socket object. When this method finishes processing, Windows waits for the BeginConnect() method to trigger its event, which indicates that a connection is established with the remote host. When this occurs, program control is passed to the Connected() method:

Connected() 複製程式碼(Copy to clipboard)
void Connected(IAsyncResult iar)
{
  client = (Socket)iar.AsyncState;
  try
  {
   client.EndConnect(iar);
   conStatus.Text = "Connected to: " + client.RemoteEndPoint.ToString();
   client.BeginReceive(data, 0, size, SocketFlags.None,
    new AsyncCallback(ReceiveData), client);
  } catch (SocketException)
  {
   conStatus.Text = "Error connecting";
  }
}

The first thing that must be done in the Connected() method is to retrieve the original Socket object used for the connection. The AsyncState property of the IAsyncResult class returns the object passed to the AsyncCallback method, which in the case of the BeginConnect() method was the newly created Socket object.

After the Socket object is recreated, the EndConnect() method can be performed. Because it is possible that the EndConnect() method could fail (for instance, if the remote host is unavailable), it is a good idea to place it in a try-catch block. Should EndConnect() fail, you can notify the customer through the conStatus TextBox object.

In this program protocol, the first thing the server does after the connection is established is to send a welcome banner. To accommodate this, the AsyncTcpClient program must be prepared to accept an incoming message immediately after establishing the connection. This is done by using a BeginReceive() method call at the end of the Connected() method.

Receiving Data
After a new connection, and after every sent message, a ReceiveData() method is performed. The BeginReceive() method declares the ReceiveData() method, the AsyncCallback method that’s used when the data is received:

ReceiveData() 複製程式碼(Copy to clipboard)
void ReceiveData(IAsyncResult iar)
{
  Socket remote = (Socket)iar.AsyncState;
  int recv = remote.EndReceive(iar);
  string stringData = Encoding.ASCII.GetString(data, 0, recv);
  results.Items.Add(stringData);
}

Again, the first statement recreates the communication socket. After the original socket is recreated, the EndReceive() method is used, referencing the original IAsyncResult object, which pairs it with the original BeginReceive() method call. When the message is received from the remote host, it is placed in the data buffer referenced in the BeginReceive() method call. Because the data variable was defined as a global variable, it is used within this method to place the message in the results ListBox.

Sending Data
After entering a message in the TextBox object to send to the remote host, the customer clicks the Send button, and the SendData() method is performed. The EventHandler for the button points to the ButtonSendOnClick() method:

ButtonSendOnClick() 複製程式碼(Copy to clipboard)
void ButtonSendOnClick(object obj, EventArgs ea) { byte[] message = Encoding.ASCII.GetBytes(newText.Text); newText.Clear(); client.BeginSend(message, 0, message.Length, SocketFlags.None, new AsyncCallback(SendData), client); }

The message is extracted from the TextBox object, converted to a byte array, and sent out the socket. Because the ButtonSendOnClick() method does not have the original Socket object passed to it, it needs to use a class member to reference the connected socket. The BeginSend() method is used on the connected socket. It specifies the message and the AsyncCallback method to call when the BeginSend() function is ready to complete, along with an object to pass to the EndSend() method. In this case, it is the connected socket.

The AsyncCallback method, SendData(), is triggered when the socket indicates it is ready to send the message:

SendData() 複製程式碼(Copy to clipboard)
void SendData(IAsyncResult iar)
{
  Socket remote = (Socket)iar.AsyncState;
  int sent = remote.EndSend(iar);
  remote.BeginReceive(data, 0, size, SocketFlags.None,
   new AsyncCallback(ReceiveData), remote);
}

Once again, you see the familiar behavior: the original connected socket is recreated using the AsyncState of the state object passed from the BeginSend() method. When the original socket is recreated, the EndSend() method completes the data transmission. The IAsyncResult object pairs the EndSend()to the original BeginSend() and EndSend()returns the number of bytes that were actually sent out from the socket.

In this application, after a message is sent, the server is expected to echo it back. Knowing this, the BeginReceive() method is called to start the receiving process. BeginReceive()again calls the same ReadData() method, just as it did to receive the original connection’s welcome banner.

Disconnecting
Now that you have the wonders of event programming at your disposal, you can finally create a button to control the disconnect from the remote host (rather than having to define a control word to stop the client). Here’s how that works:

ButtonDisconOnClick() 複製程式碼(Copy to clipboard)
void ButtonDisconOnClick(object obj, EventArgs ea)
{
  client.Close();
  conStatus.Text = "Disconnected";
}

By issuing the Close() method for the client socket, the connection is disconnected. To establish a new connection, the customer can click the Connect button.

Warning  There are lots of things that can go wrong with this simple client example, including the customer’s clicking the wrong button at the wrong time. A real-world production application should check each time a button is clicked to ensure that the function can be performed.

The Server Program

Now that you have moved the client program into the Windows world, it is time to tackle the server program. Listing 8.3 shows the AsyncTcpSrvr.cs program, which mimics the functionality of the SimpleTcpSrvr program (Listing 5.1) from Chapter 5 but uses a Windows Forms environment and asynchronous sockets.

Listing 8.3: The AsyncTcpSrvr.cs program 複製程式碼(Copy to clipboard)
using System;
using System.Drawing;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
class AsyncTcpSrvr    Form:
{
  private TextBox conStatus;
  private ListBox results;
  private byte[] data = new byte[1024];
  private int size = 1024;
  private Socket server;
  public AsyncTcpSrvr()
  {
   Text = "Asynchronous TCP Server";
   Size = new Size(400, 380);
   results = new ListBox();
   results.Parent = this;
   results.Location = new Point(10, 65);
   results.Size = new Size(350, 20 * Font.Height);
   Label label1 = new Label();
   label1.Parent = this;
   label1.Text = "Text received from client:";
   label1.AutoSize = true;
   label1.Location = new Point(10, 45);
   Label label2 = new Label();
   label2.Parent = this;
   label2.Text = "Connection Status:";
   label2.AutoSize = true;
   label2.Location = new Point(10, 330);
   conStatus = new TextBox();
   conStatus.Parent = this;
   conStatus.Text = "Waiting for client...";
   conStatus.Size = new Size(200, 2 * Font.Height);
   conStatus.Location = new Point(110, 325);
   Button stopServer = new Button();
   stopServer.Parent = this;
   stopServer.Text = "Stop Server";
   stopServer.Location = new Point(260,32);
   stopServer.Size = new Size(7 * Font.Height, 2 * Font.Height);
   stopServer.Click += new EventHandler(ButtonStopOnClick);
   server = new Socket(AddressFamily.InterNetwork,
          SocketType.Stream, ProtocolType.Tcp);
   IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9050);
   server.Bind(iep);
   server.Listen(5);
   server.BeginAccept(new AsyncCallback(AcceptConn), server);
  }
  void ButtonStopOnClick(object obj, EventArgs ea)
  {
   Close();
  }
  void AcceptConn(IAsyncResult iar)
  {
   Socket oldserver = (Socket)iar.AsyncState;
   Socket client = oldserver.EndAccept(iar);
   conStatus.Text = "Connected to: " + client.RemoteEndPoint.ToString();
   string stringData = "Welcome to my server";
   byte[] message1 = Encoding.ASCII.GetBytes(stringData);
   client.BeginSend(message1, 0, message1.Length, SocketFlags.None,
         new AsyncCallback(SendData), client);
  }
  void SendData(IAsyncResult iar)
  {
   Socket client = (Socket)iar.AsyncState;
   int sent = client.EndSend(iar);
   client.BeginReceive(data, 0, size, SocketFlags.None,
         new AsyncCallback(ReceiveData), client);
  }
  void ReceiveData(IAsyncResult iar)
  {
   Socket client = (Socket)iar.AsyncState;
   int recv = client.EndReceive(iar);
   if (recv == 0)
   {
     client.Close();
     conStatus.Text = "Waiting for client...";
     server.BeginAccept(new AsyncCallback(AcceptConn), server);
     return;
   }
   string receivedData = Encoding.ASCII.GetString(data, 0, recv);
   results.Items.Add(receivedData);
   byte[] message2 = Encoding.ASCII.GetBytes(receivedData);
   client.BeginSend(message2, 0, message2.Length, SocketFlags.None,
          new AsyncCallback(SendData), client);
  }
  public static void Main()
  {
   Application.Run(new AsyncTcpSrvr());
  }
}

Just as the client program does, the AsyncTcpSrvr program uses basic Windows Forms objects to create the customer graphical environment:

A Label object and a ListBox object to identify the data received from the connected client

A Label object and a TextBox object to display the status of the socket and the connected client

A Button object to allow the customer to stop the server and exit the program

After creating the customer interface, you must code the network programming part. Unlike the client program, the AsyncTcpSrvr program includes network programming code that must be done in the class constructor.

The Server Program Flow

Like its counterpart SimpleTcpSrvr in Chapter 5, the AsyncTcpSrvr program must follow a set procedure for communicating with remote clients. Figure 8.5 diagrams the steps required for the server.


Waiting for New Client Connections
Because you know that the server must immediately listen for incoming connections, the required network programming code is added to the class constructor after the Forms objects have been created:

new Socket() 複製程式碼(Copy to clipboard)
server = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9050);
server.Bind(iep);
server.Listen(5);
server.BeginAccept(new AsyncCallback(AcceptConn), server);

By now, you should recognize this as standard server socket code: creating a new Socket object, binding it to a local IPEndPoint object, and listening for new connection attempts.

The BeginAccept() method specifies the AsyncCallback method to use when a connection is received, and the object to pass to the AsyncCallback method.

When a connection attempt is detected, the AsyncCallback method registered with the BeginAccept() method is performed:

AcceptConn() 複製程式碼(Copy to clipboard)
void AcceptConn(IAsyncResult iar)
{
  Socket oldserver = (Socket)iar.AsyncState;
  Socket client = oldserver.EndAccept(iar);
  conStatus.Text = "Connected to: " + client.RemoteEndPoint.ToString();
  string stringData = "Welcome to my server";
  byte[] message1 = Encoding.ASCII.GetBytes(stringData);
  client.BeginSend(message1, 0, message1.Length, SocketFlags.None,
   new AsyncCallback(SendData), client);
}

The AsyncCallback method recreates the original Socket object using the AsyncState property of the IAsyncResult object. When the EndAccept() method is called, it uses the IAsyncResult object to pair it up with the calling BeginAccept() method. The EndAccept() method returns a new Socket object to be used for all communication with the remote client.

As reflected in the program flow diagram, the next step for the server is to send a welcome banner to the client. This requires the BeginSend() method, which registers an AsyncCallback method to complete the send operation when the socket is ready to send out data.

Warning  Note that the Socket object passed to the AsyncCallback method is the newly created client socket, not the original server socket. This is the socket that is connected to the remote client, and it should be used for all communications with that client.

Sending Data
The EndSend() method is found in the AsyncCallback method registered in the BeginSend() method:

SendData() 複製程式碼(Copy to clipboard)
void SendData(IAsyncResult iar)
{
  Socket client = (Socket)iar.AsyncState;
  int sent = client.EndSend(iar);
  client.BeginReceive(data, 0, size, SocketFlags.None,
   new AsyncCallback(ReceiveData), client);
}

This sample program uses the standard EndSend() format. A good-quality production program would also compare this value with the original byte count of the sent message and perform the BeginSend() method again if some of the message were not properly sent.

The server program flow model specifies that after the welcome banner is sent, the server should wait for an incoming message from the client. This is accomplished with the BeginReceive() method, which specifies a data buffer in which to place the received data, along with the AsyncCallback method to call when the data is received.

Receiving Data
The EndReceive() method is placed in the AsyncCallback method for the BeginReceive() method:

ReceiveData() 複製程式碼(Copy to clipboard)
void ReceiveData(IAsyncResult iar)
{
  Socket client = (Socket)iar.AsyncState;
  int recv = client.EndReceive(iar);
  if (recv == 0)
  {
   client.Close();
   conStatus.Text = "Waiting for client...";
   server.BeginAccept(new AsyncCallback(AcceptConn), server);
   return;
  }
  string receivedData = Encoding.ASCII.GetString(data, 0, recv);
  results.Items.Add(receivedData);
  byte[] message2 = Encoding.ASCII.GetBytes(receivedData);
  client.BeginSend(message2, 0, message2.Length, SocketFlags.None, new AsyncCallback(SendData), client);
}

The ReceiveData() method is a little lengthier than the others because it has to accommodate two scenarios in the program flow. First, as always, the client socket is recreated using the standard AsyncState property. Next, the EndReceive() method is called using the IAsyncResult object to pair it up with the appropriate BeginReceive() method. EndReceive()returns the number of bytes received from the socket. As stated earlier, a real-time production program would check the number of bytes received against an expected message size.

If the received message size is zero bytes, it is assumed that the remote client has disconnected the TCP session. In that case, the client socket is closed, and the server can start listening for a new client connection by using the BeginAccept() method and starting the whole program flow all over again.

If a message has been received from the remote client, it must be displayed in the ListBox object and echoed back to the client. This is done by using the BeginSend() method again. Because the program flow now duplicates the original flow from sending the welcome banner, the original SendData() method is used for this AsyncCallback method as well.

取自C# Network Programing

沒有留言:

張貼留言