C#: a TCP echo server on the loopback interface

Use TcpListener and TcpClient with async methods to send a line and read the reply.

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

Explanation

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.

Explore the dry run
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

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