ZetCode

C# TcpListener

last modified October 17, 2026

In this article we show how to create TCP servers in C# with the TcpListener class.

TCP (Transmission Control Protocol) provides reliable, ordered, two-way byte streams between two programs. A server listens on a port and accepts connections from clients. The TcpListener class from the System.Net.Sockets namespace listens for incoming connections; each accepted connection is represented by a TcpClient, whose NetworkStream is used to read and write data.

The client side is covered in the C# TcpClient article. We test our servers with our own clients or with the nc (netcat) tool.

C# TcpListener daytime server

We start with a server that sends the current date and time to each client and closes the connection.

Program.cs
using System.Net;
using System.Net.Sockets;
using System.Text;

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start();

Console.WriteLine($"Listening on {listener.LocalEndpoint}");

while (true)
{
    using TcpClient client = await listener.AcceptTcpClientAsync();
    Console.WriteLine($"Client connected: {client.Client.RemoteEndPoint}");

    NetworkStream stream = client.GetStream();

    byte[] data = Encoding.ASCII.GetBytes($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n");
    await stream.WriteAsync(data);
}

The server handles one client at a time.

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start();

We create a listener bound to port 5000 of the loopback interface, so only programs on the same computer can connect. To accept connections from other computers, we would use IPAddress.Any. The Start method starts listening.

using TcpClient client = await listener.AcceptTcpClientAsync();

The AcceptTcpClientAsync method waits for a client to connect and returns a TcpClient for the connection. Disposing it at the end of the loop iteration closes the connection.

byte[] data = Encoding.ASCII.GetBytes($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n");
await stream.WriteAsync(data);

TCP transfers bytes, so we encode the text before writing it to the network stream.

$ dotnet run
Listening on 127.0.0.1:5000
Client connected: 127.0.0.1:49512
Client connected: 127.0.0.1:49518

We run the server.

$ nc localhost 5000
2026-10-17 19:12:54

In another terminal, we connect to the server with nc.

C# TcpListener echo server

A real server must serve several clients at the same time. In the following example, each client is handled in its own asynchronous task. The server reads lines of text and sends them back.

Program.cs
using System.Net;
using System.Net.Sockets;

var listener = new TcpListener(IPAddress.Loopback, 5001);
listener.Start();

Console.WriteLine($"Echo server listening on {listener.LocalEndpoint}");

while (true)
{
    TcpClient client = await listener.AcceptTcpClientAsync();
    _ = HandleClientAsync(client);
}

static async Task HandleClientAsync(TcpClient client)
{
    var endpoint = client.Client.RemoteEndPoint;
    Console.WriteLine($"{endpoint} connected");

    try
    {
        using (client)
        {
            NetworkStream stream = client.GetStream();

            using var reader = new StreamReader(stream);
            using var writer = new StreamWriter(stream) { AutoFlush = true };

            string? line;

            while ((line = await reader.ReadLineAsync()) is not null)
            {
                Console.WriteLine($"{endpoint}: {line}");
                await writer.WriteLineAsync($"echo: {line}");
            }
        }
    }
    catch (IOException e)
    {
        Console.WriteLine($"{endpoint} error: {e.Message}");
    }

    Console.WriteLine($"{endpoint} disconnected");
}

The example is a line-based echo server.

while (true)
{
    TcpClient client = await listener.AcceptTcpClientAsync();
    _ = HandleClientAsync(client);
}

We start a task for the client and do not await it; the loop immediately goes back to accepting the next client. The discard _ shows that we ignore the task intentionally. Because of this, the task must handle its own exceptions.

using var reader = new StreamReader(stream);
using var writer = new StreamWriter(stream) { AutoFlush = true };

A TCP stream has no message boundaries; a single read may return part of a message or several messages. A simple solution is a line-based protocol: the StreamReader splits the incoming bytes into lines for us. With AutoFlush, each write is sent immediately.

while ((line = await reader.ReadLineAsync()) is not null)
{
    Console.WriteLine($"{endpoint}: {line}");
    await writer.WriteLineAsync($"echo: {line}");
}

The ReadLineAsync method returns null when the client closes the connection.

catch (IOException e)
{
    Console.WriteLine($"{endpoint} error: {e.Message}");
}

If the connection is broken, for instance when the client is killed, the reading fails with an IOException.

The following client sends three messages to the server and prints the replies.

Program.cs
using System.Net.Sockets;

using var client = new TcpClient();
await client.ConnectAsync("localhost", 5001);

NetworkStream stream = client.GetStream();

using var reader = new StreamReader(stream);
using var writer = new StreamWriter(stream) { AutoFlush = true };

string[] messages = ["Hello there", "How are you?", "Bye"];

foreach (var msg in messages)
{
    await writer.WriteLineAsync(msg);

    string? reply = await reader.ReadLineAsync();
    Console.WriteLine(reply);
}
$ dotnet run
echo: Hello there
echo: How are you?
echo: Bye

We run two clients at the same time. The server log shows that their messages are handled concurrently.

$ dotnet run
Echo server listening on 127.0.0.1:5001
127.0.0.1:58218 connected
127.0.0.1:58218: Hello there
127.0.0.1:58234 connected
127.0.0.1:58234: Hello there
127.0.0.1:58218: How are you?
127.0.0.1:58218: Bye
127.0.0.1:58234: How are you?
127.0.0.1:58234: Bye
127.0.0.1:58218 disconnected
127.0.0.1:58234 disconnected

C# TcpListener chat server

In the last example, we create a chat server. Every message from a client is sent to all connected clients. The server stops gracefully when we press Ctrl+C.

Program.cs
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;

using var cts = new CancellationTokenSource();

Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    cts.Cancel();
};

var clients = new ConcurrentDictionary<int, StreamWriter>();
var broadcastLock = new SemaphoreSlim(1, 1);
int nextId = 0;

var listener = new TcpListener(IPAddress.Any, 5002);
listener.Start();

Console.WriteLine("Chat server listening on port 5002; press Ctrl+C to stop");

var tasks = new List<Task>();

try
{
    while (true)
    {
        TcpClient client = await listener.AcceptTcpClientAsync(cts.Token);

        int id = Interlocked.Increment(ref nextId);
        tasks.Add(HandleClientAsync(id, client, cts.Token));
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Shutting down...");
}
finally
{
    listener.Stop();
}

await Task.WhenAll(tasks);
Console.WriteLine("Server stopped");

async Task HandleClientAsync(int id, TcpClient client, CancellationToken token)
{
    using (client)
    {
        NetworkStream stream = client.GetStream();

        using var reader = new StreamReader(stream);
        var writer = new StreamWriter(stream) { AutoFlush = true };

        clients[id] = writer;
        await BroadcastAsync($"* user{id} joined");

        try
        {
            string? line;

            while ((line = await reader.ReadLineAsync(token)) is not null)
            {
                await BroadcastAsync($"user{id}: {line}");
            }
        }
        catch (OperationCanceledException)
        {
            clients.TryRemove(id, out _);
            await writer.WriteLineAsync("* server is shutting down");
        }
        catch (IOException)
        {
        }
        finally
        {
            clients.TryRemove(id, out _);
        }

        await BroadcastAsync($"* user{id} left");
    }
}

async Task BroadcastAsync(string message)
{
    await broadcastLock.WaitAsync();

    try
    {
        Console.WriteLine(message);

        foreach (var (id, writer) in clients)
        {
            try
            {
                await writer.WriteLineAsync(message);
            }
            catch (Exception e) when (e is IOException or ObjectDisposedException)
            {
                clients.TryRemove(id, out _);
            }
        }
    }
    finally
    {
        broadcastLock.Release();
    }
}

The server keeps the writers of the connected clients in a concurrent dictionary.

Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    cts.Cancel();
};

By default, Ctrl+C terminates the process immediately. We set e.Cancel to keep the process running and signal the cancellation token instead.

TcpClient client = await listener.AcceptTcpClientAsync(cts.Token);

When the token is cancelled, the waiting accept throws an OperationCanceledException, which ends the accept loop.

await Task.WhenAll(tasks);

Before the server exits, it waits for the client tasks to finish. The same token is passed to ReadLineAsync in each client handler, so the handlers notify their clients and end as well.

var clients = new ConcurrentDictionary<int, StreamWriter>();
var broadcastLock = new SemaphoreSlim(1, 1);

Clients join and leave on different threads, so we use a thread-safe dictionary. A StreamWriter does not allow two writes at the same time; if two clients sent a message at once, two broadcasts could write to the same writer concurrently and fail. The semaphore lets only one broadcast run at a time.

catch (Exception e) when (e is IOException or ObjectDisposedException)
{
    clients.TryRemove(id, out _);
}

A client can disconnect while a message is being sent to it. We remove such clients from the dictionary.

$ dotnet run
Chat server listening on port 5002; press Ctrl+C to stop
* user1 joined
* user2 joined
user1: hello all
user2: hi user1
* user2 left
^CShutting down...
* user1 left
Server stopped

We run the server and connect two clients with nc localhost 5002.

$ nc localhost 5002
* user1 joined
* user2 joined
hello all
user1: hello all
user2: hi user1
* user2 left
* server is shutting down

This is the session of the first client.

Source

TcpListener class - language reference

In this article we have created TCP servers in C# with TcpListener.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.