Introduction
In modern software systems data transfer is usually carried out using serialized objects of types well-known
to both the client and server. However, in many cases continuous streams of bytes and raw TCP sockets are still used (e.g., due to performance or compatibility
considerations). I recently came across such a communication for transfer of financial information, medical data, and “soft” sensors output.
Although TCP sockets are well documented their usage requires writing of tedious and sometimes tricky code. This article presents
a TCP socket wrapper allowing
the developer to use it as a “black box” and saving him/her from dealing with
the implementation details.
The wrapper provides the following features:
- implementation of TCP client (connection initiator) and server (connection acceptor),
- synchronous (blocking) and asynchronous (non-blocking) data sending,
- processing received data in a separate dedicated thread for each socket,
- processing of continuous data stream,
- notification on significant events, errors, and exceptions,
- slow receiver identification by sender,
- possibility to make client wait until server starts,
- automatic configurable reconnection attempts when connection lost.
Most of the above features will be discussed in detail in the article.
Code Description
The wrapper itself is placed in the assembly IL.TcpCommunicationLib.dll. It contains
a base class TcpChannel implementing most of
the functionality, and two derived classes, namely, TcpServer and
TcpClient. The assembly IL.WorkerThreadLib.dll provides a supporting
WorkerThread class for multithreading (this type may be used independently, apart from communication purposes for any
cyclic processing in dedicated threads). To establish communication the developer has to provide
a local IP address for the server and the client (alternatively
it can be obtained automatically), callback methods for received data processing and notifications (optionally), and
- for server: call the static method
TcpServer.StartAcceptSubscribersOnPort() to start listening
for incoming connections on a given port,
- for client: create an object of
TcpClient type and call its method
Connect() to initiate connection to the server.
Communication using the wrapper classes is performed as follows. Both the Server and Client provide / automatically obtain their respective
local IP addresses. The Server starts to listen for incoming connections on
a specified port by calling the static method TcpServer.StartAcceptSubscribersOnPort(). The Client calls
the method Connect() providing the Server's IP address and port that
the Server
is listening on. The Server accepts the Client's call and actually establishes connection with the Client automatically allocating for this connection some port different
from the one listening on. Upon connection established both Server and Client internally call
the method Receive() to receive arrays of bytes sent by the other side.
TcpChannel.Receive() receives data from the socket synchronously. This is done to ensure strictly sequential bytes processing and
to avoid too many threads
from the framework thread pool being involved. The Receive() method provides continuous listening for incoming byte stream and
places the received bytes in a thread safe queue for processing. The received bytes are processed in a dedicated thread of
a WorkerThread type object. Receive() provides callback delegates for both reading
and processing threads. The processing thread callback method calls in its turn
the onReceived event supplied by the user in the TcpClient (and down to
TcpChannel) constructor for the Client, and in the callback within
AcceptBegin() for the Server.
During data exchange, the onReceived event of type EventHandler<TcpChannelReceivedEventArgs> is called periodically by the processing thread to parse
the received bytes.
The event handler should be implemented by the user. Its first parameter is of type
TcpChannel and the second is of type TcpChannelReceivedEventArgs containing a chunk of received bytes. Since
onReceived is always called from the same processing thread, its handler is thread safe.
Continuously received bytes should be parsed to obtain required data, usually in
the form of objects of well-known types. The incoming stream may be split
into
certain records (objects) by delimiters or using fixed fields length. A given fragment of incoming string may contain
an incomplete record at the start and at the
end. To deal with such situations, the TcpChannel class provides
a property UnparsedBytes of type byte[] to save
bytes between the last complete record and the end of the currently received chunk of data. These bytes (if any) will be placed before
the next chunk of received data
in the TcpChannelReceivedEventArgs.BtsReceived property to ensure that this property always starts with a new record. It is
the responsibility of the user provided onReceived event handler to fill the TcpChannel.UnparsedBytes property with the leading bytes of the last [incomplete] record at the end of
the current chunk parsing. It is preferred to keep the unparsed bytes in the TcpChannel object itself over keeping them in a caller object
because the caller object may contain several TcpChannel objects forcing
the usage of an additional [synchronized] dictionary.
Events and Diagnostics
Strictly speaking, the handler for the onReceived event is the only compulsory callback that should be implemented by
the
TcpChannel
user. But the socket wrapper types provide a set of notifications about a variety of their internal events providing
the caller with relevant data. These notifications are available as
events of type EventHandler<TcpChannelNotifyEventArgs>. They may be used for state switch, diagnostics, and logging. In addition to
TcpChannel
notification events, TcpServer may use the static onServerNotifies to report events happened in
TcpServer's static methods before a connection is established. The
onServerNotifies event handler implemented by the caller is the fourth optional parameter of
the TcpServer.StartAcceptSubscribersOnPort()
static method. TcpServer may provide a more special event onInitConnectionToServer. Its delegate is called by
the Server
when the Server has accepted an incoming connection request from the Client, created
an appropriate object of TcpServer (the first "sender" argument
in the event call), and allotted a socket for this connection. In its handler, Server may send some connection acknowledgement
message to the Client. onInitConnectionToServer constitutes the third optional parameter of
the
TcpServer.StartAcceptSubscribersOnPort() static method.
For the sender side it is often important to identify the situation of "slow receiver", that is when
the time required for the socket to perform a send operation exceeds the parameter
SendTimeout of the socket. This can happen, e. g., due to not wide enough network bandwidth or when
the receiver side gets data too slow. The type
TcpChannel
diagnoses this situation for both synchronous (by analyzing the exception brought by
the onSyncSendException event) and asynchronous (onAsyncSendSendingTimeoutExceeded event) sending modes.
Code Sample
The small application SampleApp.exe illustrates the usage of the socket wrapper types. Depending on its arguments, the application acts either as a Server or as
a Client. The following code fragment presents part of its Main() method.
TcpChannel.LocalHost = localHost;
onReceived = new TcpChannelEventHandler<tcpchannelreceivedeventargs>((tcpChannelSender, e) =>
{
if (e.AreBytesAvailable)
{
if (tcpChannelSender != null)
tcpChannelSender.Send());
}
});
switch (role)
{
case Role.Server:
onInitConnectionToServer = new TcpChannelEventHandler<EventArgs>((tcpServerSender, e) =>
{
SetEventHandlers(tcpServerSender);
tcpServerSender.Send();
});
onServerNotifies = new TcpChannelEventHandler<TcpChannelNotifyEventArgs>((tcpServerSender, e) =>
{
});
TcpServer.StartAcceptSubscribersOnPort(localPort, onReceived, onInitConnectionToServer, onServerNotifies);
isListening = true;
break;
case Role.Client:
TcpClient tcpClient = new TcpClient(onReceived,
localPort.ToString(), 15, 9, 10); SetEventHandlers(tcpClient);
tcpClient.Connect(remoteHost, remotePort);
break;
}</tcpchannelreceivedeventargs>
Let's discuss the above code. Both the Server and Client caller applications need to implement a handler for the onReceived event. The Server
also implements handlers for the onInitConnectionToServer and onServerNotifies events. The Server calls the static method
TcpServer.StartAcceptSubscribersOnPort() to start listening on localPort for incoming connection requests from Clients. When a connection has been
established, the onInitConnectionToServer event is called with the newly created TcpServer object as sender (the first argument). The Client first creates
a TcpClient object by calling the TcpClient public constructor. To provide a certain degree of flexibility, the static method
TcpServer.StartAcceptSubscribersOnPort() and the constructor of the TcpClient type have several arguments, most of which however have default values.
The table below provides information about the arguments:
| Arguments |
Type |
Description |
Default Value |
Relevant for |
onReceived |
TcpChannelEventHandler <TcpChannelReceivedEventArgs> |
Event which handler is implemented by caller. It
is called upon receiving a chunk of data. |
n/a |
Server & Client |
id |
string |
Parameter to identify given TcpChannel
object. |
null |
Server & Client |
receiveTimeoutInSec |
int |
If during this time interval (in sec) TcpClient
does not receive incoming data then the channel is considered closed.
|
15 sec |
Client |
reconnectionAttempts |
int |
Maximum number of consequent reconnection
attempts that TcpClient
will undertake after it decides that channel was closed. |
0 |
Client |
delayBetweenReconnectionAttemptsInSec |
int |
Time interval (in sec) between two sequential
reconnection attempts. |
15 sec |
Client |
socketReceiveTimeoutInSec |
int |
Converted to ms and assigned to parameter
ReceiveTimeout of socket. |
15 sec |
Server & Client |
socketSendTimeoutInSec |
int |
Converted to ms and assigned to parameter
SendTimeout of socket. |
5 sec |
Server & Client |
socketReceiveBufferSize |
int |
Assigned to parameter
ReceiveBufferSize of
socket. |
128 KB |
Server & Client |
socketSendBufferSize |
int |
Assigned to parameter
SendBufferSize of
socket. |
128 KB |
Server & Client |
The method SetEventHandlers() assigns handlers to notification events of TcpChannel and TcpClient.
static void SetEventHandlers(TcpChannel tcpChannel)
{
if (tcpChannel == null)
return;
tcpChannel.onSocketNullOrNotConnected += ((tcpChannelSender, e) =>
{
});
TcpClient tcpClient = tcpChannel as TcpClient;
if (tcpClient != null)
{
tcpClient.onSocketConnectionFailed += ((tcpClientSender, e) =>
{
});
}
}
In the case of the Server, this method is called by the onInitConnectionToServer event handler, and in
the case of the Client, the method immediately follows the
TcpClient constructor.
After the call to the SetEventHandlers() method the Client calls
the method Connect() providing the address and port of the Server as arguments. If
the Server is listening
for an incoming connection and is ready to accept one, then in the Server synchronization event,
evAccept is set in the callback defined in the method
tcpListener.BeginAcceptSocket(), an object of TcpServer type is created,
a connection with the Client established, and the event
onInitConnectionToServer is called. In case the Server is not listening at the moment for
an incoming connection, it is still possible for the Client to keep sending a connection
request periodically until the Server is ready to accept it. To enable this feature,
a configurable reconnection mechanism is supported by the TcpClient
and TcpChannel types. This mechanism is configured with three arguments of
the TcpClient constructor, namely, receiveTimeoutInSec, reconnectionAttempts, and
delayBetweenReconnectionAttemptsInSec. Their description and default values are given in the table above. By default the reconnection mechanism is
disabled (reconnectionAttempts is equal to 0). But with appropriate configuration (like, e. g., in the code fragments above),
reconnection attempts will be periodically carried out including the initial connection to
the Server. Reconnection is exclusively a Client feature, as any
connection initiation. As soon as a connection between the Client and Server is established, both parties can send (synchronously or asynchronously) arrays of
bytes, receive, and process received bytes with the onReceived event handler.
To run the demo, command files RunServer.cmd, RunClient1.cmd, and RunClient2.cmd containing
SampleApp.exe with appropriate arguments should be started. If Clients are started before
the Server then a connection will happen
within delayBetweenReconnectionAttemptsInSec (in the sample, this is 10 sec) after the Server's start. In order to demonstrate reconnection in action,
the Server demo sample has a timer causing Server's cyclic work: the Server works for
a minute and then remains idle for another minute. After the Server resumes its
work, Clients get connected to the Server again within delayBetweenReconnectionAttemptsInSec. During
the Server's idle time, Clients keep
trying to connect to the Server according to their reconnection configuration.
Conclusion
This article presents a small library that simplifies continuous data exchange through TCP sockets. Very little user's code is required. Types of the library are equipped
with a set of notification events for diagnostics and logging purposes. A socket receives data synchronously with their processing in another dedicated thread.
Thanks
Many thanks to a great professional and a friend of mine Michael Molotsky for a very useful discussion on the topic of this article.