Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Asynchronous Socket Communications Using the .NET Framework

0.00/5 (No votes)
31 Aug 2003 1  
An article on asynchronous socket communications using the .NET framework.

Introduction

I was recently writing a server-based component that happened to need TCP/ IP socket communication in the .NET framework. I discovered that the .NET framework has enhanced and, in some ways, bettered the existing socket classes offered by the MFC framework. The MFC classes work fine for the framework they were designed for - MFC and the docucentric paradigm. The MFC framework is well-suited for GUI applications and performed very well in that arena, but fell short in other areas such as server-based applications. The CAscyncSocket performed reasonably well asynchronous socket operations but was still tied to the MFC framework and the bloat that comes with it. Along came the .NET framework socket and network classes. These classes are, in my opinion, an excellent implementation of the socket API in C#. They logically wrap communication using the TcpClient, TcpListener, and IpEndpoint classes. These classes are responsible for two-way communications over a socket connection, listening for new socket connection requests, and wrapping endpoints for TCP/ IP communications, respectively. This is still not enough to create a reusable asynchronous socket library, however, as some sort of threading is required for robust asynchronous communications. The .NET framework threading classes, in my opinion, are easier to use than ever. The threading classes contain many thread API functions encapsulated as static functions. Construction is also very straight-forward. This brings us to a related topic: Thread Pooling. Thread pooling can be used to efficiently manage multiple threads. If your application has several small tasks that require multiple threads, by using the ThreadPool class you can take advantage of system controlled threads.

Using the code

//add the two threads for Receive and transmit

ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveThreadEntryPoint));
ThreadPool.QueueUserWorkItem(new WaitCallback(SendThreadEntryPoint));

The code above illustrates how to add threads to the ThreadPool class. It creates two threads for sending and receiving data and adds them to the ThreadPool using the QueueUserWorkItem. The WaitCallback object is a delegate object for the ThreadPool class - it wraps the thread function as a callback. The system will queue each thread and invoke the callback - all without any further interaction. The function QueueUserWorkItem is shown below.

public static bool QueueUserWorkItem(
   WaitCallback callBack,
   object state
);
Parameters
  • callBack

    A WaitCallback representing the delegate to invoke, when a thread in the thread pool picks up the work item.

  • state

    The object that is passed to the delegate when serviced from the thread pool.

The .NET framework Socket class, as it's name implies, wraps the Berkeley socket implementation and is the lowest level of socket access that can be obtained. The code below shows how to connect to a remote host using the Socket class.

// find an endpoint using DNS

IPAddress addrHost = Dns.Resolve("ftp.microsoft.com").AddressList[0];

// create an endpoint

IPEndPoint IPHost = new IPEndPoint(addrHost, FTP_PORT);

// create a TCP socket

Socket s = new Socket(AddressFamily.InterNetwork, 
               SocketType.Stream, ProtocolType.Tcp );

// connect to host described by endpoint

s.Connect(IPHost);

The code above will create a TCP/ IP endpoint that resolves to Microsoft's public FTP site. It will then create a TCP/ IP socket for stream-based communications. Finally, the Connect method is called to connect to the endpoint created earlier.

Let's look at the Socket constructor.

public Socket(
  AddressFamily addressFamily,
  SocketType socketType,
  ProtocolType protocolType
);
Parameters
  • addressFamily
  • socketType
  • protocolType

[From MSDN]

The AddressFamily member specifies the addressing scheme that a Socket instance uses to resolve an address. For example, InterNetwork indicates that an IP version 4 address is expected when a Socket connects to an endpoint. The SocketType member specifies the type of socket an instance of the Socket class represents. SocketType implicitly indicates which ProtocolType will be used within an AddressFamily. For example when the SocketType is Dgram, the ProtocolType is always UDP. When the SocketType is Stream, the ProtocolType is always TCP. The ProtocolType specifies the protocols that the Socket class supports. The ProtocolType enumeration is used by the Socket class to indicate to the Windows Socket API, the requested protocol for the socket. Low-level driver software for the requested protocol must be present on the computer for the Socket to be created.

The final piece of the asynchronous puzzle lies in Socket functions BeginRead, BeginWrite, EndRead, and EndWrite. Along with a little thread synchronization magic and we have all the tools for our Asynchronous Socket Manager.

IAsyncResult iar;
SocketStateObject so2 = new SocketStateObject(_socket);
iar = _socket.BeginReceive(so2.buffer,
0, SocketStateObject.BUFFER_SIZE, 0,
new AsyncCallback(AsynchReadCallback), so2);

The code snippet above shows usage of the Socket method, BeginReceive. Let's look at that method in a little more detail.

public IAsyncResult BeginReceive(
   byte[] buffer,
   int offset,
   int size,
   SocketFlags socketFlags,
   AsyncCallback callback,
   object state
);
Parameters
  • buffer

    The storage location for the received data.

  • offset

    The zero-based position in the buffer parameter, at which to store the received data.

  • size

    The number of bytes to receive.

  • socketFlags

    A bitwise combination of the SocketFlags values.

  • callback

    The AsyncCallback delegate.

  • state

    An object containing state information for this request.

      The AsyncCallback delegate deserves a little attention. This is where we will implement our data processing, as the BeginXXX methods initiate the asynchronous operation only. We must supply a delegate function to process the results from the asynchronous operation. Below is an example of using an AsynchCallback delegate function.

      private void AsynchReadCallback(IAsyncResult ar)
      {
          SocketStateObject so = (SocketStateObject)ar.AsyncState;
          Socket s = so.WorkSocket;
      
          try
          {
              // sanity check
      
              if (s == null || !s.Connected) return;
      
              // call EndRecieve which will return the number of bytes read
      
              int read = s.EndReceive(ar);
              if (read > 0)
              {
                  // get the buffer as a string
      
                  string msg = Encoding.ASCII.GetString(so.buffer, 0, read);
                  if (OnReceive != null)
                  {
                      // call delegate to inform client
      
                      OnReceive(_myID, msg);
                  }
      
                  // and start receiving more
      
                  s.BeginReceive(so.buffer,
                          0, SocketStateObject.BUFFER_SIZE, 0,
                          new AsyncCallback(AsynchReadCallback), so);
              }
          }
          catch {}
      }
      
      public void AsynchSendCallback(System.IAsyncResult ar)
      {
          SocketStateObject so = (SocketStateObject)ar.AsyncState;
          Socket s = so.WorkSocket;
      
          try
          {
              // sanity check
      
              if (s == null || !s.Connected) return;
              int send = s.EndSend(ar);
          }
          catch {}
      }

      The code above first obtains the user defined object using the AsynchState property of the ar parameter. The Socket stored in the state object is obtained and the EndRecieve method is called on the socket. The receive processing function will loop until there are no more bytes to read, at which point, it re-enters the main loop. While data is left to receive, the receive handler will continue to call BeginRecieve. The send function is much simpler, it will just call EndSend and terminate. It's important to note that EndRecieve and EndSend will most likely block.

      Let's start looking at the required interface for a bare-bones socket manager. We obviously need Send and Receive methods. Let's look at those now. Below is an example of a public Send method.

      public void SendMessage(string Message)
      {
          // queue it...
      
          TransmitLock.AcquireWriterLock(-1);
          try
          {
              TransmitQueue.Enqueue(Message);
          }
          catch {}
          finally { TransmitLock.ReleaseWriterLock(); }
      
          // signal that data was sent
      
          DataReady.Set();
      }

      The code above begins, by using a critical section object to provide thread safety. The message is then added to the transmit queue, and the critical section object is released. Finally, the DataReady event is raised which is an AutoResetEvent object used to inform the send thread, that data is ready to be processed.

      The next public method we need is a Connect method. Please see below for an example.

      public int Connect(string hostName, int serviceport)
      {
          //no need to do anything once connected
      
          if (_isConnected) return -1;
      
          // resolve...
      
          IPHostEntry hostEntry = Dns.Resolve(hostName);
          if ( hostEntry != null )
          {
              // create an end-point for the first address...
      
              IPEndPoint endPoint = new 
                     IPEndPoint(hostEntry.AddressList[0], serviceport);
      
              // connect to host described by endpoint
      
              _socket.Connect(endPoint); OnConnect(_myID, true);
      
              // spin up the threads...
      
              //add the two threads for Receive and transmit
      
              ThreadPool.QueueUserWorkItem
                      (new WaitCallback(ReceiveThreadEntryPoint));
              ThreadPool.QueueUserWorkItem
                      (new WaitCallback(SendThreadEntryPoint));
      
              // set connected flag
      
              _isConnected = true;
      
              // return this unique id
      
              return _myID;
          }
          else
          {
              return -1;
          }
      }

      The code above combines two snippets we have already seen, for connecting a Socket and adding threads to the ThreadPool. The Disconnect method is shown below.

      public void Disconnect()
      {
          if (_isConnected)
          {
              _isConnected = false;
      
              // signal the threads to end
      
              StopEvent.Set();
      
              // now kill the socket
      
              if (_socket != null)
              {
                  _socket.Close();
              }
              _socket = null;
          }
      }

      The code above uses a ManualResetEvent object, StopEvent, to signal the worker threads to terminate their loop.

      Let's start looking at some implementation details. We will begin with the send and receive threads that are executed from the thread pool. Please see below for code examples.

      public void ReceiveThreadEntryPoint(object state)
      {
          try
          {
              // loop forever...
      
              while( true )
              {
                  WaitHandle[] handles = new WaitHandle[1];
                  handles[0] = StopEvent;
      
                  if ( _socket != null && _socket.Connected )
                  {
                      // not disconnected
      
                      try
                      {
                          // start the receive operation
      
                          System.IAsyncResult iar;
                          SocketStateObject so2 = new SocketStateObject(_socket);
                          iar = _socket.BeginReceive(so2.buffer,
                                  0, SocketStateObject.BUFFER_SIZE, 0,
                                  new AsyncCallback(AsynchReadCallback), so2);
      
                          if( WaitHandle.WaitAny(handles) == 0 )
                          {
                              // stop event was signaled, break out of loop
      
                              break;
                          }
                      }
                      catch {}
                  }
              }
          }
          catch {}
      }
      
      public void SendThreadEntryPoint(object state)
      {
          try
          {
              Queue workQueue = new Queue();
      
              // loop...
      
              while( true )
              {
                  WaitHandle[] handles = new WaitHandle[2];
                  handles[0] = StopEvent;
                  handles[1] = DataReady;
      
                  if( WaitHandle.WaitAny(handles) == 0 )
                  {
                      // if the stop event was signalled, time to exit the loop
      
                      break;
                  }
                  else if (_socket != null && _socket.Connected)
                  {
                      // not disconnected
      
                      // go through the queue...
      
                      TransmitLock.AcquireWriterLock(-1);
                      try
                      {
                          workQueue.Clear();
                          foreach( string message in _transmitQueue)
                          {
                              workQueue.Enqueue(message);
                          }
                          TransmitQueue.Clear();
                      }
                      catch {}
                      finally
                      {
                          TransmitLock.ReleaseWriterLock();
                      }
      
                      // loop the outbound messages...
      
                      foreach( string message in workQueue )
                      {
                          SocketStateObject so2 = new SocketStateObject(_socket);
                          byte[] buff = Encoding.ASCII.GetBytes(message);
      
                          // send it...
      
                          System.IAsyncResult iar;
                          iar = _socket.BeginSend(buff,
                                  0, buff.Length, 0,
                                  new AsyncCallback(AsynchSendCallback), so2);
                      }
                  }
              }
          }
          catch {}
      }

      The above illustrates the most complicated code in the entire AsynchSocketManager class. You will see, however, that it is easy to understand after analyzing the previous snippets. The first function is the thread pool managed receive thread. This function loops until a StopEvent is raised, which signifies that a disconnect message was received. If the StopEvent is not raised, the receive thread function calls BeginRecieve. This call will block until data is ready to be received which will be handled by the asynchronous callback AsynchReadCallback which was covered previously. The next function is the send thread function managed by the thread pool. This function loops until either the StopEvent or the DataReady event is raised. If StopEvent is raised the function exits, signifying a disconnect. If DataReady raises, the function will then remove all items currently in the TransmitQueue and add them to workQueue, which are then sent, one at a time, via the asynchronous BeginSend call.

      Points of interest

      Asynchronous socket I/O can be straight-forward, but proper thread safety and synchronization is essential. The ThreadPool object helps manage some of the headaches with terminating threads, especially if the destructor is not called - which can happen if the main thread is terminated unexpectedly. This can leave threads running or in an indeterminate state. The ThreadPool insulates us from that, when we allow it to manage our threads. In my next article I will discuss, using .NET socket classes in a ThreadPool managed socket server, using I/O completion ports.

    • License

      This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

      A list of licenses authors might use can be found here