Simplest Single-Client TCP Server and Client in .NET Core

Goal

Build a minimal TCP server and client in .NET Core 3.1 that can exchange messages.

TCP Server

//Program.cs
public class Program
{
    private const int Port = 8080;

    public static async Task Main(String[] args)  
    {
        var address = GetLocalIPAddress();
        var tcpListener = new TcpListener(GetLocalIPAddress(), Port);
        tcpListener.Start();  
        Console.WriteLine($"Server started. Listening to TCP clients at {address}, port {Port}"); 

        while (true)
        {
            await WaitForClientConnectionAndHandleAsync(tcpListener);
        }
    }

    static IPAddress GetLocalIPAddress()
    {
        var host = Dns.GetHostEntry(Dns.GetHostName());
        foreach (var ip in host.AddressList)
        {
            if (ip.AddressFamily == AddressFamily.InterNetwork)
            {
                return ip;
            }
        }
        throw new Exception("No network adapters with an IPv4 address in the system!");
    }

    static async Task WaitForClientConnectionAndHandleAsync(TcpListener tcpListener)
    {
        Console.WriteLine($"Waiting for a client connection at {DateTime.Now.ToString()}..");
        using(var client = tcpListener.AcceptTcpClient())
        using(var networkStream = client.GetStream())
        {
            Console.WriteLine($"Client connected at {DateTime.Now.ToString()}...");

            Console.WriteLine($"Receiving message...");
            var receivedMessage = await ReceiveMessageAsync(networkStream);
            Console.WriteLine($"Message received from client at {DateTime.Now.ToString()}: {receivedMessage}");

            var messageToSend = GenerateAnswerMessageText(receivedMessage);
            await SendMessageAsync(networkStream, messageToSend);
            Console.WriteLine($"Sent message at {DateTime.Now.ToString()}: {messageToSend} ");
        }
    }

    private static string GenerateAnswerMessageText(string receivedMessage)
    {
        return "makes Jack a dull boy";
    }

    static async Task SendMessageAsync(NetworkStream stream, string message)
    {
        byte[] helloMessage = new byte[100];
        helloMessage = Encoding.Default.GetBytes(message);
        await stream.WriteAsync(helloMessage, 0, helloMessage.Length);
    }

    static async Task<string> ReceiveMessageAsync(NetworkStream networkStream)
    {
        byte[] receivedMessage = new byte[1024];
        await networkStream.ReadAsync(receivedMessage, 0, receivedMessage.Length);
        var receivedMessageText = Encoding.Default.GetString(receivedMessage).Trim(' ', '\r', '\n');
        return receivedMessageText;
    }
}

Important: Don’t bind the server to localhost or 127.0.0.1. The server would only respond to that specific address, not its real IP when deployed on a network or in a container.

Test with netcat

netcat [SERVER_IP] [PORT]
hello server!

TCP Client

class Program
{
    static async Task Main(string[] args)
    {
        try
        {
            Console.WriteLine("Enter server IP:");
            var serverIp = Console.ReadLine();
            Console.WriteLine("Enter server port:");
            var serverPort = int.Parse(Console.ReadLine());
            
            using(var client = new TcpClient(serverIp, serverPort))
            {
                using (var networkStream = client.GetStream())
                {
                    Console.WriteLine("Connected to server...");

                    var messageToSendText = "All work and no play..";
                    await SendMessageAsync(networkStream, messageToSendText);
                    Console.WriteLine($"Sent message at {DateTime.Now.ToString()}: {messageToSendText}");

                    var messageReceivedText = await ReceiveMessageAsync(networkStream);
                    Console.WriteLine($"Received message at {DateTime.Now.ToString()}: {messageReceivedText}");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed with exception: {ex.ToString()}");
        }
    }

    static async Task SendMessageAsync(NetworkStream stream, string message)
    {
        var messageToSend = System.Text.Encoding.ASCII.GetBytes(message);
        await stream.WriteAsync(messageToSend, 0, messageToSend.Length);
    }

    static async Task<string> ReceiveMessageAsync(NetworkStream stream)
    {
        var messageReceived = new Byte[1024];
        var numberOfBytes = await stream.ReadAsync(messageReceived, 0, messageReceived.Length);
        var messageReceivedText = System.Text.Encoding.ASCII.GetString(messageReceived, 0, numberOfBytes);
        return messageReceivedText;
    }
}

Docker Deployment

## Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build-env
WORKDIR /app

## Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

## Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o publish

## Build runtime image
FROM mcr.microsoft.com/dotnet/runtime:3.1
WORKDIR /app
COPY --from=build-env /app/publish .

EXPOSE 8080
ENTRYPOINT ["dotnet", "Bustroker.TcpServer.ConsoleUI.dll"]

Build and run

docker build -t tcp-server .
docker run -p 8080:8080 tcp-server

Test from another terminal

netcat [SERVER_IP] [PORT]
hello tcp!