Contact Me
Phone: (801) 910-4256
Email: mike@UpperSetting.com

Implementing a TCP Client/Server Application


The other day I was on stackoverflow and read a question titled:

    C# best way to implement TCP Client Server Application

Since I've been developing client/server applications for the last 20 years I decided to provide the community with some of my insights. Here goes.



There are several things to consider when writing a TCP client/server application:

  • Application layer packets may span multiple TCP packets.
  • Multiple application layer packets may be contained within a single TCP packet.
  • Encryption.
  • Authentication.
  • Lost and unresponsive clients.
  • Data serialization format.
  • Thread based or asynchronous socket readers.

Retrieving packets properly requires a wrapper protocol around your data. The protocol can be very simple. For example, it may be as simple as an integer that specifies the payload length. The snippet I have provided below was taken directly from the open source client/server application framework project DotNetOpenServer available on this website as well as GitHub. Note this code is used by both the client and the server:

private byte[] buffer = new byte[8192];
private int payloadLength;
private int payloadPosition;
private MemoryStream packet = new MemoryStream();
private PacketReadTypes readState;
private Stream stream;
private void ReadCallback(IAsyncResult ar)
{
    try
    {
        int available = stream.EndRead(ar);
        int position = 0;
        while (available > 0)
        {
            int lengthToRead;
            if (readState == PacketReadTypes.Header)
            {
                lengthToRead = (int)packet.Position + available >= SessionLayerProtocol.HEADER_LENGTH ?
                        SessionLayerProtocol.HEADER_LENGTH - (int)packet.Position :
                        available;
                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;
                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH)
                    readState = PacketReadTypes.HeaderComplete;
            }
            if (readState == PacketReadTypes.HeaderComplete)
            {
                packet.Seek(0, SeekOrigin.Begin);
                BinaryReader br = new BinaryReader(packet, Encoding.UTF8);
                ushort protocolId = br.ReadUInt16();
                if (protocolId != SessionLayerProtocol.PROTOCAL_IDENTIFIER)
                    throw new Exception(ErrorTypes.INVALID_PROTOCOL);
                payloadLength = br.ReadInt32();
                readState = PacketReadTypes.Payload;
            }
            if (readState == PacketReadTypes.Payload)
            {
                lengthToRead = available >= payloadLength - payloadPosition ?
                    payloadLength - payloadPosition :
                    available;
                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;
                payloadPosition += lengthToRead;
                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH + payloadLength)
                {
                    if (Logger.LogPackets)
                        Log(Level.Debug, "RECV: " + ToHexString(packet.ToArray(), 0, (int)packet.Length));
                    MemoryStream handlerMS = new MemoryStream(packet.ToArray());
                    handlerMS.Seek(SessionLayerProtocol.HEADER_LENGTH, SeekOrigin.Begin);
                    BinaryReader br = new BinaryReader(handlerMS, Encoding.UTF8);
                    if (!ThreadPool.QueueUserWorkItem(OnPacketReceivedThreadPoolCallback, br))
                        throw new Exception(ErrorTypes.NO_MORE_THREADS_AVAILABLE);
                    Reset();
                }
            }
        }
        stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(ReadCallback), null);
    }
    catch (ObjectDisposedException)
    {
        Close();
    }
    catch (Exception ex)
    {
        ConnectionLost(ex);
    }
}
private void Reset()
{
    readState = PacketReadTypes.Header;
    packet = new MemoryStream();
    payloadLength = 0;
    payloadPosition = 0;
}

 

If you're transmitting private information, it should be encrypted. I suggest TLS 1.2 which is easily enabled through .Net. The code is very simple and there are quite a few samples out there so for brevity I'm not going to show it here. If you are interested, you can find an example implementation in DotNetOpenServer.

All connections should be authenticated. There are many ways to accomplish this. I've use Windows Authentication (NTLM) as well as Basic. Although NTLM is powerful as well as automatic it is limited to specific platforms. Basic authentication simply passes a username and password after the socket has been encrypted. Basic authentication can still, however; authenticate the username/password combination against the local server or domain controller essentially impersonating NTLM. The latter method enables developers to easily create non-Windows client applications that run on iOS, Mac, Unix/Linux flavors as well as Java platforms (although some Java implementations support NTLM). Your server implementation should never allow application data to be transferred until after the session has been authenticated.

There are only a few things we can count on: taxes, networks failing and client applications hanging. It's just the nature of things. Your server should implement a method to clean up both lost and hung client sessions. I've accomplished this in many client/server applications through a keep-alive (AKA heartbeat) protocol. On the server side I implement a timer that is reset every time a client sends a packet, any packet. If the server doesn't receive a packet within the timeout, the session is closed. The keep-alive protocol is used to send packets when other application layer protocols are idle. Since your application only sends XML once every 15 minutes sending a keep-alive packet once a minute would able the server side to issue an alert to the administrator when a connection is lost prior to the 15 minute interval possibly enabling the IT department to resolve a network issue in a more timely fashion.

Next, data format. When speed and packet size are irrelevant, as is the case for client applications that pull data from a server every few minutes for client-side analysis, then REST queries are typically satisfactory. If, however; your customers require instantaneous or real-time interaction with their data or other connected devices, binary will always trump the bloated nature of string represented data REST provides.

Finally, threads or asynchronous doesn't really matter in most cases. I've written servers that scale to 10000 connections based on threads as well as asynchronous callbacks. It's all really the same thing when it comes down to it. Most network developers are using asynchronous callbacks now and the latest server implementation I've written, DotNetOpenServer, follows that model as well.

I hope this open source project is beneficial to the community and I welcome all of your comments. Thank you very much for reading this blog.


Michael Janulaitis
CEO