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
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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