Introduction
I recently needed an inter-process communication mechanism for a .NET project. The project consisted of multiple servers and clients with ASP.NET, Windows Forms, and console applications. Considering the possibilities, I landed on using raw sockets, instead of many of the pre-built mechanism in .NET, like named pipes, NetTcpClient and the Azure Service Bus.
The server in this article is based on the async methods of the System.Net.Sockets class. These methods allow you to support a large number of socket clients, and the only blocking mechanism of the server is when a client connects. The blocking time is neglible, and the server will operate virtually as a multi-threaded socket server.
Background
Raw sockets have the advantage of giving you full control of the communication layer, and has great flexibility when processing different data types. You can even send serialized CLR objects through sockets, although I won't go into that here. This project will show you how to send text between the sockets.
Using the code
To use the code, you instantiate the Server class, and run the Start() method:
Server myServer = new Server();
myServer.Start();
If you plan to host the server in a Windows form, I recommend using a BackgroundWorker, since the socket methods (in particular, the ManualResentEvent) will block the GUI thread.
The Server class:
using System.Net.Sockets;
public class Server
{
private static Socket listener;
public static ManualResetEvent allDone = new ManualResetEvent(false);
public const int _bufferSize = 1024;
public const int _port = 50000;
public static bool _isRunning = true;
class StateObject
{
public Socket workSocket = null;
public byte[] buffer = new byte[bufferSize];
public StringBuilder sb = new StringBuilder();
}
static string Between(string str, string str1, string str2)
{
int i1 = 0, i2 = 0;
string rtn = "";
i1 = str.IndexOf(str1, StringComparison.InvariantCultureIgnoreCase);
if (i1 > -1)
{
i2 = str.IndexOf(str2, i1 + 1, StringComparison.InvariantCultureIgnoreCase);
if (i2 > -1)
{
rtn = str.Substring(i1 + str1.Length, i2 - i1 - str1.Length);
}
}
return rtn;
}
static bool IsSocketConnected(Socket s)
{
return !((s.Poll(1000, SelectMode.SelectRead) && (s.Available == 0)) || !s.Connected);
}
}
ManualResetEvent
is a .NET class that implements events in your socket server. We need this object to signal to our code when we want to release blocking operations. You can experiment with bufferSize
to suit your needs. If you have predictable sized messages, set bufferSize to the message size in bytes. port
is the TCP port to listen on. Beware of using ports that are reserved for other applications. If you want to be able to stop the server gracefully, you need to implement some mechanism for setting _isRunning
to false. This can typically be done by using a BackgroundWorker, where you replace _isRunning with myWorker.CancellationPending. The reason I included _isRunning is to give you a direction on handling cancellation, and show you that the listener can be stopped gracefully.
Between()
and IsSocketConnected()
are helper methods.
Now to the methods. First, the Start() method:
public void Start()
{
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
IPEndPoint localEP = new IPEndPoint(IPAddress.Any, _port);
listener = new Socket(localEP.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(localEP);
while (_IsRunning)
{
allDone.Reset();
listener.Listen(10);
listener.BeginAccept(new AsyncCallback(acceptCallback), listener);
bool isRequest = allDone.WaitOne(new TimeSpan(12, 0, 0));
if (!isRequest)
{
allDone.Set();
}
}
listener.Close();
}
This method initiates the listener socket, and starts waiting for clients to connect. The main pattern in this project is using async delegates. Async delegates are functions that are called asynchronously when the state changes in the caller. isRequest
tells you wether WaitOne
exited because a client connected, or because the timout was reached.
If you have a large number of clients connecting simultaneously, consider increasing the queue parameter of the Listen() method.
Now to the next method, acceptCallback
. This method is called asynchronously by listener.BeginAccept. When the method completes, the listener will immediately listen for new clients.
static void acceptCallback(IAsyncResult ar)
{
Socket listener = (Socket)ar.AsyncState;
if (listener != null)
{
Socket handler = listener.EndAccept(ar);
allDone.Set();
StateObject state = new StateObject();
state.workSocket = handler;
handler.BeginReceive(state.buffer, 0, _bufferSize, 0, new AsyncCallback(readCallback), state);
}
}
acceptCallback
forks another async delegate: readCallback
. This method will read actual data from the socket. I have made my own protocol for sending and receiving data, which is invariant to _bufferSize. Every string sent to the server must be wrapped by <!--SOCKET--> and <!--ENDSOCKET-->. Likewise, the client, when receiving the response from the server, must unwrap the response, which is wrapped by <!--RESPONSE--> and <!--ENDRESPONSE-->
static void readCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket;
if (!IsSocketConnected(handler))
{
handler.Close();
return;
}
int read = handler.EndReceive(ar);
if (read > 0)
{
state.sb.Append(Encoding.UTF8.GetString(state.buffer, 0, read));
if (state.sb.ToString().Contains("<!--ENDSOCKET-->"))
{
string toSend = "";
string cmd = ts.Strings.Between(state.sb.ToString(), "<!--SOCKET-->", "<!--ENDSOCKET-->");
switch (cmd)
{
case "Hi!":
toSend = "How are you?";
break;
case "Milky Way?":
toSend = "No I am not.";
break;
}
toSend = "<!--RESPONSE-->" + toSend + "<!--ENDRESPONSE-->";
byte[] bytesToSend = Encoding.UTF8.GetBytes(toSend);
handler.BeginSend(bytesToSend, 0, bytesToSend.Length, SocketFlags.None
, new AsyncCallback(sendCallback), state);
}
else
{
handler.BeginReceive(state.buffer, 0, _bufferSize, 0
, new AsyncCallback(readCallback), state);
}
}
else
{
handler.Close();
}
}
readCallback
will fork another method, sendCallback
, that will send the response to the client. If the client doesn't close the connection, sendCallback
will signal the socket to listen for more data.
static void sendCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket;
handler.EndSend(ar);
StateObject newstate = new StateObject();
newstate.workSocket = handler;
handler.BeginReceive(newstate.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(readCallback), newstate);
}
I will leave it as an exercise to the reader to write the socket client. The socket client should use the same programming pattern of async callbacks. I hope you enjoyed the article, and that you will excel as a socket programmer!
Points of Interest
I am using this code in a production environment where the socket server is a freetext search engine. SQL Server lacks support for freetext search (you can use freetext indexes, but they are slow and expensive). The socket server loads huge amounts of text data into iEnumerables, and uses Linq to search the text. The response from the socket server is in the order of milliseconds when searching millions of rows of unicode text data. We also use three distributed Sphinx servers (www.sphinxsearch.com). The socket server serves as a cache for the Sphinx servers. If you need a fast freetext search engine, I highly recommend Sphinx.
History
First version.