TCP/IP client-server application: exchange with string messages

Let’s implement simple TCP/IP client-server application which allows to exchange with string messages.

In short about TCP protocol (Wikipedia):

The Transmission Control Protocol (TCP) is one of the core protocols of the Internet protocol suite. TCP is one of the two original components of the suite, complementing the Internet Protocol (IP), and therefore the entire suite is commonly referred to as TCP/IP. TCP provides reliable, ordered delivery of a stream of octets from a program on one computer to another program on another computer. TCP is the protocol used by major Internet applications such as the World Wide Web, email, remote administration and file transfer.

So, TCP just provides the stream to send/receive bytes over the network.

The send/receive functions of socket does not guarantee that all the data you provided will be sent/received at one call. The functions return actual number of sent/received bytes.

Let’s assume the string messages will be sent as bytes (ASCII encoding). Then how to detect the end of one message?

To illustrate the problem, assume that there is two “Hello!” messages are sent.

Receiving part has the following stream:

-----------------------------------------------------------------------
| Stream bytes (ASCII)                                                |
| (chars are shown for simplicity instead of ASCII codes)             |
-----------------------------------------------------------------------
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
-----------------------------------------------------------------------
| H | e | l | l | o | ! | H | e | l | l |  o |  ! | .. | .. | .. | .. |
-----------------------------------------------------------------------

Because there is no guarantee of receiving the message at once, there is just a stream of bytes, it is necessary to use some message splitting mechanism. There are the following options:

  • Prefix the message with its size (size itself has fixed length, for example: 1 byte, 2 bytes, 4 bytes, etc.), i.e. introduce the message header.
  • Message terminator, use some byte or byte sequence to mark the end of the message.

That is why application-level protocol (see Application layer) has to be designed using one of those mechanisms to exchange with the messages.

Example

There are many approaches to design the protocol, but let’s consider the message protocol (the application-level protocol) with fixed size header (see above, first option):

----------------------------------------------------
| Number of string bytes | String bytes in ASCII   |
----------------------------------------------------
| 1 byte                 | 2 - ... bytes           |
----------------------------------------------------

There is just one byte for byte length, so the maximum string length is 255 chars.

Sending the “message”: the string should be converted to ASCII (for example) representation and sent with the byte length prefix (as described above).

Illustration:

-----------------------------------------------------------------------
| Stream bytes (ASCII)                                                |
| (chars are shown for simplicity instead of ASCII codes)             |
-----------------------------------------------------------------------
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
-----------------------------------------------------------------------
| 6 | H | e | l | l | o | ! | 6 | H | e |  l |  l |  o |  ! | .. | .. |
-----------------------------------------------------------------------

Receiving the message: receive the data to memory buffer. The process of extracting the “message” is opposite to the sending ones: receive the header (number of string bytes) and after that receive that number of bytes.

Code example

Let’s implement the client and server part of application using C#: the client will connect the server and send three messages.

Client

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;

namespace NetMan.TCPIPMessenger.Client
{
    class Program
    {
        private static byte[] MessageToByteArray(string message, Encoding encoding)
        {
            var byteCount = encoding.GetByteCount(message);
            if (byteCount > byte.MaxValue)
                throw new ArgumentException("Message size is greater than 255 bytes in the provided encoding");
            var byteArray = new byte[byteCount + 1];
            byteArray[0] = (byte)byteCount;
            encoding.GetBytes(message, 0, message.Length, byteArray, 1);
            return byteArray;
        }

        public static void Main(string[] args)
        {
            const string message = "Hello, World!";
            var byteArray = MessageToByteArray(message, Encoding.ASCII);
            using (var tcpClient = new TcpClient())
            {
                tcpClient.Connect("127.0.0.1", 31337);
                using (var networkStream = tcpClient.GetStream())
                using (var bufferedStream = new BufferedStream(networkStream))
                {
                    // Send three exactly the same messages.
                    bufferedStream.Write(byteArray, 0, byteArray.Length);
                    bufferedStream.Write(byteArray, 0, byteArray.Length);
                    bufferedStream.Write(byteArray, 0, byteArray.Length);
                }
            }
        }
    }
}

Server

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace NetMan.TCPIPMessenger.Server
{
    class Program
    {
        public static void Main(string[] args)
        {
            // Create a TCP/IP listener.
            var localAddress = IPAddress.Parse("127.0.0.1");
            var tcpListener = new TcpListener(localAddress, 31337);

            // Start listening for connections.
            tcpListener.Start();

            while (true)
            {
                Console.WriteLine("Waiting for a connection...");

                // Program is suspended while waiting for an incoming connection.
                var tcpClient = tcpListener.AcceptTcpClient();
                Console.WriteLine("Client has been accepted!");

                // An incoming connection needs to be processed.
                Thread thread = new Thread(() => ClientSession(tcpClient))
                    {
                        IsBackground = true
                    };
                thread.Start();
                Console.WriteLine("Client session thread has been started!");
            }
        }

        private static bool TryReadExact(Stream stream, byte[] buffer, int offset, int count)
        {
            int bytesRead;
            while (count > 0 && ((bytesRead = stream.Read(buffer, offset, count)) > 0))
            {
                offset += bytesRead;
                count -= bytesRead;
            }

            return count == 0;
        }

        private static void ClientSession(TcpClient tcpClient)
        {
            const int totalByteBuffer = 4096;
            byte[] buffer = new byte[256];

            using (var networkStream = tcpClient.GetStream())
            using (var bufferedStream = new BufferedStream(networkStream, totalByteBuffer))
                while (true)
                {
                    // Receive header - byte length.
                    if (!TryReadExact(bufferedStream, buffer, 0, 1))
                    {
                        break;
                    }
                    byte messageLen = buffer[0];

                    // Receive the ASCII bytes.
                    if (!TryReadExact(bufferedStream, buffer, 1, messageLen))
                    {
                        break;
                    }

                    var message = Encoding.ASCII.GetString(buffer, 1, messageLen);
                    Console.WriteLine("Message received: {0}", message);
                }
            Console.WriteLine("Client session completed!");
        }
    }
}

BufferedStream Class is used to reduce the number of calls to the networking subsystem. Buffers improve read and write performance (in this case, receive and send).

5 thoughts on “TCP/IP client-server application: exchange with string messages

  1. Tomi

    I have to agree with you.

    Really one of the best and easiest to get it right out there. Only two things missing for this one to be 100% complete are some kind of basic encryption (e.g. RijndaelManaged) and response from server to client.

    Anyway great job!

    Reply
    1. Sergey Brunov Post author

      Thank you very much for the comment!

      Encryption is out of the scope of this article. Sure, you can decorate the BufferedStream with the CryptoStream (for both sides) to achieve that. Useful example is here.

      Reply
      1. Tomi

        Yup, I already had encryption implemented when I was writing the comment but it’s pretty basic thing in communication now days and that’s the reason why I had mentioned it. But you’re right, it is out of scope of the article.

        On the other side, response from server isn’t out of scope so I will give some hints if you are willing to edit the article.

        On the client side: (for catching the server’s response)

        After the last of these lines

        bufferedStream.Write(byteArray, 0, byteArray.Length);
        

        one should add:

        int readSize = bufferedStream.ReadByte();
        byte[] dataToRead = new byte[readSize];
        bufferedStream.Read(dataToRead, 0, dataToRead.Length);
        var messageResponse = Encoding.ASCII.GetString(dataToRead, 0, readSize);
        

        On the server side: (for sending the response to client)

        After the last of these lines

        Console.WriteLine("Message received: {0}", message);
        

        one should add:

        var byteArray = MessageToByteArray("RESPONSE FROM SERVER", Encoding.ASCII);
        bufferedStream.Write(byteArray, 0, byteArray.Length);
        bufferedStream.Flush();
        

        Keep in mind also that MessageToByteArray function will also have to be available in server’s program.

        There is also one thing that I just found out by running the code analysis
        networkStream and bufferedStream should be used like this:

        var networkStream = tcpClient.GetStream();
        using (var bufferedStream = new BufferedStream(networkStream, totalByteBuffer))
        {
            ..
        

        Instead of putting both into using block.
        If networkStream is in using block, VS Code analysis reports that it will be disposed twice because it’s already disposed by disposing bufferedStream. bufferedStream.Dispose() releases all resources used by bufferedStream and networkStream is one of them.

        Reply
        1. Sergey Brunov Post author

          Thank you for the hints.

          Yes, sending response from server and receiving it on the client side originally was not considered to be in the scope of this article – the article is just to illustrate the approach: how to send and receive the data. But I think I will update the article.

          Let’s inspect the code:

          int readSize = bufferedStream.ReadByte();
          byte[] dataToRead = new byte[readSize];
          bufferedStream.Read(dataToRead, 0, dataToRead.Length);
          var messageResponse = Encoding.ASCII.GetString(dataToRead, 0, readSize);
          

          This code is fragile, because it:

          1. does not check that BufferedStream.ReadByte() returns -1 (indicates the end of the stream), see the documentation;
          2. does not check that BufferedStream.Read() reads the whole response (total number of bytes read can be less than the number of bytes requested) or there is the end of the stream, see the documentation.

          The TryReadExact() method is used to read exact number of bytes.

          As for Visual Studio Code analysis twice disposal warning: the warning should be suppressed because you should not have to know whether other classes take ownership of the disposables you created (please see this answer for details). Furthermore, the IDisposable.Dispose Method documentation states:

          If an object’s Dispose method is called more than once, the object must ignore all calls after the first one. The object must not throw an exception if its Dispose method is called multiple times.

          By the way, the blog uses Markdown (like Stackoverflow). Feel free to use it for the comments.

          Reply

Leave a Reply (Markdown is enabled)

Your email address will not be published. Required fields are marked *