Using Non-blocking Socket Methods



Using Non-blocking Socket Methods

Asynchronous sockets have been created specifically for the Windows environment. As explained in Chapter 3, traditional Unix network programming does not use asynchronous sockets. Instead, programs contain non-blocking socket methods that check a socket without actually committing to a blocking function. This allows the program to continue executing even if the network operation would normally block the program. If the socket is determined to be ready for the network function, the blocking function is performed.

The .NET Sockets library includes non-blocking socket methods found in the Unix socket library. This allows network programmers to easily port Unix network programs to the Windows environment. This section describes the Poll() and Select() methods that are used to determine whether a network function would block and to help network applications avoid getting stuck on a blocking network function.

The Poll() Method

Often, when you’re attempting to perform a blocking network function such as a Receive() command, you need to have the capability of checking the socket before committing to the command.

The Socket method Poll() gives you just that. It checks a Socket object to see whether a network method call would block or be successfully completed. If the poll indicates that the method would execute without blocking, you’re home free. Otherwise, you can perform some other functions and check again at a later time.

The format of the Poll() method is simple:

bool Poll(int microseconds, SelectMode mode);

It returns a simple boolean value: true if the action would complete, or false if the action would block.

The int parameter allows you to set the amount of time (in microseconds) the Poll() method will wait and watch the socket for the indicated events. Use the SelectMode parameter to specify what type of action to watch for. The SelectMode class enumerates three possible events for the Poll() method to monitor:

SelectRead The SelectRead value for SelectMode will cause the Poll()to return a true value under the following conditions:

  • If an Accept() method call would succeed

  • If data is available on the socket

  • If the connection has been closed

    SelectWrite The SelectWrite value for SelectMode will cause the Poll()to return a true value under the following conditions:

  • If a Connect() method call has succeeded

  • If data can be sent on the socket

    SelectError The SelectError value for SelectMode will cause the Poll() method to return a true value under the following conditions:

  • If a Connect() method call has failed

  • If out-of-band data is available and the Socket OutOfBandInline property has not been set.

You can check the return value of the Poll() method to determine if a socket is ready for a blocking function or not, based on the SelectMode value set. The resulting Poll() method call would look like this:

result = sock.Poll(1000000, SelectMode.SelectRead);

The Socket object, sock, would be checked for 1,000,000 microseconds (one second) to determine if data is present on the socket. If the return value is true, a Receive() method will complete successfully without blocking.

A Sample Poll() Program

The TcpPollSrvr.cs program, Listing 8.4, demonstrates the Poll() method at work in a server program.

Listing 8.4: The TcpPollSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class TcpPollSrvr
{
  public static void Main()
  {
   int recv;
   byte[] data = new byte[1024];
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any,
               9050);
   Socket newsock = new
     Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   newsock.Bind(ipep);
   newsock.Listen(10);
   Console.WriteLine("Waiting for a client...");
   bool result;
   int i = 0;
   while(true)
   {
     i++;
     Console.WriteLine("polling for accept#{0}...", i);
     result = newsock.Poll(1000000, SelectMode.SelectRead);
     if (result)
     {
      break;
     }
   }
   Socket client = newsock.Accept();
   IPEndPoint newclient =
          (IPEndPoint)client.RemoteEndPoint;
   Console.WriteLine("Connected with {0} at port {1}",
           newclient.Address, newclient.Port);
 
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   client.Send(data, data.Length,
            SocketFlags.None);
   
   i = 0;
   while(true)
   {
     Console.WriteLine("polling for receive #{0}...", i);
     i++;
     result = client.Poll(3000000, SelectMode.SelectRead);
     if(result)
     {
      data = new byte[1024];
      i = 0;
      recv = client.Receive(data);
      if (recv == 0)
        break;
   
      Console.WriteLine(
         Encoding.ASCII.GetString(data, 0, recv));
      client.Send(data, recv, 0);
     }
   }
   Console.WriteLine("Disconnected from {0}",
            newclient.Address);
   client.Close();
   newsock.Close();
  }
}

After the server socket is created, it is bound to a local IPEndPoint object and placed in listen mode using the standard Bind() and Listen() Socket methods. In a normal blocking program, the next step would be to use the Accept() method to wait for an incoming connection attempt from a client. This would cause the program to wait at that point and prevent it from doing any other work until an incoming connection is received.

Instead, in this example, the non-blocking Poll() method polls the socket to determine if an incoming connection is available:

while(true)
{
  Console.WriteLine("polling for receive #{0}...", i
  i++;
  result = client.Poll(3000000, SelectMode.SelectRead);
  if(result)
  {
   data = new byte[1024];
   i = 0;
   recv = client.Receive(data);
   if (recv == 0)
     break;
  
   Console.WriteLine(
      Encoding.ASCII.GetString(data, 0, recv));
   client.Send(data, recv, 0);
  }
}

Here, a while() loop is created to continuously loop, checking the socket for the SelectRead attribute. If the SelectRead attribute is set, the Poll() method returns a value of true. An incoming connection is then available and the Accept() method can be safely called without blocking. The Poll() method is set to wait one second each time.

Between calls to the Poll() method, the program can perform any other function, including calls to other methods. This program merely increments a counter and displays it on the console to inform you that the program is still running while it is polling.

After an incoming connection attempt is connected, another poll loop is created to wait for incoming data for the Receive() method. Again, between Poll() calls, the program can perform any other operation that is needed.

End example

Testing the Poll() Method

You can test this program by running it from the command prompt, watching it poll, and waiting for a client connection. Either the SimpleTcpClient program, shown in Chapter 5, or the fancy AsyncTcpClient program created earlier in this chapter, can connect to TcpPollSrvr. After running TcpPollSrvr for a few seconds, start the client program to establish a connection. Once the connection attempt is detected, Poll()returns a true value and the program continues, sending the welcome banner to the client and then entering another loop to receive data.

Following is sample output from this test:

C:\>TcpPollSrvr
Waiting for a client...
polling for accept#1...
polling for accept#2...
polling for accept#3...
polling for accept#4...
polling for accept#5...
polling for accept#6...
polling for accept#7...
Connected with 192.168.1.2 at port 1985
polling for receive #0...
polling for receive #1...
polling for receive #2...
polling for receive #3...
This is a test message.
polling for receive #0...
polling for receive #1...
polling for receive #2...
This is another test message.
polling for receive #0...
Disconnected from 192.168.1.2
C:\>

As you can see, the program polls and waits for a client connection. Once the connection is established, the program polls again, waiting for data. As each new message is received, a new poll loop is started, waiting again for data. When the remote client closes the connection, Poll()detects the closure and returns a true value. Because the return value was true, the Receive() method is called and a zero value is returned.

Note 

Unlike the Accept() or Receive() methods, the Connect() method must be started before the Poll() method can be used. Therefore, to ensure that the socket will not block, you must manually use the non-blocking socket option.

The Poll() method works well for watching a single socket for activity but can get extremely complex if you are trying to monitor multiple sockets. For that situation you can use the Select() method.

The Select() Method

The Select() method of the Socket class polls one or more sockets for blocking functions. As sockets become available for reading or writing, the Select() method can determine which ones are ready to use and which ones would block if used.

The Select() Format

Because the Select() method is declared as a static method, it cannot be used on an instance of a Socket object. Instead, you must use it from a generic Socket declaration: Here’s the format:

Socket.Select(IList checkRead, IList checkWrite, IList checkError, int microseconds)

The three IList objects represent three categories to monitor for socket activity:

  • checkRead monitors the specified sockets for the ability to read data from the socket.

  • checkWrite monitors the specified sockets for the ability to write data to the socket.

  • checkError monitors the specified sockets for error conditions.

The IList object represents a collection of objects that can be individually accessed by an index value. When the Select() method exits, each IList object will be modified to only contain Socket objects that meet the criteria of the position (read, write, or error).

The IList object must allow the Select() method to manipulate the entries in the list, because sockets are added and removed as data becomes available. The easiest way to create an IList object that can be manipulated is to use an ArrayList object. The ArrayList class can be found in the System.Collections namespace. In the case of the Select() method, an ArrayList of Socket objects is used. The microseconds parameter determines how long the Select() method will monitor the sockets before returning to the program (similar to the Poll() method).

For example, the following code snippet creates three separate sockets and monitors them for new connections:

IPEndPoint iep1 = new IPEndPoint(IPAddress.Any, 9050);
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sock.Bind(iep1);
sock.Listen(5);
Socket newsock1 = sock.Accept();
Socket newsock2 = sock.Accept();
Socket newsock3 = sock.Accept();
ArrayList socketList = new ArrayList(3);
socketList.Add(newsock1);
socketList.Add(newsock2);
socketList.Add(newsock3);
Socket.Select(sockList, null, null, 10000000);
for(int I = 0; I < sockList.Count; i++)
{
  client = (Socket)sockList[i];
  data = new byte[1024];
  recv = client.Receive(data);
}

This code snippet demonstrates the placing of three separate sockets in an ArrayList and passing it to the Select() method for monitoring. The three sockets are the result of client connections to a single socket placed in listen mode. The Select() method waits ten seconds and then exits. The result is that the sockList ArrayList will now contain any sockets that have data waiting to be read.

Warning 

It is very important to remember that the ArrayList passed to the Select() method will be modified. On return, it will only contain the sockets that pass the Select filter. If you need to use the original ArrayList of sockets, make a copy of it before passing it to the Select() method.

After the Select() method returns, the individual sockets within the ArrayList can be extracted and the data read.

A Sample Select() Program

This section demonstrates how to use the Select() method to monitor more than one socket at a time in a program. This technique is especially useful for server programs that must service multiple clients simultaneously.

The Select() Server Program

The SelectTcpSrvr.cs program (Listing 8.5) uses the Select() method to monitor two separate clients at the same time. It creates a single socket, places it in listen mode, and then waits for two clients to connect to the socket. Each client generates its own separate socket, which is placed in an ArrayList. The ArrayList of sockets is then monitored using the Select() method for incoming data. When data is detected on a socket, it is echoed back to the same client.

Listing 8.5: The SelectTcpSrvr.cs program
Start example
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Text;
class SelectTcpSrvr
{
  public static void Main()
  {
   ArrayList sockList = new ArrayList(2);
   ArrayList copyList = new ArrayList(2);
   Socket main = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
   IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9050);
   byte[] data = new byte[1024];
   string stringData;
   int recv;
   main.Bind(iep);
   main.Listen(2);
   Console.WriteLine("Waiting for 2 clients...");
   Socket client1 = main.Accept();
   IPEndPoint iep1 = (IPEndPoint)client1.RemoteEndPoint;
   client1.Send(Encoding.ASCII.GetBytes("Welcome to my server"));
   Console.WriteLine("Connected to {0}", iep1.ToString());
   sockList.Add(client1);
   Console.WriteLine("Waiting for 1 more client...");
   Socket client2 = main.Accept();
   IPEndPoint iep2 = (IPEndPoint)client2.RemoteEndPoint;
   client2.Send(Encoding.ASCII.GetBytes("Welcome to my server"));
   Console.WriteLine("Connected to {0}", iep2.ToString());  
   sockList.Add(client2);
   main.Close();
   while(true)
   {
     copyList = new ArrayList(sockList);
     Console.WriteLine("Monitoring {0} sockets...", copyList.Count);
     Socket.Select(copyList, null, null, 10000000);
     foreach(Socket client in copyList)
     {
      data = new byte[1024];
      recv = client.Receive(data);
      stringData = Encoding.ASCII.GetString(data, 0, recv);
      Console.WriteLine("Received: {0}", stringData);
      if (recv == 0)
      {
        iep = (IPEndPoint)client.RemoteEndPoint;
        Console.WriteLine("Client {0} disconnected.", iep.ToString());
        client.Close();
        sockList.Remove(client);
        if (sockList.Count == 0)
        {
         Console.WriteLine("Last client disconnected, bye");
         return;
        }
      }
      else
        client.Send(data, recv, SocketFlags.None);
     }
   }
  }
}

The SelectTcpSrvr program starts off simply enough by creating a TCP socket to listen for incoming connections. It then waits for two clients to connect to the server by using two separate Accept() method calls. Each client connection is saved in a separate Socket object, and each Socket object is added to the ArrayList sockList to be used by the Select() method.

After the ArrayList object is created, the program enters a loop checking the ArrayList sockets for incoming data using the Select() method. To allow for the fact that the ArrayList used in the Select() method will be destroyed, a copy is created to use on each iteration:

copyList = new ArrayList(sockList);
Console.WriteLine("Monitoring {0} sockets...", copyList.Count);
Socket.Select(copyList, null, null, 10000000);

After either one of the sockets receives data, or 10 seconds elapse (whichever comes first), the Select() method exits. The copyList ArrayList object will contain any sockets that have received data. The for loop iterates through all of the sockets returned in the copyList array.

The first step in the for loop is to get the socket that has received data. The Socket object contained in the copyList ArrayList is copied to the client Socket object. Then it can be used as any normal socket, using the Receive() method to receive the waiting data:

client = (Socket)copyList[i];
data = new byte[1024];
recv = client.Receive(data);

The return value from the Receive() method has to be examined to see if the remote client disconnected from the session. This can be determined by detecting a zero return value. If the remote client disconnected, you must close the socket and remove it from the ArrayList. This is easily done using the Remove() method:

if (recv == 0)
{
  iep = (IPEndPoint)client.RemoteEndPoint;
  Console.WriteLine("Client {0} disconnected.", iep.ToString());
  client.Close();
  sockList.Remove(client);
  if (sockList.Count == 0)
  {
   Console.WriteLine("Last client disconnected, bye");
   return;
  }
}
End example
Warning 

When no more sockets are left in the ArrayList, be sure to terminate the Select() loop, or you will get an Exception error.

Finally, if the socket did receive actual data, it is echoed back to the originating client using the standard Send() method. After the for loop completes, the whole process can start over again (after the original ArrayList of sockets has been recopied back into the working copy).

The Select() Client Program

The SelectTcpClient.cs program (Listing 8.6) is a simple client program that connects to the specified server and waits for the customer to enter a text message on the console. The message is sent to the SelectTcpSrvr program, which echoes it back to the client. The message is displayed on the console, and the program waits for another message to be entered. The SelectTcpClient program itself does not need to use the Select() method because it only connects with one server.

Listing 8.6: The SelectTcpClient.cs program
Start example
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Text;
class SelectTcpClient
{
  public static void Main()
  {
   Socket sock = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
   IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
   byte[] data = new byte[1024];
   string stringData;
   int recv;
   sock.Connect(iep);
   Console.WriteLine("Connected to server");
   recv = sock.Receive(data);
   stringData = Encoding.ASCII.GetString(data, 0, recv);
   Console.WriteLine("Received: {0}", stringData);
   while(true)
   {
     stringData = Console.ReadLine();
     if (stringData == "exit")
      break;
     data = Encoding.ASCII.GetBytes(stringData);
     sock.Send(data, data.Length, SocketFlags.None);
     data = new byte[1024];
     recv = sock.Receive(data);
     stringData = Encoding.ASCII.GetString(data, 0, recv);
     Console.WriteLine("Received: {0}", stringData);
   }
   sock.Close();
  }
}
End example

Testing the SelectTcpClient.cs Program

To test this setup, you need to have three command-prompt windows open—one for the SelectTcpSrvr program and two others for the SelectTcpClient program. You can run the SelectTcpClient programs from other workstations on the network if you want; just remember to set the IPEndPoint value to the server address.

When you start the SelectTcpSrvr program, it waits for two connections from clients. The first client that connects to the server will block, waiting for the second connection to complete. After both clients are connected to the server, either one can send messages to the server at any time.

The server program monitors both sockets for incoming data. The Console.WriteLine() method displays status information so you can see what is happening on the server at any time. Here’s what the output from the server should look like:

C:\>SelectTcpSrvr
Waiting for 2 clients...
Connected to 127.0.0.1:1893
Waiting for 1 more client...
Connected to 127.0.0.1:1894
Monitoring 2 sockets...
Monitoring 2 sockets...
Received: This is a test message
Monitoring 2 sockets...
Received: This is another test message
Monitoring 2 sockets...
Received:
Client 127.0.0.1:1894 disconnected.
Monitoring 1 sockets...
Received: This is a final test message
Monitoring 1 sockets...
Received:
Client 127.0.0.1:1893 disconnected.
Last client disconnected, bye
C:\>

The server accepts two client connections and goes into the while loop to monitor the sockets for incoming data. As each client sends a message to the server, it is processed and echoed back to the proper client. Figure shows Analyzer output from monitoring the two clients and the server.

Click To expand
Figure: Monitoring the SelectTcpSrvr program with the Analyzer program

The trace shows two client connections, one using port 1031 and the other using port 1032. The server handles each client TCP session independently. When one client disconnects from the server, the other client can still send messages to the server for processing. When the last client disconnects, the server stops processing sockets and exits.

Tip 

Although the example shown in this section manually creates two client connections, the Select() method can also be used to accept many clients for a single server. The main socket can be polled for new connection attempts and passed to an Accept() method, which will assign each connection a separate socket and place it in the ArrayList to be monitored.

 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows