ZetCode

C# TcpClient

last modified October 18, 2026

In this article we show how to create network programs with TcpClient in C#. The C# tutorial is a comprehensive tutorial on the C# language.

C# TcpClient

The TcpClient class provides simple methods for connecting, sending, and receiving stream data over a TCP network. It wraps a Socket and exposes the data through a NetworkStream. The server side is implemented with TcpListener; see the C# TcpListener tutorial. For lower-level control, there is the Socket class.

TCP is a reliable, connection-oriented protocol that delivers a stream of bytes. It has no notion of messages. A single Write on one side can arrive as several Read calls on the other, and the other way around, so the application protocol must define where a message ends: a line terminator, a length prefix, or closing the connection.

Note: for ordinary HTTP work, use HttpClient. The examples below send raw HTTP requests only to demonstrate how TcpClient works. They use plain, unencrypted HTTP on port 80.

C# TcpClient HEAD request

The HTTP HEAD method requests the headers that would be returned if the specified resource were requested with the GET method.

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

using var client = new TcpClient();

var hostname = "example.com";
client.Connect(hostname, 80);

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 2000;

var message = $"HEAD / HTTP/1.1\r\nHost: {hostname}\r\nUser-Agent: C# program\r\n"
    + "Connection: close\r\nAccept: text/html\r\n\r\n";

Console.WriteLine(message);

byte[] bytes = Encoding.UTF8.GetBytes(message);
stream.Write(bytes, 0, bytes.Length);

using var reader = new StreamReader(stream, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());

In the example, we send a HEAD request to example.com. The request is synchronous.

using var client = new TcpClient();

A new TcpClient is created. The using declaration releases the underlying socket when the variable goes out of scope.

var hostname = "example.com";
client.Connect(hostname, 80);

With the Connect method, we connect to the host on the given port. The name is resolved through DNS.

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 2000;

We get a NetworkStream and set a read timeout of two seconds. If no data arrives in this time, a read throws an IOException.

var message = $"HEAD / HTTP/1.1\r\nHost: {hostname}\r\nUser-Agent: C# program\r\n"
    + "Connection: close\r\nAccept: text/html\r\n\r\n";

We define a HEAD request. It consists of the request line (method, resource, and protocol version), the header fields, and an empty line. The \r\n characters are part of the syntax. The Host header is mandatory in HTTP/1.1, and Connection: close asks the server to close the connection after the response, which lets us read until the end of the stream. The details are described in RFC 9110.

byte[] bytes = Encoding.UTF8.GetBytes(message);
stream.Write(bytes, 0, bytes.Length);

The text request is converted to bytes with the GetBytes method and written to the network stream.

using var reader = new StreamReader(stream, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());

A StreamReader reads the response until the server closes the connection.

$ dotnet run
HEAD / HTTP/1.1
Host: example.com
User-Agent: C# program
Connection: close
Accept: text/html


HTTP/1.1 200 OK
Date: Sun, 18 Oct 2026 17:51:08 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Server: cloudflare
Last-Modified: Tue, 13 Oct 2026 22:05:48 GMT
Allow: GET, HEAD
Accept-Ranges: bytes
...

The exact set of response headers depends on the server and changes over time.

C# TcpClient GET request

The HTTP GET method requests a representation of the specified resource.

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

using var client = new TcpClient();

var hostname = "example.com";
client.Connect(hostname, 80);

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 2000;

var message = $"GET / HTTP/1.1\r\nHost: {hostname}\r\nAccept: text/html, charset=utf-8\r\n"
    + "Accept-Language: en-US\r\nUser-Agent: C# program\r\nConnection: close\r\n\r\n";

Console.WriteLine(message);

byte[] bytes = Encoding.UTF8.GetBytes(message);
stream.Write(bytes, 0, bytes.Length);

using var reader = new StreamReader(stream, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());

The example retrieves the home page of the specified website.

var message = $"GET / HTTP/1.1\r\nHost: {hostname}\r\nAccept: text/html, charset=utf-8\r\n"
    + "Accept-Language: en-US\r\nUser-Agent: C# program\r\nConnection: close\r\n\r\n";

We define a GET request. Compared to HEAD, only the method differs.

$ dotnet run
GET / HTTP/1.1
Host: example.com
Accept: text/html, charset=utf-8
Accept-Language: en-US
User-Agent: C# program
Connection: close


HTTP/1.1 200 OK
Date: Sun, 18 Oct 2026 17:51:08 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: close
Server: cloudflare
...

241
<!doctype html><html lang=en><head><meta charset=utf-8>...
<title>Example Domain</title>...</html>

0

The response uses Transfer-Encoding: chunked. The body is split into chunks, each preceded by its length in hexadecimal (here 241, which is 577 bytes), and the final chunk has length 0. A raw socket client sees these markers; HttpClient removes them for us. We could avoid chunking by sending an HTTP/1.0 request.

TcpClient POST request

The POST request sends data to the server. With the Content-Type header field, we tell the server what kind of data is sent. With the application/x-www-form-urlencoded type, the data is sent in the body of the request as key-value pairs separated by '&', with a '=' between the key and the value. This type is not suitable for binary data, because non-alphanumeric characters in both keys and values are percent encoded.

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

using var client = new TcpClient();

var hostname = "httpbin.org";
client.Connect(hostname, 80);

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 5000;

var data = "name=John+Doe&occupation=gardener";
int n = Encoding.UTF8.GetByteCount(data);

var message = $"POST /anything HTTP/1.1\r\nHost: {hostname}\r\nAccept: */*\r\n"
    + "User-Agent: C# program\r\nContent-Type: application/x-www-form-urlencoded\r\n"
    + $"Content-Length: {n}\r\nConnection: close\r\n\r\n{data}";

Console.WriteLine(message);

byte[] bytes = Encoding.UTF8.GetBytes(message);
stream.Write(bytes, 0, bytes.Length);

Console.WriteLine("---------------------");

using var reader = new StreamReader(stream, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());

The example sends a POST request with URL-encoded data in its body.

var data = "name=John+Doe&occupation=gardener";
int n = Encoding.UTF8.GetByteCount(data);

This is the data to be sent. We also need its length in bytes, not in characters; the two differ for non-ASCII text.

var message = $"POST /anything HTTP/1.1\r\nHost: {hostname}\r\nAccept: */*\r\n"
    + "User-Agent: C# program\r\nContent-Type: application/x-www-form-urlencoded\r\n"
    + $"Content-Length: {n}\r\nConnection: close\r\n\r\n{data}";

We construct a POST request. We must set the Content-Type and the Content-Length header fields. The header and the body are separated by an empty line, that is by \r\n\r\n.

$ dotnet run
POST /anything HTTP/1.1
Host: httpbin.org
Accept: */*
User-Agent: C# program
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
Connection: close

name=John+Doe&occupation=gardener
---------------------
HTTP/1.1 200 OK
Date: Sun, 18 Oct 2026 17:51:08 GMT
Content-Type: application/json
Content-Length: 487
Connection: close
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "name": "John Doe", 
    "occupation": "gardener"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Content-Length": "33", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "C# program", 
    "X-Amzn-Trace-Id": "Root=1-6ad5070c-2ee56d8809ca237e7f6bb310"
  }, 
  "json": null, 
  "method": "POST", 
  "origin": "203.0.113.7", 
  "url": "http://httpbin.org/anything"
}

C# TcpClient async request

In the following example, we create an asynchronous request. Network I/O spends most of its time waiting, so the asynchronous methods avoid blocking a thread.

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

string host = "example.com";
int port = 80;

using var client = new TcpClient();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await client.ConnectAsync(host, port, cts.Token);

await using NetworkStream stream = client.GetStream();

var request = $"GET / HTTP/1.1\r\nHost: {host}\r\nUser-Agent: Console app\r\nConnection: close\r\n\r\n";
await stream.WriteAsync(Encoding.UTF8.GetBytes(request), cts.Token);

using var reader = new StreamReader(stream, Encoding.UTF8);
string response = await reader.ReadToEndAsync(cts.Token);

Console.WriteLine(response.Split("\r\n")[0]);
Console.WriteLine($"received {response.Length} characters");

The example sends an asynchronous GET request to example.com and prints the status line and the size of the response.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await client.ConnectAsync(host, port, cts.Token);

We connect asynchronously. The cancellation token gives us a simple overall timeout: when five seconds pass, the pending operation is cancelled and throws an OperationCanceledException. The same token is passed to the write and read operations below.

await using NetworkStream stream = client.GetStream();

NetworkStream supports asynchronous disposal.

await stream.WriteAsync(Encoding.UTF8.GetBytes(request), cts.Token);

The WriteAsync overload with a ReadOnlyMemory<byte> writes the request bytes. We build the request as a single string with explicit \r\n terminators. Using WriteLineAsync would append an extra newline, which does not belong to the request.

string response = await reader.ReadToEndAsync(cts.Token);

The ReadToEndAsync method reads the response until the server closes the connection.

$ dotnet run
HTTP/1.1 200 OK
received 931 characters

C# TcpClient echo client

To see both sides of a TCP conversation, we start a small echo server in the same program, using TcpListener on a free local port, and connect to it with TcpClient. The messages are lines of text, so a line terminator marks where a message ends.

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

// a tiny echo server on a free local port
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;

var server = Task.Run(async () =>
{
    using TcpClient peer = await listener.AcceptTcpClientAsync();
    using var s = peer.GetStream();
    byte[] buf = new byte[1024];
    int n;

    while ((n = await s.ReadAsync(buf)) > 0)
    {
        await s.WriteAsync(buf.AsMemory(0, n));
    }
});

using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);

Console.WriteLine($"connected: {client.Connected}");

NetworkStream stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.UTF8);
using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };

foreach (var msg in new[] { "hello", "falcon", "owl" })
{
    await writer.WriteLineAsync(msg);
    string? echo = await reader.ReadLineAsync();
    Console.WriteLine($"sent '{msg}', received '{echo}'");
}

client.Client.Shutdown(SocketShutdown.Send);   // tells the server that we are done
await server;
listener.Stop();

The server accepts one connection and writes back everything it receives. The client sends three lines and prints the echoes.

var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;

Port 0 asks the operating system for any free port, which we then read from the local endpoint. This avoids clashes with other programs.

using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };

We wrap the network stream in a StreamReader and a StreamWriter for line-oriented text. AutoFlush is important here; without it the line would sit in the writer's buffer and the server would never receive it, so we would wait for the echo forever.

client.Client.Shutdown(SocketShutdown.Send);
await server;

Shutting down the sending side tells the server that no more data will come, its read returns 0 and its loop ends. Then we wait for the server task to finish.

$ dotnet run
connected: True
sent 'hello', received 'hello'
sent 'falcon', received 'falcon'
sent 'owl', received 'owl'

C# TcpClient connection errors and timeouts

Connections fail in several ways. When nothing listens on the port, the operating system rejects the attempt at once with a SocketException. When the address does not respond, the attempt hangs until the operating system gives up, which can take a long time; a cancellation token gives us control over this wait.

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

// a port with no listener refuses the connection
using var probe = new TcpListener(IPAddress.Loopback, 0);
probe.Start();
int freePort = ((IPEndPoint)probe.LocalEndpoint).Port;
probe.Stop();

try
{
    using var client = new TcpClient();
    await client.ConnectAsync(IPAddress.Loopback, freePort);
}
catch (SocketException e)
{
    Console.WriteLine($"{e.SocketErrorCode}: {e.Message}");
}

// a non-routable address: the connection attempt times out
try
{
    using var client = new TcpClient();
    using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
    await client.ConnectAsync("10.255.255.1", 80, cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("connection attempt timed out");
}
catch (SocketException e)
{
    Console.WriteLine($"{e.SocketErrorCode}");
}

The program provokes both failures.

catch (SocketException e)
{
    Console.WriteLine($"{e.SocketErrorCode}: {e.Message}");
}

The SocketErrorCode property tells what went wrong, for example ConnectionRefused, HostNotFound, or TimedOut.

await client.ConnectAsync("10.255.255.1", 80, cts.Token);

The address is in a private range that is not routed anywhere on most networks, so the connection attempt never completes and our 500 ms token cancels it. On some networks this address may answer immediately with an error, in which case the SocketException branch runs instead.

$ dotnet run
ConnectionRefused: Connection refused
connection attempt timed out

Source

TcpClient class

In this article we have worked with C# TcpClient.

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.