|
A Simple C# Asynchronous Class Example
In this example, we will create a shared asynchronous packet class that will be used by the C# and VB .NET server and client programs that will be created later. Create a new class library project and you might want to use AsyncPacketClass as the project and AsyncPacketClassCS as the solution names as shown in the following Figure.
|
Next, rename the source file to AsyncPacket to reflect the application that we are going to develop.
Then, rename the first default class definition to IoPacket.
Add the following code for the IoPacketType enum, IoPacket class and several other classes.
// This file contains common class definitions used by both the client // and server asynchronous code samples. This file contains classes // used as the context information associated with each asynchronous // operation posted.
using System; using System.Net; using System.Net.Sockets; using System.Collections; using System.Threading;
namespace AsyncPacketClass { /// <summary> /// Indicates the type of asynchronous operation. /// </summary> public enum IoPacketType { Accept, Connect, Send, SendTo, Receive, ReceiveFrom }
/// <summary> /// Describes an IO asynchronous IO request. This class contains the delegate which /// is fired when the specified operation completes. It also allocates the data /// buffer used by a send or receive operation. /// </summary> public class IoPacket { public IoPacketType ioType; public SocketConnection connObject; public AsyncCallback ioCallback; public byte[ ] dataBuffer; public const int IoPacketDefaultBufferSize = 4096;
/// <summary> /// Initialize member variables and allocate the data buffer if this IO packet /// describes an asynchronous send or receive operation. /// </summary> /// <param name="connObj">Connection object to which this IO packet belongs to</param> /// <param name="packetType">Operation type being created</param> public IoPacket(SocketConnection connObj, IoPacketType packetType) { ioType = packetType; connObject = connObj; ioCallback = new AsyncCallback(IoCompleteCallback);
// If this packet describes a send or receive operation create the // data buffer. if (packetType != IoPacketType.Accept) { dataBuffer = new byte[IoPacketDefaultBufferSize]; } }
/// <summary> /// Posts an asynchronous connect operation on the underlying TCP socket. /// </summary> /// <param name="serverEndPoint"></param> public void PostConnect(IPEndPoint serverEndPoint) { try { connObject.tcpSocket.BeginConnect(serverEndPoint, ioCallback, this); connObject.IncrementOutstanding(ioType); // Console.WriteLine("Posting a BeginConnect..."); } catch (SocketException err) { Console.WriteLine("IoPacket.PostConnect: {0}", err.Message); throw; } }
/// <summary> /// Posts an asynchronous accept operation on the underlying TCP socket. /// </summary> public void PostAccept() { try { // Post the async accept and increment the accept count connObject.tcpSocket.BeginAccept(ioCallback, this); connObject.IncrementOutstanding(ioType); //Console.WriteLine("Posting a BeginAccept..."); } catch (SocketException err) { Console.WriteLine("IoPacket.PostAccept: {0}", err.Message); throw; } }
/// <summary> /// Posts an asynchronous receive operation on the underlying TCP socket. /// </summary> public void PostReceive() { try { // Post the async receive and increment the accept count connObject.tcpSocket.BeginReceive( dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ioCallback, this ); connObject.IncrementOutstanding(ioType); //Console.WriteLine("Posting a BeginReceive..."); } catch (SocketException err) { Console.WriteLine("IoPacket.BeginReceive: {0}", err.Message); throw; } }
/// <summary> /// Posts an asynchronous send operation on the underlying TCP socket. /// </summary> public void PostSend() { try { // Post an async send and increment the send count connObject.tcpSocket.BeginSend( dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ioCallback, this ); connObject.IncrementOutstanding(ioType); //Console.WriteLine("Posting a BeginSend..."); } catch (SocketException err) { Console.WriteLine("IoPacket.BeginSend: {0}", err.Message); throw; } }
/// <summary> /// This is the asynchronous delegate that is called when an operation on the posted /// IO object completes. This method simply calls the owning objects HandleIo method. /// Only object derived from the SocketConnection class can use this IoPacket class /// for describing async IO operations. /// </summary> /// <param name="ar">Asynchronous context information</param> static void IoCompleteCallback(IAsyncResult ar) { IoPacket ioOperation = (IoPacket)ar.AsyncState;
ioOperation.connObject.DecrementOutstanding(ioOperation.ioType); ioOperation.connObject.HandleIo(ioOperation, ar); } }
/// <summary> /// Base class from which the TcpServer and ClientConnection objects are derived /// from. /// </summary> abstract public class SocketConnection { public Socket tcpSocket; public IPEndPoint tcpAddress; protected int receiveOutstanding; protected int receiveFromOutstanding; protected int sendOutstanding; protected int sendToOutstanding; protected int connectOutstanding; protected int acceptOutstanding; protected int sendByteCount; protected int receiveByteCount;
/// <summary> /// Simple constructor to initialize counters to zero. /// </summary> public SocketConnection() { receiveOutstanding = 0; receiveFromOutstanding = 0; sendOutstanding = 0; sendToOutstanding = 0; connectOutstanding = 0; acceptOutstanding = 0; sendByteCount = 0; receiveByteCount = 0; }
/// <summary> /// Returns the total bytes sent on the socket. /// </summary> public int TotalSendBytes { get { return sendByteCount; } }
/// <summary> /// Returns the total bytes received on the socket. /// </summary> public int TotalReceiveBytes { get { return receiveByteCount; } }
/// <summary> /// This method handles the IO operation that completed. /// </summary> /// <param name="io">Object describing the IO operation that completed</param> /// <param name="ar">Asynchronous context information</param> abstract public void HandleIo(IoPacket io, IAsyncResult ar);
/// <summary> /// Removes the indicated IO packet from any lists or queues the packet may be in /// </summary> /// <param name="io">IO object to remove from any lists</param> abstract public void RemoveIo(IoPacket io);
/// <summary> /// Increments the count for the outstanding operation type. This is implemented such /// that the IoPacket post routines can make a call to increment the count such that /// the caller of each post routine doesn't have to make the increment call. /// </summary> /// <param name="ioType">Operation type to increment count for</param> public void IncrementOutstanding(IoPacketType ioType) { switch (ioType) { case IoPacketType.Connect: Interlocked.Increment(ref connectOutstanding); break; case IoPacketType.Accept: Interlocked.Increment(ref acceptOutstanding); break; case IoPacketType.Receive: Interlocked.Increment(ref receiveOutstanding); break; case IoPacketType.ReceiveFrom: Interlocked.Increment(ref receiveFromOutstanding); break; case IoPacketType.Send: Interlocked.Increment(ref sendOutstanding); break; case IoPacketType.SendTo: Interlocked.Increment(ref sendToOutstanding); break; default: break; } }
/// <summary> /// Decrements the count for the outstanding operation type. This is implemented such /// that the IoPacket post routines can make a call to decrement the count such that /// the caller of each post routine doesn't have to make the decrement call. /// </summary> /// <param name="ioType">Operation type to decrement count for</param> public void DecrementOutstanding(IoPacketType ioType) { switch (ioType) { case IoPacketType.Connect: Interlocked.Decrement(ref connectOutstanding); break; case IoPacketType.Accept: Interlocked.Decrement(ref acceptOutstanding); break; case IoPacketType.Receive: Interlocked.Decrement(ref receiveOutstanding); break; case IoPacketType.ReceiveFrom: Interlocked.Decrement(ref receiveFromOutstanding); break; case IoPacketType.Send: Interlocked.Decrement(ref sendOutstanding); break; case IoPacketType.SendTo: Interlocked.Decrement(ref sendToOutstanding); break; default: break; } } } /// <summary> /// This class identifies a client connection. It keeps track of all outstanding /// async send and receive operations and handles completed IO operations. /// </summary> public class ClientConnection : SocketConnection { private ArrayList ownerList; private ManualResetEvent ownerEvent; private ArrayList sendList; private ArrayList recvList; private int sendCountLeft; private IoPacket connectPacket;
/// <summary> /// This is the constructor for the client which takes only the client socket /// object. It creates the lists of outstanding send and receive operations /// and posts the initial BeginReceive. /// </summary> /// <param name="tcpClient">Socket object representing the client connection</param> /// <param name="serverObject">Reference to the server object that owns this client connection</param> public ClientConnection(IPEndPoint serverEndPoint, ArrayList clientList, ManualResetEvent clientEmpty) : base() { ownerList = clientList; ownerEvent = clientEmpty;
sendList = new ArrayList(); recvList = new ArrayList();
sendCountLeft = 15;
tcpSocket = new Socket( serverEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp );
connectPacket = new IoPacket(this, IoPacketType.Connect);
try { connectPacket.PostConnect(serverEndPoint); } catch (Exception err) { Console.WriteLine("ClientConnection: PostConnect failed: {0}", err.Message); } }
/// <summary> /// This overridden routine handles all asynchronous IO that completes on the ClientConnection /// object. Since the client only sends and receives only the Send and Receive commands are /// handled. The client connection first receives a request from the client until the client /// shuts down the send path. At this point the client connection will receive zero bytes /// indicating the client is done sending. This is followed by the client sending a response /// to the client. Once the requested number of send operations are performed to the client /// the client socket is closed and resources are freed (by closing the socket handle and /// indicating to the owning TcpServer to remove the ClientConnection object from its list /// of connections). /// </summary> /// <param name="io">IO packet object that describes the completed operation</param> /// <param name="ar">Asynchronous context information for the completed operation</param> public override void HandleIo(IoPacket io, IAsyncResult ar) { int rc;
switch (io.ioType) { case IoPacketType.Connect: // Connect operation completed try { tcpSocket.EndConnect(ar);
// Add the IoPacket to the send list sendList.Add(io); // Make it a send operation and post a send operation io.ioType = IoPacketType.Send; io.PostSend(); } catch (Exception err) { Console.WriteLine("Socket connect failed: {0}", err.Message); tcpSocket.Close(); ownerList.Remove(this); ownerEvent.Set(); } break; case IoPacketType.Send: // An async send operation completed, first decrement the number of sends left Interlocked.Decrement(ref sendCountLeft);
try { // Get the results from the send rc = tcpSocket.EndSend(ar); // Update counters sendByteCount += rc;
// Console.WriteLine("Client wrote {0} bytes", rc);
// Check to see if we're done sending, if so close the socket and clean // up the IoPacket used. if (sendCountLeft == 0) { // Console.WriteLine("Shutting down connection");
tcpSocket.Shutdown(SocketShutdown.Send); RemoveIo(io); io.ioType = IoPacketType.Receive; recvList.Add(io); io.PostReceive(); } else { // If there are still sends left, post another async send operation io.PostSend(); } } catch (Exception err) { Console.WriteLine("ClientConnection.HandleIo: Send op failed: {0}", err.Message); tcpSocket.Close(); ownerList.Remove(this); ownerEvent.Set(); } break; case IoPacketType.Receive: // An async receive operation completed try { // Get the results from the operation rc = tcpSocket.EndReceive(ar);
// Update the byte counters receiveByteCount += rc;
// Console.WriteLine("Client read {0} bytes", rc); // If zero bytes were returned, the client has shutdown its send side. This // side then initiates sending data back to the client. if (rc == 0) { // Console.WriteLine("Client connection shutdown..."); // Once the entire request is received start the response RemoveIo(io); tcpSocket.Close(); ownerList.Remove(this); ownerEvent.Set(); } else { // Client hasn't finished sending data so post another async receive io.PostReceive(); } } catch (Exception err) { // Clean up the client connection if a receive fails Console.WriteLine("ClientConnection.HandleIo: Receive op failed: {0}", err.Message); tcpSocket.Close(); ownerList.Remove(this); ownerEvent.Set(); } break; default: // Should never be here Console.WriteLine("ClientConnection.HandleIo: Accept indication received ?!?!"); break; } }
/// <summary> /// Removes the given IoPacket from the appropriate ArrayList depending on the operation /// type. /// </summary> /// <param name="io">IO object to remove from any lists</param> public override void RemoveIo(IoPacket io) { switch (io.ioType) { case IoPacketType.Receive: recvList.Remove(io); break; case IoPacketType.Send: sendList.Remove(io); break; default: break; } io = null; } }
/// <summary> /// This class identifies a client connection. It keeps track of all outstanding /// async send and receive operations and handles completed IO operations. /// </summary> public class AcceptConnection : SocketConnection { private ArrayList sendList; private ArrayList recvList; private TcpServer tcpParent; private int sendCountLeft;
/// <summary> /// This is the constructor for the client which takes only the client socket /// object. It creates the lists of outstanding send and receive operations /// and posts the initial BeginReceive. /// </summary> /// <param name="tcpClient">Socket object representing the client connection</param> /// <param name="serverObject">Reference to the server object that owns this client connection</param> public AcceptConnection(Socket tcpClient, TcpServer serverObject) : base() { tcpSocket = tcpClient; sendList = new ArrayList(); recvList = new ArrayList(); tcpParent = serverObject;
sendCountLeft = 15; Console.WriteLine("Client connection from: {0}", tcpSocket.RemoteEndPoint.ToString()); // Post the initial BeginReceive on the client socket IoPacket recvPacket = new IoPacket(this, IoPacketType.Receive); recvList.Add(recvPacket);
try { recvPacket.PostReceive(); } catch (Exception err) { Console.WriteLine("AcceptConnection.ClientConnection: PostReceive failed: {0}", err.Message); } }
/// <summary> /// This overridden routine handles all asynchronous IO that completes on the ClientConnection /// object. Since the client only sends and receives only the Send and Receive commands are /// handled. The client connection first receives a request from the client until the client /// shuts down the send path. At this point the client connection will receive zero bytes /// indicating the client is done sending. This is followed by the client sending a response /// to the client. Once the requested number of send operations are performed to the client /// the client socket is closed and resources are freed (by closing the socket handle and /// indicating to the owning TcpServer to remove the ClientConnection object from its list /// of connections). /// </summary> /// <param name="io">IO packet object that describes the completed operation</param> /// <param name="ar">Asynchronous context information for the completed operation</param> public override void HandleIo(IoPacket io, IAsyncResult ar) { int rc;
switch (io.ioType) { case IoPacketType.Send: // An async send operation completed, first decrement the number of sends left Interlocked.Decrement(ref sendCountLeft);
try { // Get the results from the send rc = tcpSocket.EndSend(ar); // Update counters sendByteCount += rc; tcpParent.IncrementByteCount(rc, 0); // Console.WriteLine("Client wrote {0} bytes", rc); // Check to see if we're done sending, if so close the socket and clean // up the IoPacket used. if (sendCountLeft == 0) { tcpSocket.Shutdown(SocketShutdown.Send); tcpSocket.Close(); RemoveIo(io); io = null; tcpSocket = null; // Console.WriteLine("Connection finished..."); } else { // If there are still sends left, post another async send operation io.PostSend(); } } catch (Exception err) { Console.WriteLine("AcceptConnection.HandleIo: Send op failed: {0}", err.Message); tcpSocket.Close(); tcpSocket = null; } finally { // Check to see if the client connection is done, if so indicate to the TcpServer // parent to remove the client from its list.
// Console.WriteLine("sendCountLeft = {0} | sendOutstanding = {1} | recvOutstanding = {2}", // sendCountLeft, // sendOutstanding, // base.receiveOutstanding // );
if ((tcpSocket == null) && (sendCountLeft == 0) && (sendOutstanding == 0) && (receiveOutstanding == 0)) { tcpParent.RemoveClient(this); } } break; case IoPacketType.Receive: // An async receive operation completed try { // Get the results from the operation rc = tcpSocket.EndReceive(ar); // Update the byte counters receiveByteCount += rc; tcpParent.IncrementByteCount(0, rc); // Console.WriteLine("Client read {0} bytes", rc); // If zero bytes were returned, the client has shutdown its send side. This // side then initiates sending data back to the client. if (rc == 0) { //Console.WriteLine("Client connection shutdown..."); // Once the entire request is received start the response RemoveIo(io); // Switch the packet type to sending io.ioType = IoPacketType.Send; sendList.Add(io); // Post a send operation to the client io.PostSend(); } else { // Client hasn't finished sending data so post another async receive io.PostReceive(); } } catch (Exception err) { // Clean up the client connection if a receive fails Console.WriteLine("ClientConnection.HandleIo: Receive op failed: {0}", err.Message); tcpSocket.Close(); tcpSocket = null; tcpParent.RemoveClient(this); } break; default: // Should never be here Console.WriteLine("AcceptConnection.HandleIo: Accept indication received ?!?!"); break; } }
/// <summary> /// Removes the given IoPacket from the appropriate ArrayList depending on the operation /// type. /// </summary> /// <param name="io">IO object to remove from any lists</param> public override void RemoveIo(IoPacket io) { switch (io.ioType) { case IoPacketType.Receive: recvList.Remove(io); break; case IoPacketType.Send: sendList.Remove(io); break; default: break; } io = null; } }
/// <summary> /// This class encapsulates functionality for the TCP server object. This /// class keeps track of all clients accepted on the listening socket /// as well as the outstanding asynchronous accept operations. The class /// keeps track the send and receive byte counters and client counts. /// </summary> public class TcpServer : SocketConnection { private ArrayList clientList, clientSyncList; private ArrayList acceptList; private int serverShutdown; public ManualResetEvent serverDone; public int totalClientCount; public int currentClientCount;
/// <summary> /// Simple constructor for the TcpServer class. This initializes the counters /// and creates the listening TCP socket. /// </summary> /// <param name="listenEndPoint">Endpoint to create the listening socket on</param> /// <param name="asyncListenLimit">Maximum number of async accepts outstanding</param> public TcpServer(IPEndPoint listenEndPoint, int asyncListenLimit) : base() { int optval = 1;
// Initialize some member variables serverShutdown = 0; totalClientCount = 0; currentClientCount = 0; serverDone = new ManualResetEvent(false);
// Create the listening socket tcpSocket = new Socket( listenEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp );
// Set exclusive use so that the port we bind to cannot be stolen by anyone else tcpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, optval); // Bind the socket to the specified address and port tcpSocket.Bind(listenEndPoint); // Set the socket to listening tcpSocket.Listen(int.MaxValue); // Indicate what address/port socket is listening on Console.WriteLine("Listening on {0}", listenEndPoint.ToString()); // Create list for outstanding client connections clientList = new ArrayList(); clientSyncList = ArrayList.Synchronized(clientList); // Create list for outstanding BeginAccept operations acceptList = new ArrayList(); // Post the initial BeginAccepts on the listening socket for (int i = 0; i < asyncListenLimit; i++) { IoPacket acceptPacket = new IoPacket(this, IoPacketType.Accept); acceptList.Add(acceptPacket); try { acceptPacket.PostAccept(); } catch (Exception err) { Console.WriteLine("TcpServer.TcpServer: PostAccept failed: {0}", err.Message); } } }
/// <summary> /// This method removes an accepted client object (AcceptConnection) from the /// list of accepted connections. It decrements the current client count. /// </summary> /// <param name="clientObject">Accepted client object to remove</param> public void RemoveClient(AcceptConnection clientObject) { clientSyncList.Remove(clientObject); clientObject = null; Interlocked.Decrement(ref currentClientCount); }
/// <summary> /// This method marks the TcpServer as shutting down. This flag is set before all the /// listening sockets are closed. /// </summary> public void Shutdown() { Interlocked.Increment(ref serverShutdown); }
/// <summary> /// This routine handles all IO completion events initiated by the TcpServer /// listening socket which will consist of BeginAccept events. Before accessing /// the socket, a check is made to make sure the server is not shutting down /// in which case the listening socket have already been closed. /// </summary> /// <param name="io">IO packet describing the operation that completed</param> /// <param name="ar">Asynchronous context information</param> public override void HandleIo(IoPacket io, IAsyncResult ar) { switch (io.ioType) { case IoPacketType.Accept: // Console.WriteLine("TcpServer.HandleIo: Accept completed: outstanding {0}", acceptOutstanding ); if (serverShutdown != 0) { io.connObject.RemoveIo(io); return; }
try { Socket clientSocket; AcceptConnection clientConn;
try { // Handle the accept that completed clientSocket = tcpSocket.EndAccept(ar); clientConn = new AcceptConnection(clientSocket, this); clientSyncList.Add(clientConn); // Update the counters Interlocked.Increment(ref totalClientCount); Interlocked.Increment(ref currentClientCount); } catch { Console.WriteLine("Accept: Client aborted connection most likely..."); } // Repost the accept io.PostAccept(); } catch (Exception err) { Console.WriteLine("TcpServer.HandleIo: Unknown error: {0}", err.Message); acceptList.Remove(io); io = null; } break; default: break; } }
/// <summary> /// This method removes the IoPacket from the appropriate list. /// </summary> /// <param name="io">The IoPacket to remove from the correct list</param> public override void RemoveIo(IoPacket io) { switch (io.ioType) { case IoPacketType.Accept: acceptList.Remove(io); io = null; break; default: break; } }
/// <summary> /// Increments the sent and received byte counts for all client connections established /// on this TCP server socket. Each AcceptConnection has a reference to the TcpServer /// on which the client connection was accepted -- it then calls this method to /// increment the byte counters. /// </summary> /// <param name="sendCount">Number of bytes sent to increment counter by</param> /// <param name="recvCount">Number of bytes received to increment counter by</param> public void IncrementByteCount(int sendCount, int recvCount) { base.sendByteCount += sendCount; base.receiveByteCount += recvCount; } } } |
Build the project and make sure there is no error that can be seen in the Output window.
The generated DLL file that will be used in the next client and server programs is shown below.