C# · Course navigation

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
Before you start

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

C#
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();
  }
}
Output
echo: hello

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1using System;
2using System.IO;
3using System.Net;
4using System.Net.Sockets;
5using System.Threading.Tasks;
6
7class Program {
8 static async Task Main() {
9 var listener = new TcpListener(IPAddress.Loopback, 0);
10 listener.Start();
11 int port = ((IPEndPoint)listener.LocalEndpoint).Port;
12
13 Task server = Task.Run(async () => {
14 using TcpClient accepted = await listener.AcceptTcpClientAsync();
15 using var reader = new StreamReader(accepted.GetStream());
16 using var writer = new StreamWriter(accepted.GetStream()) { AutoFlush = true };
17 string line = await reader.ReadLineAsync();
18 await writer.WriteLineAsync("echo: " + line);
19 });
20
21 using (var client = new TcpClient()) {
22 await client.ConnectAsync(IPAddress.Loopback, port);
23 using var reader = new StreamReader(client.GetStream());
24 using var writer = new StreamWriter(client.GetStream()) { AutoFlush = true };
25 await writer.WriteLineAsync("hello");
26 Console.WriteLine(await reader.ReadLineAsync());
27 }
28 await server;
29 listener.Stop();
30 }
31}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading