C#: a TCP echo server on the loopback interface
Use TcpListener and TcpClient with async methods to send a line and read the reply.
On this page
C# async and await.
- Use TcpListener and TcpClient with async methods to send a line and read the reply.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
TCP provides a reliable byte stream between two endpoints. A server creates a TcpListener bound to an address and port, starts listening, and accepts connections. A client creates a TcpClient and connects to the server's address and port. Both sides read and write through a NetworkStream.
Port 0 asks the operating system to choose a free port, and LocalEndpoint reports which one. IPAddress.Loopback keeps the traffic on the local machine. The server runs as a separate task so the client can connect to it from the same program. StreamReader and StreamWriter turn the byte stream into lines of text.
Code example
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
Task server = Task.Run(async () => {
using TcpClient accepted = await listener.AcceptTcpClientAsync();
using var reader = new StreamReader(accepted.GetStream());
using var writer = new StreamWriter(accepted.GetStream()) { AutoFlush = true };
string line = await reader.ReadLineAsync();
await writer.WriteLineAsync("echo: " + line);
});
using (var client = new TcpClient()) {
await client.ConnectAsync(IPAddress.Loopback, port);
using var reader = new StreamReader(client.GetStream());
using var writer = new StreamWriter(client.GetStream()) { AutoFlush = true };
await writer.WriteLineAsync("hello");
Console.WriteLine(await reader.ReadLineAsync());
}
await server;
listener.Stop();
}
}echo: hello
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
using System;using System.IO;using System.Net;using System.Net.Sockets;using System.Threading.Tasks; class Program { static async Task Main() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); int port = ((IPEndPoint)listener.LocalEndpoint).Port; Task server = Task.Run(async () => { using TcpClient accepted = await listener.AcceptTcpClientAsync(); using var reader = new StreamReader(accepted.GetStream()); using var writer = new StreamWriter(accepted.GetStream()) { AutoFlush = true }; string line = await reader.ReadLineAsync(); await writer.WriteLineAsync("echo: " + line); }); using (var client = new TcpClient()) { await client.ConnectAsync(IPAddress.Loopback, port); using var reader = new StreamReader(client.GetStream()); using var writer = new StreamWriter(client.GetStream()) { AutoFlush = true }; await writer.WriteLineAsync("hello"); Console.WriteLine(await reader.ReadLineAsync()); } await server; listener.Stop(); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Listen on the loopback address; port 0 means any free port. | listener bound to 127.0.0.1 | — | |
| The server task waits here until the client connects. | listener bound to 127.0.0.1 | — | |
| The client opens a TCP connection to the chosen port. | connected | — | |
| The client sends one line; AutoFlush sends it immediately. | sent: hello | — | |
| The client reads the server's reply line and prints it. | received: echo: hello | echo: hello |
Behind the scenes
How C# processes this code
C# compiles to code for the .NET runtime, which invokes the program entry point.
Cost and common mistakes
O(m) time for an m-byte message plus the connection setup round trip.
- TCP has no message boundaries. The newline here is what lets ReadLineAsync know where the message ends.
- Without AutoFlush the writer buffers the line and both sides wait forever for each other.
- Binding a listener to IPAddress.Any exposes it to the network. Use loopback while learning.
Try it yourself
How would the server serve many clients?
Reveal answer and reasoning
Loop on AcceptTcpClientAsync and start a new task for every accepted client.