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
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.
IPAddress addrHost = Dns.Resolve("ftp.microsoft.com").AddressList[0];
IPEndPoint IPHost = new IPEndPoint(addrHost, FTP_PORT);
Socket s = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp );
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
{
if (s == null || !s.Connected) return;
int read = s.EndReceive(ar);
if (read > 0)
{
string msg = Encoding.ASCII.GetString(so.buffer, 0, read);
if (OnReceive != null)
{
OnReceive(_myID, msg);
}
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
{
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)
{
TransmitLock.AcquireWriterLock(-1);
try
{
TransmitQueue.Enqueue(Message);
}
catch {}
finally { TransmitLock.ReleaseWriterLock(); }
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)
{
if (_isConnected) return -1;
IPHostEntry hostEntry = Dns.Resolve(hostName);
if ( hostEntry != null )
{
IPEndPoint endPoint = new
IPEndPoint(hostEntry.AddressList[0], serviceport);
_socket.Connect(endPoint); OnConnect(_myID, true);
ThreadPool.QueueUserWorkItem
(new WaitCallback(ReceiveThreadEntryPoint));
ThreadPool.QueueUserWorkItem
(new WaitCallback(SendThreadEntryPoint));
_isConnected = true;
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;
StopEvent.Set();
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
{
while( true )
{
WaitHandle[] handles = new WaitHandle[1];
handles[0] = StopEvent;
if ( _socket != null && _socket.Connected )
{
try
{
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 )
{
break;
}
}
catch {}
}
}
}
catch {}
}
public void SendThreadEntryPoint(object state)
{
try
{
Queue workQueue = new Queue();
while( true )
{
WaitHandle[] handles = new WaitHandle[2];
handles[0] = StopEvent;
handles[1] = DataReady;
if( WaitHandle.WaitAny(handles) == 0 )
{
break;
}
else if (_socket != null && _socket.Connected)
{
TransmitLock.AcquireWriterLock(-1);
try
{
workQueue.Clear();
foreach( string message in _transmitQueue)
{
workQueue.Enqueue(message);
}
TransmitQueue.Clear();
}
catch {}
finally
{
TransmitLock.ReleaseWriterLock();
}
foreach( string message in workQueue )
{
SocketStateObject so2 = new SocketStateObject(_socket);
byte[] buff = Encoding.ASCII.GetBytes(message);
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.