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.
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.
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.
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.
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
List all C# tutorials.