< VB .NET Asynchronous Client Program Example | Main | Chap 10: HTTP Protocol With .NET >

 


 

Chapter 9 Part 8:

Server or Listening Sockets Programming

 

 

What do we have in this chapter 9 Part 8?

  1. Asynchronous Socket Operations

  2. Posting Asynchronous Operations

  3. Asynchronous Accept

  4. Asynchronous Connect

  5. Asynchronous Data Transfer

  6. Canceling Pending Asynchronous Operations

 

 

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.

 

Table 9-2: Asynchronous Socket Methods

 

Start Method

End Method

Description

BeginAccept()

EndAccept()

Accepts a client connection on a connection-oriented server socket and returns a Socket object for the client connection

BeginConnect()

EndConnect()

Initiates a client connection to the indicated server

BeginReceive()

EndReceive()

Receives data into the specified buffer on the connected socket

BeginReceiveFrom()

EndReceiveFrom()

Receives data into the specified buffer and returns the EndPoint from which the data originated

BeginSend()

EndSend()

Sends the given data buffer on the connected socket

BeginSendTo()

EndSendTo()

Sends the given data buffer to the specified destination

 

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:

 

C#

 

public delegate void AsyncCallback(IAsyncResult ar);

 

Visual Basic .NET

 

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:

 

C#

 

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;

    }

}

 

Visual Basic .NET

 

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.

 

Asynchronous Accept

 

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:

 

C#

 

try

{

    IoPacket ioContext = new IoPacket(IoPacketType.Accept, tcpListenSocket);

    tcpListenSocket.BeginAccept( new AsyncCallback( AcceptCallback ),

        ioContext );

}

catch ( SocketException err )

{

    Console.WriteLine("Socket error: {0}", err.Message );

}

 

Visual Basic .NET

 

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.

 

C#

 

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;

    }

}

 

Visual Basic .NET

 

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.

 

 

Asynchronous Connect

 

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.

 

Asynchronous Data Transfer

 

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:

 

C#

 

public IAsyncResult BeginSend(

    byte[ ] buffer,

    int offset,

    int size,

    SocketFlags socketFlags,

    AsyncCallback callback,

    object state

);

 

Visual Basic .NET

 

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:

 

C#

 

public IAsyncResult BeginReceive(

    byte[ ] buffer,

    int offset,

    int size,

    SocketFlags socketFlags,

    AsyncCallback callback,

    object state

);

 

Visual Basic .NET

 

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:

 

C#

 

Monitor.Enter( tcpSocket );

ioPacket.ReceiveCount = gGlobalReceiveCount++;

tcpSocket.BeginReceive(

    byteArray,

    0,

    byteArray.Length,

    SocketFlags.None,

    new AsyncCallback( recvCallback ),

    ioPacket

    );

Monitor.Exit( tcpSocket );

 

Visual Basic .NET

 

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.

 

Canceling Pending Asynchronous Operations

 

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

 

 

 


 

< VB .NET Asynchronous Client Program Example | Main | Chap 10: HTTP Protocol With .NET >