Custom socket communication






4.50/5 (2 votes)
Custom socket communication between two .NET projects
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 clientsOnResponseRecieved
: 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.
// after that we send data to all clients
foreach (ComClass<D> com in clients)
{
Boolean sendData = true;
// if we have a handler we check with the handler before sending
if (this.onCanSend != null)
sendData = this.onCanSend.Invoke(com.RemoteAddress, data);
// send
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 backOnNewData
: 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();
// new socket is stored in a list of clients
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.
// after that we send data to all clients
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.
// first we remove dead clients
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);
// size tag on begining
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
<?xml version="1.0" encoding="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....");
// open socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// set timeout
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);
// we have connection status change
if (onConnectionChanged != null)
{
onConnectionChanged.Invoke(socket.Connected);
}
connected = true;
// read timeout from server and set it
socket.ReceiveTimeout = readTimeOut();
}
// wait for read new data
readData();
}
catch (System.Threading.ThreadAbortException)
{
log.Debug("ThreadAbort exception");
// end the loop
break;
}
catch (Exception ex)
{
// errors on comunication are only logged
log.Debug("Exception: " + ex.Message);
socket.Close();
// we have connection status change
if ((connected != socket.Connected) && (onConnectionChanged != null))
{
onConnectionChanged.Invoke(socket.Connected);
}
connected = false;
// after disconnect we sleep for some time - before we try reconnect
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()
{
// read the data - sync with size message
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))
{
// send the new data
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);
// size tag on message begin
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