Introduction
I was looking for a high performance code for a client socket. Previously, I wrote a code based on the traditional asynchronous programming model methods from the Socket
class (BeginSend
, BeginReceive
, and so on). But it didn't fill the performance requirements I needed. Then, I found the new model for event-based asynchronous operations (see "Get Connected with the .NET Framework 3.5" in the September 2007 issue of the MSDN Magazine).
Background
The Asynchronous Programming Model (APM) is used widely in I/O-bound applications to achieve high performance, due to the reduction of blocking threads. APM is implemented in the .NET Framework since its first version, and is improving since then, using new techniques like lambda expressions from C# 3.0. Specifically for socket programming, the new model for APM delivers an easier coding, not to mention the performance benefits. It is done towards the use of the SocketAsyncEventArgs
class to keep the context between I/O operations, which reduces object allocation and the garbage collection working.
The SocketAsyncEventArgs
class is also available in the .NET Framework 2.0 SP1, and the code from this article was written using Microsoft Visual Studio .NET 2008.
Using the Code
To begin with the SocketAsyncEventArgs
class, I studied the example from MSDN, but there was something missing: the AsyncUserToken
class. I understood the class should expose a property Socket
, corresponding to the socket used to perform the I/O operations. But how to keep data received from client until the accept operation ends? Since UserToken
is an Object
, it could accept anything, so I created a Token
class to track the accept operation. Below shown are the modified methods to use the instance of Token
class as the UserToken
.
private void ProcessAccept(SocketAsyncEventArgs e)
{
Socket s = e.AcceptSocket;
if (s.Connected)
{
try
{
SocketAsyncEventArgs readEventArgs = this.readWritePool.Pop();
if (readEventArgs != null)
{
readEventArgs.UserToken = new Token(s, this.bufferSize);
Interlocked.Increment(ref this.numConnectedSockets);
Console.WriteLine("Client connection accepted.
There are {0} clients connected to the server",
this.numConnectedSockets);
if (!s.ReceiveAsync(readEventArgs))
{
this.ProcessReceive(readEventArgs);
}
}
else
{
Console.WriteLine("There are no more available sockets to allocate.");
}
}
catch (SocketException ex)
{
Token token = e.UserToken as Token;
Console.WriteLine("Error when processing data received from {0}:\r\n{1}",
token.Connection.RemoteEndPoint, ex.ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
this.StartAccept(e);
}
}
private void ProcessReceive(SocketAsyncEventArgs e)
{
if (e.BytesTransferred > 0)
{
if (e.SocketError == SocketError.Success)
{
Token token = e.UserToken as Token;
token.SetData(e);
Socket s = token.Connection;
if (s.Available == 0)
{
token.ProcessData(e);
if (!s.SendAsync(e))
{
this.ProcessSend(e);
}
}
else if (!s.ReceiveAsync(e))
{
this.ProcessReceive(e);
}
}
else
{
this.ProcessError(e);
}
}
else
{
this.CloseClientSocket(e);
}
}
private void ProcessSend(SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
Token token = e.UserToken as Token;
if (!token.Connection.ReceiveAsync(e))
{
this.ProcessReceive(e);
}
}
else
{
this.ProcessError(e);
}
}
I also modified the code to show where you can manipulate the message received by the listener. In the example, I created the method ProcessData
in the Token
class to echoing back to the client the received message.
To control the listener lifetime, an instance of the Mutex
class is used. The Start
method, which is based on the original Init
method creates the mutex, and the corresponding Stop
method releases the mutex. These methods are suitable to implement the socket server as a Windows Service.
internal void Start(Int32 port)
{
IPAddress[] addressList =
Dns.GetHostEntry(Environment.MachineName).AddressList;
IPEndPoint localEndPoint =
new IPEndPoint(addressList[addressList.Length - 1], port);
this.listenSocket = new Socket(localEndPoint.AddressFamily,
SocketType.Stream, ProtocolType.Tcp);
if (localEndPoint.AddressFamily == AddressFamily.InterNetworkV6)
{
this.listenSocket.SetSocketOption(SocketOptionLevel.IPv6,
(SocketOptionName)27, false);
this.listenSocket.Bind(new IPEndPoint(IPAddress.IPv6Any,
localEndPoint.Port));
}
else
{
this.listenSocket.Bind(localEndPoint);
}
this.listenSocket.Listen(this.numConnections);
this.StartAccept(null);
mutex.WaitOne();
}
internal void Stop()
{
mutex.ReleaseMutex();
}
Now that we have a socket server, the next step is to create a socket client using the SocketAsyncEventArgs
class. Although the MSDN says that the class is specifically designed for network server applications, there is no restriction in using this APM in a client code. Below, there is the SocketClien
class, written in this way:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace SocketAsyncClient
{
internal sealed class SocketClient : IDisposable
{
private const Int32 ReceiveOperation = 1, SendOperation = 0;
private Socket clientSocket;
private Boolean connected = false;
private IPEndPoint hostEndPoint;
private static AutoResetEvent autoConnectEvent =
new AutoResetEvent(false);
private static AutoResetEvent[]
autoSendReceiveEvents = new AutoResetEvent[]
{
new AutoResetEvent(false),
new AutoResetEvent(false)
};
internal SocketClient(String hostName, Int32 port)
{
IPHostEntry host = Dns.GetHostEntry(hostName);
IPAddress[] addressList = host.AddressList;
hostEndPoint =
new IPEndPoint(addressList[addressList.Length - 1], port);
clientSocket = new Socket(hostEndPoint.AddressFamily,
SocketType.Stream, ProtocolType.Tcp);
}
internal void Connect()
{
SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs();
connectArgs.UserToken = clientSocket;
connectArgs.RemoteEndPoint = hostEndPoint;
connectArgs.Completed +=
new EventHandler<socketasynceventargs />(OnConnect);
clientSocket.ConnectAsync(connectArgs);
autoConnectEvent.WaitOne();
SocketError errorCode = connectArgs.SocketError;
if (errorCode != SocketError.Success)
{
throw new SocketException((Int32)errorCode);
}
}
internal void Disconnect()
{
clientSocket.Disconnect(false);
}
private void OnConnect(object sender, SocketAsyncEventArgs e)
{
autoConnectEvent.Set();
connected = (e.SocketError == SocketError.Success);
}
private void OnReceive(object sender, SocketAsyncEventArgs e)
{
autoSendReceiveEvents[SendOperation].Set();
}
private void OnSend(object sender, SocketAsyncEventArgs e)
{
autoSendReceiveEvents[ReceiveOperation].Set();
if (e.SocketError == SocketError.Success)
{
if (e.LastOperation == SocketAsyncOperation.Send)
{
Socket s = e.UserToken as Socket;
byte[] receiveBuffer = new byte[255];
e.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
e.Completed +=
new EventHandler<socketasynceventargs />(OnReceive);
s.ReceiveAsync(e);
}
}
else
{
ProcessError(e);
}
}
private void ProcessError(SocketAsyncEventArgs e)
{
Socket s = e.UserToken as Socket;
if (s.Connected)
{
try
{
s.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
finally
{
if (s.Connected)
{
s.Close();
}
}
}
throw new SocketException((Int32)e.SocketError);
}
internal String SendReceive(String message)
{
if (connected)
{
Byte[] sendBuffer = Encoding.ASCII.GetBytes(message);
SocketAsyncEventArgs completeArgs = new SocketAsyncEventArgs();
completeArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
completeArgs.UserToken = clientSocket;
completeArgs.RemoteEndPoint = hostEndPoint;
completeArgs.Completed +=
new EventHandler<socketasynceventargs />(OnSend);
clientSocket.SendAsync(completeArgs);
AutoResetEvent.WaitAll(autoSendReceiveEvents);
return Encoding.ASCII.GetString(completeArgs.Buffer,
completeArgs.Offset, completeArgs.BytesTransferred);
}
else
{
throw new SocketException((Int32)SocketError.NotConnected);
}
}
#region IDisposable Members
public void Dispose()
{
autoConnectEvent.Close();
autoSendReceiveEvents[SendOperation].Close();
autoSendReceiveEvents[ReceiveOperation].Close();
if (clientSocket.Connected)
{
clientSocket.Close();
}
}
#endregion
}
}
Points of Interest
I had an experience with a socket server running in a clustered environment. In this scenario, you can't use the first entry in the address list from the host. Instead, you should use the last address, as shown in the Start
method. Another technique presented here is how to set the dual mode for an IP6 address family, which is helpful if you want to run the server in a Windows Vista or Windows Server 2008, which enables IP6 by default.
Both programs use command-line arguments to run. In the client example, you should inform "localhost" as the name of the host instead of the machine name if both the server and the client are running in a machine out of a Windows domain.
History
- 15 January, 2008 - Original version posted
- 28 September, 2010 - Updated article and server example