Brunov's blog

Sergey Vyacheslavovich Brunov's blog

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

2013-02-09 13:44:00 Moscow time

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:

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).

Tags: C# network programming tcp/ip