|
Asynchronous Socket Operations
Let discuss what we have done in the previous program examples. The key to writing server applications that scale to many thousands of concurrent connections is using asynchronous socket calls. Chapter 3 introduced the concept of threading as well as asynchronous operations. Using a separate thread to handle each client connection along with blocking I/O calls is acceptable if the server is not expected to handle many connections. However, threads are an expensive resource, and depending on the physical resources available, the maximum number of threads that can be created could be small. If the server is expected to handle thousands of connections, asynchronous socket calls must be used to ensure efficient use of the local resources. The Socket class implements asynchronous socket operations using completion ports on Windows NT, Windows 2000, Windows XP, and Windows Server 2003. On Windows 9x and Windows Me, overlapped I/O is used. Table 9-2 lists the asynchronous Socket class methods. Note that the maximum number of outstanding asynchronous calls is limited only by local resources.
Posting Asynchronous Operations
As we saw in Chapter 3, all asynchronous socket operations take a delegate and a context object. The delegate is a method that’s invoked when the asynchronous operation completes either successfully or with an error. The following code shows the prototype for the delegate: |
public delegate void AsyncCallback(IAsyncResult ar);
Public Delegate Sub AsyncCallback(ByVal ar As IAsyncResult)
Perhaps the most important part of posting and managing multiple asynchronous operations is the context information associated with each Begin operation. This context blob can be any object to identify the operation - typically, it’s a class that contains at a minimum the Socket object on which the asynchronous operation was posted as well as an indicator of the type of operation completed. This context information is especially relevant if multiple asynchronous operations are posted simultaneously, such as sends and receives. The following IoPacket class is an example of the state information associated with an operation:
public enum IoPacketType
{
Accept,
Connect,
Send,
SendTo,
Receive,
ReceiveFrom
}
public class IoPacket
{
public IoPacketType ioType;
public Socket ioSocket;
public IoPacket(ioPacketType type, Socket asyncSocket)
{
ioType = type;
ioSocket = asyncSocket;
}
}
Public Enum IoPacketType
Accept
Connect
Send
SendTo
Receive
ReceiveFrom
End Enum
Public Class IoPacket
Public ioType As IoPacketType
Public ioSocket As Socket
Public Sub New(ByVal type As IoPacketType, ByVal asyncSocket As Socket)
ioType = type
ioSocket = asyncSocket
End Sub
End Class
For each operation posted on the socket object, an instance of the IoPacket class is created that indicates the Socket object and operation type for the asynchronous operation posted. This way, when the delegate is executed, the IoPacket context information can be retrieved to determine the Socket and operation just completed. The following few sections discuss posting and processing specific asynchronous Socket operations. This chapter’s AsyncSocket sample provides examples of asynchronous TCP client and server applications.
The BeginAccept() method posts an asynchronous connection accept request on a listening socket. When the delegate registered with BeginAccept() is fired, the EndAccept() method on the listening Socket should be called, which returns a Socket object for the client connection request just accepted. The following code snippet shows how to post a BeginAccept() on an already created TCP listening socket:
try
{
IoPacket ioContext = new IoPacket(IoPacketType.Accept, tcpListenSocket);
tcpListenSocket.BeginAccept( new AsyncCallback( AcceptCallback ),
ioContext );
}
catch ( SocketException err )
{
Console.WriteLine("Socket error: {0}", err.Message );
}
Try
Dim ioContext As IoPacket
ioContext = New IoPacket(IoPacketType.Accept, tcpListenSocket)
tcpListenSocket.BeginAccept(New AsyncCallback(AddressOf AcceptCallback), ioContext)
Catch err As SocketException
Console.WriteLine("Socket error: {0}", err.Message)
End Try
Once the operation is posted, the delegate will be invoked on completion. The delegate is required to call the corresponding method to terminate the asynchronous operation, which in this case is the EndAccept() method. Each terminating operation will return the result of the asynchronous operation that includes the result of the operation as well as the context information.
public static void AcceptCallback( IAsyncResult ar )
{
IoPacket ioContext = (IoPacket) ar.AsyncState;
Socket tcpClient = null;
switch ( ioContext->ioType )
{
case IoPacketType.Accept:
try
{
tcpClient = ioContext.ioSocket.EndAccept( ar );
}
catch ( SocketException err )
{
Console.WriteLine("Socket error: {0}", err.Message);
}
catch ( System.NullReferenceException err )
{
Console.WriteLine("Socket closed: {0}", err.Message);
}
break;
default:
Console.WriteLine("Error: Invalid IO type! ");
break;
}
}
Public Sub AcceptCallback(ByVal ar As IAsyncResult)
Dim ioContext As IoPacket = ar.AsyncState
Dim tcpClient As Socket = Nothing
If ioContext.ioType = IoPacketType.Accept Then
Try
tcpClient = ioContext.ioSocket.EndAccept(ar)
Catch err As SocketException
Console.WriteLine("Socket error: {0}", err.Message)
Catch err As System.NullReferenceException
Console.WriteLine("Socket closed")
End Try
Else
Console.WriteLine("Error: Invalid IO type! ")
End If
End Sub
The delegate retrieves the IoPacket context object from the IAsyncResult passed. It then calls the EndAccept() method for the IoPacketType.Accept operation that completed. Note that the terminating asynchronous method should always be called in a try…except block. If an error occurs on the asynchronous operation, an exception will be thrown when the termination method is called. In the case of an asynchronous accept, if the connecting client resets the connection, an exception will occur. Additionally, the only way to cancel an asynchronous socket operation is to close the Socket object itself via the Close() method. If there are any pending asynchronous operations, the delegate associated with the operation will be invoked and the System.NullReferenceException error is thrown when the corresponding termination method is called. It’s possible and often desirable to post multiple BeginAccept() operations on a TCP listening socket to ensure that a high volume of client connections can be handled simultaneously. Remember that a listening socket can queue a limited number of client connections; by posting multiple asynchronous accept operations, the backlog value is effectively increased. This ability to increase the backlog is true for Socket-based applications running on Windows NT– based operating systems such as Windows 2000, Windows XP, and Windows Server 2003 because the underlying Winsock API call is AcceptEx(). If a listening socket sets the backlog value to the maximum allowed and also posts 100 asynchronous accept operations, the effective backlog is now 300. Chapter 14 will discuss high-performance servers in more detail. |
|
The asynchronous connect method is useful for applications that need to have multiple sockets simultaneously connecting to servers, possibly with already connected sockets performing asynchronous data transfers. Because a single socket can be connected to only one destination, there can be only one outstanding asynchronous connect call on a given socket.
Posting an asynchronous operation is similar to using BeginAccept() except that the BeginConnect() method also takes the EndPoint describing the server to connect to. Aside from this difference, posting and completing asynchronous connect operations follow the same guidelines as the BeginAccept() example shown earlier in this chapter.
Because sockets generally spend most of their time sending and receiving data, the asynchronous data transfer methods are the most useful of all the asynchronous Socket methods. The asynchronous data transfer methods are somewhat simpler than their corresponding synchronous methods because they’re not overloaded - that is, each asynchronous function has only one instance of the method. The asynchronous send method, BeginSend(), is prototyped as shown in the following code:
public IAsyncResult BeginSend(
byte[ ] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
);
Public Function BeginSend( _
ByVal buffer() As Byte, _
ByVal offset As Integer, _
ByVal size As Integer, _
ByVal socketFlags As SocketFlags, _
ByVal callback As AsyncCallback, _
ByVal state As Object _
) As IAsyncResult
The asynchronous BeginSendTo() method is the same as BeginSend() except that the EndPoint parameter describing the datagram destination is specified after the SocketFlags parameter. The asynchronous receive methods are BeginReceive() and BeginReceiveFrom(), for connection-oriented and connectionless receive operations, respectively. The BeginReceive method is prototyped as shown here:
public IAsyncResult BeginReceive(
byte[ ] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
);
Public Function BeginReceive( _
ByVal buffer() As Byte, _
ByVal offset As Integer, _
ByVal size As Integer, _
ByVal socketFlags As SocketFlags, _
ByVal callback As AsyncCallback, _
ByVal state As Object _
) As IAsyncResult
The BeginReceiveFrom() method takes the same parameters as BeginReceive() except for an EndPoint parameter passed by reference after SocketFlags.
We’ve seen all the parameters to these functions before because they’re the same as the synchronous versions except for the callback and state information. However, note that each data transfer operation takes a byte array for sending or receiving data. This array should not be touched or modified between the time the operation is posted and the time the operation completes.
Preserving the array is especially important when sending data on a connection-oriented streaming protocol such as TCP. Because the TCP protocol performs flow control, the receiver can indicate to the sender to stop sending data when the receiver is not handling the incoming data fast enough. At this point, if a BeginSend() is posted, it will not complete until the TCP receiver indicates to resume sending, at which point the local network stack will transmit the byte array and call the associated callback routine. If the buffer is modified, it’s undetermined what the actual data sent will be.
Another consideration when calling the asynchronous data methods is posting multiple operations on the same socket. The order in which asynchronous callbacks are invoked is not guaranteed. If two BeginReceive() operations, R1 and R2, are queued on a connected socket, it’s possible that the callbacks for the completion of these events could be R2 followed by R1, which can cause headaches when processing the results of these operations. This problem can be solved by encapsulating the code that posts the BeginReceive() operation with a Monitor.Enter() call on the Socket object, as follows:
Monitor.Enter( tcpSocket );
ioPacket.ReceiveCount = gGlobalReceiveCount++;
tcpSocket.BeginReceive(
byteArray,
0,
byteArray.Length,
SocketFlags.None,
new AsyncCallback( recvCallback ),
ioPacket
);
Monitor.Exit( tcpSocket );
Monitor.Enter(tcpSocket)
ioPacket.ReceiveCount = gGlobalReceiveCount + 1
tcpSocket.BeginReceive( _
byteArray, _
0, _
byteArray.Length, _
SocketFlags.None, _
New AsyncCallback(AddressOf recvCallback), _
ioPacket _
)
Monitor.Exit(tcpSocket)
In this code, execution is synchronized on the tcpSocket object, which is an instance of a connected TCP Socket class. A global counter is maintained, gGlobalReceiveCount, for marking the order in which the BeginReceive() operations are posted. BeginReceive() is invoked in the synchronized code to ensure that another thread using the same socket doesn’t get swapped in between the assignment of the count and the execution of the operation. Of course, the synchronization is required only if asynchronous socket operations are being posted from multiple threads. When the asynchronous operation completes, the delegate can look at the ReceiveCount() property to determine the order in which the operations were posted.
Because the send and receive data channels are independent, there’s generally no problem with having a single asynchronous send along with a single asynchronous receive operation posted simultaneously.
Once one or more asynchronous operations are posted on a Socket object, it might be necessary to cancel all outstanding asynchronous operations on that socket. The only way to cancel these operations is to close the Socket object via the Close() method. Once the Socket object is closed, all pending asynchronous operations will complete. When the corresponding termination method is called (for example, EndReceive() or EndSend()), a System.NullReferenceExceptionError exception will occur.
In general, it’s always recommended to keep track of the number of pending asynchronous operations on each socket, for several reasons. First, limit the total number of outstanding asynchronous I/O operations. Consider a TCP server with 25,000 client connections on a 100 megabit (Mb) Ethernet connection. It’s not feasible to post a 4 kilobyte (KB) send on each connection because that would amount to over 102 megabytes (MB) of data. It’s likely an out-of- memory exception would occur because the sheer number of outstanding asynchronous operations as well as the number of send buffers submitted.
Second, the number of pending operations needs to be tracked in the event that the socket closes while socket operations are outstanding. This commonly occurs when the process terminates or the connection abortively closes because of a network attack or an excessively idle connection. In this case, where the process is being terminated, the process should close all active Socket objects and then wait for all outstanding asynchronous calls to complete with an exception before exiting the process. This way, you ensure that all events are handled gracefully and prevent timing issues that can cause exceptions (such as when a delegate fires while the process is cleaning up).