Introduction
This article describes a simple custom socket communication useful in inter-process communication. The attached classes can be used for any kind of data exchange.
The communication is client – server, server accepts one or more client connections and sends custom data to all connected clients. The clients can also send
some data to the server.
Using the code
Server
The socket server is implemented in a class SocketSender
. We start the server using this code:
SocketSender<SocketData,ResponseData> socketSender = new SocketSender<SocketData,ResponseData>(8000, 10000, true);
socketSender.OnClientConnect += new SocketSender<SocketData,ResponseData>.ClientConnect(clientConnected);
socketSender.OnResponseRecieved += new SocketSender<SocketData, ResponseData>.ResponseRecieved(responseRecieved);
socketSender.OnClientRemove += new SocketSender<SocketData, ResponseData>.ClientRemove(socketSender_OnClientRemove);
socketSender.OnCanSend += new SocketSender<SocketData, ResponseData>.CanSend(socketSender_OnCanSend);
socketSender.Start();
In the constructor, we set the server listening port to 8000 and LifeTickTimeout
in ms. LifeTickTimeout
is the time between two lifetick packets
the server sends to keep connections with clients active. The third parameter is a simple boolean if SocketSender starts the lifetick timer. We also define the data that
the server sends to clients (SocketData
class)
and the data the clients sends to the server (ResponseData
class). We attach the server events:
OnClientConnect
: this event is fired when a new client is connected. OnClientRemove
: this event is fired when a client is removed from the list of server clients OnResponseRecieved
: this event is fried when a client sends some response to the server. OnCanSend
: this event is fired before sending data to the client.
The server sends the data to all clients with:
socketSender.SendData(new SocketData(num, DateTime.Now));
Data is send using a new thread so this does not stop the execution of the main thread. Before send to each client the OnCanSend event is checked.
foreach (ComClass<D> com in clients)
{
Boolean sendData = true;
if (this.onCanSend != null)
sendData = this.onCanSend.Invoke(com.RemoteAddress, data);
if (sendData)
com.SendData(data);
}
In this example the server is sending a simple class with an integer and
DateTime
data, but the SocketData
can be any kind of .NET class that can be serialized to XML.
Client
The socket clients are implemented in the SocketReciever
class. We initialize the client using this code:
SocketReciever<SocketData,ResponseData> socketReciver =
new SocketReciever<SocketData,ResponseData>("localhost", 8000);
socketReciver.OnConnectionChanged +=
new SocketReciever<SocketData,ResponseData>.ConnectionChanged(socketReciver_OnConnectionChanged);
socketReciver.OnNewData += new SocketReciever<SocketData,ResponseData>.NewData(socketReciver_OnNewData);
socketReciver.Start(5000);
In the constructor we set the server hostname ("localhost") and the listening port of the server. We attach two event handlers:
OnConnectionChanged
: this event is fired when the connection status is changed from connected to disconnected, or back OnNewData
: this event is fired when the clients receive some new data from the server
The start method accepts the reconnect timeout parameter in ms. This is the time clients wait after disconnect from server to retry connect. If this parameter is 0 - client does not do reconnection.
As mentioned before, the client can also send some responses to the server using this code:
socketReciver.SendResponse(new ResponseData(readLine, DateTime.Now));
The response class in this example is a simple class with a String
and DateTime
data.
How it works
Client connections
The server class uses System.Net.Sockets.Socket
for accepting client connections. When a new connection occurs the server creates
a new class
ComClass
, sends the TimeOut
message to the client, and adds this class on a list of active clients.
Socket newConnection = sock.Accept();
lock (clients)
{
ComClass<D> newClient = new ComClass<D>(newConnection, clientResponse);
newClient.SendTimeOut(lifeTickTimeout + TIMEOUT_INC);
clients.Add(newClient);
newClient.StartRead();
log.Debug("Clients: " + clients.Count.ToString());
}
The ComClass
starts reading client responses using the method
StartRead()
.
When the server sends data using SendData()
- it loops the clients list and calls
SendData
on all active ComClass
es.
foreach (ComClass<D> com in clients)
{
com.Send(data);
}
Before sending the list is also checked for dead clients and this clients are removed from the list.
int i = 0;
while (i < clients.Count)
{
if (clients[i].IsDead)
{
clients.RemoveAt(i);
}
else
{
i++;
}
}
Data sending
The ComClass
is also responsible for data sending to a connected client. The data is sent using a StreamWriter
.
We serialize the data object into XML, and then we calculate the XML length. Data is sent with by writing two lines:
- first line: size of data formatted: size=xxxxxx
- second line: serialized object in
XML
lock (m_writer)
{
try
{
StringBuilder sBuilder = new StringBuilder();
StringWriter sWriter = new StringWriter(sBuilder);
dataSerializer.Serialize(sWriter, (D)data);
m_writer.WriteLine(CreateSizeString(sBuilder.Length));
m_writer.WriteLine(sBuilder.ToString());
m_writer.Flush();
}
catch (Exception ex)
{
log.Debug(ex.Message);
this.isDead = true;
this.Dispose();
}
}
Here is the example row data that is written to the socket:
size=000240
="1.0"="utf-16"
<SocketData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Number>10</Number>
<Time>2012-12-08T14:29:56.2034557+01:00</Time>
</SocketData>
Server also sends two special messages to clients: timeout message and lifetick messages.
TimeOut
message
A timeout message is sent to the client immediately after connecting. Time is specified in ms and on the client side this timeout is used for socket
RecieveTimeout
.
The message is formatted:
Timeout=0012000
On the server side this timeout interval is the time between two lifetick messages incremented
by a second or two (the time needed for sending a lifetick).
LifeTick message
The lifetick message is important to keep the clients connected.
The server sends a lifetick in a user defined interval specified in the constructor.
The lifetick message is a message with no data so clients receive only the size line with size =0.
Receiving data
The data is received in an infinite loop in the class SocketReciever
:
try
{
if ((socket == null) || !socket.Connected)
{
log.Debug("Socket not connected - connect....");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.ReceiveTimeout = RECIEVE_TIMEOUT;
socket.Connect(hostname, port);
m_reader = new StreamReader(new NetworkStream(socket), Encoding.Unicode);
m_writer = new StreamWriter(new NetworkStream(socket), Encoding.Unicode);
if (onConnectionChanged != null)
{
onConnectionChanged.Invoke(socket.Connected);
}
connected = true;
socket.ReceiveTimeout = readTimeOut();
}
readData();
}
catch (System.Threading.ThreadAbortException)
{
log.Debug("ThreadAbort exception");
break;
}
catch (Exception ex)
{
log.Debug("Exception: " + ex.Message);
socket.Close();
if ((connected != socket.Connected) && (onConnectionChanged != null))
{
onConnectionChanged.Invoke(socket.Connected);
}
connected = false;
if (running)
if (reconnectSleep > 0)
{
log.Debug("Reconnect sleep: " + reconnectSleep.ToString() + " ms");
Thread.Sleep(reconnectSleep);
}
else
{
log.Debug("Reconnect disabled");
running = false;
}
}
The method readData
parses the data received and deserializes the data object:
private void readData()
{
int size = -1;
do
{
String message = m_reader.ReadLine();
size = getMessageSize(message);
}
while (size < 0);
char[] buf = new char[BUF_SIZE];
StringBuilder sb = new StringBuilder();
while (sb.Length < size)
{
int len = m_reader.Read(buf, 0, size - sb.Length);
sb.Append(new string(buf, 0, len));
}
log.Debug("Deserialization");
StringReader sReader = new StringReader(sb.ToString());
D sd = (D)dataSerializer.Deserialize(sReader);
if ((sd != null) && (onNewData != null))
{
onNewData.Invoke(sd);
}
}
The SocketReciever
waits for data for RECIEVE_TIMEOUT
and after that closes the connection and tries to reconnect to the server.
Sending responses
SocketReciever
can also send responses to the server. The communication protocol is the same - two lines with data size and
XML data. The sending of responses is done in a new thread:
private void sendResponseInThread(Object data)
{
if (m_writer != null)
lock (this.m_writer)
try
{
StringBuilder sBuilder = new StringBuilder();
StringWriter sWriter = new StringWriter(sBuilder);
responseSerializer.Serialize(sWriter, (R)data);
m_writer.WriteLine(CreateSizeString(sBuilder.Length));
m_writer.WriteLine(sBuilder.ToString());
m_writer.Flush();
}
catch (Exception ex)
{
log.Debug(ex.Message);
}
}
Conclusion
We wrote this communication object for a digital signage project. This code
has worked fine a few years now.
We use some very useful .NET concepts:
- .NET Socket
- XML serialization and deserialization
- Multi-threading
The original code was enhanced with a lifetick packet sending an automatic
receive timeout setting on the client side. The client also supports reconnection to server after connection loss.
History
- 8/12/2012 - Initial release.
- 11/12/2012 - Lifetick sending / Timeout sending / Client reconnect.
- 13/12/2012 - Added OnClientRemove and OnCanSend events