ZetCode

C# Socket

last modified October 18, 2026

In this article we show how to work with sockets in C#.

Network protocols

TCP/IP is a suite of protocols used by devices to communicate over the Internet and most local networks. TCP is reliable: it establishes a connection, retransmits lost data, and delivers the bytes in order. It is used by services such as HTTP, SMTP, or FTP. UDP is connectionless; it sends independent datagrams without a guarantee of delivery or order. It has lower overhead and is used by services such as DNS, VoIP, or online games.

Socket

In programming, a socket is an endpoint of a communication between two programs running on a network. In .NET, the Socket class from the System.Net.Sockets namespace is a thin wrapper over the operating system's sockets. It provides both synchronous and asynchronous methods for sending and receiving data.

The higher-level TcpClient, TcpListener, and UdpClient classes are built on top of Socket; for HTTP, we use HttpClient. Working with the Socket class directly is useful for learning how network protocols work and when we need full control over the connection.

C# UDP Socket example

The Quote of the Day (QOTD) is a simple service defined in RFC 865. The client sends a datagram to port 17 and the server replies with a short quote.

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

IPAddress[] addresses = await Dns.GetHostAddressesAsync("djxmmx.net");
IPAddress address = addresses[0];

var ep = new IPEndPoint(address, 17);
using var socket = new Socket(address.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

await socket.SendToAsync(Array.Empty<byte>(), ep);

byte[] buf = new byte[1024];
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var res = await socket.ReceiveFromAsync(buf, ep, cts.Token);

Console.WriteLine($"Message size: {res.ReceivedBytes}");
Console.WriteLine($"Received from: {res.RemoteEndPoint}");
Console.WriteLine(Encoding.ASCII.GetString(buf, 0, res.ReceivedBytes));

The program is a client of a QOTD service. Note that such public services are often short-lived.

IPAddress[] addresses = await Dns.GetHostAddressesAsync("djxmmx.net");
IPAddress address = addresses[0];

The Dns.GetHostAddressesAsync method resolves the host name to its IP addresses. A host may have IPv4 and IPv6 addresses.

using var socket = new Socket(address.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

A UDP socket is created. We pass the address family, the socket type, and the protocol type. We take the address family from the resolved address; an IPv4 socket cannot send to an IPv6 address and vice versa.

await socket.SendToAsync(Array.Empty<byte>(), ep);

We send an empty datagram to the endpoint. The content of the request is ignored by the server.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var res = await socket.ReceiveFromAsync(buf, ep, cts.Token);

The ReceiveFromAsync method waits for a datagram and stores it in the buffer. Since UDP does not guarantee delivery, the reply may never come; we give up after five seconds. The ReceiveTimeout property applies only to the synchronous methods, so we use a cancellation token. The method returns a SocketReceiveFromResult with the number of received bytes and the sender's endpoint.

$ dotnet run
Message size: 134
Received from: 104.9.242.101:17
"Here's the rule for bargains: "Do other men, for they would do you."
 That's the true business precept." Charles Dickens (1812-70)

C# Socket HTTP HEAD request

A HEAD request is an HTTP GET request without a message body. The headers of a response contain metadata, such as the status, the content type, or the date of the last modification.

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

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

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(host, port);

Console.WriteLine($"Connected to {socket.RemoteEndPoint}");

string request = $"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n";
byte[] data = Encoding.ASCII.GetBytes(request);

await socket.SendAsync(data);

byte[] buf = new byte[4096];
int n = await socket.ReceiveAsync(buf);

Console.WriteLine(Encoding.ASCII.GetString(buf, 0, n));

In the code example, we send a HEAD request to example.com.

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(host, port);

HTTP/1.1 and HTTP/2 run over TCP, so we create a stream socket. The constructor without the address family creates a dual-mode socket, which can connect to both IPv6 and IPv4 addresses. The ConnectAsync overload that takes a host name resolves it and tries the addresses one by one until it succeeds. If the connection fails, it throws a SocketException.

Console.WriteLine($"Connected to {socket.RemoteEndPoint}");

On a dual-mode socket, an IPv4 address is shown as an IPv4-mapped IPv6 address.

string request = $"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n";

The request line contains the method, the resource path, and the protocol version. HTTP/1.1 requires the Host header. With Connection: close, we ask the server to close the connection after the response. Each line ends with \r\n; an empty line ends the headers. The format is described in RFC 9112.

int n = await socket.ReceiveAsync(buf);
Console.WriteLine(Encoding.ASCII.GetString(buf, 0, n));

We receive and print the headers. A short response fits into a single read; in general, we must read in a loop, as shown in the next example.

$ dotnet run
Connected to [::ffff:104.20.23.154]:80
HTTP/1.1 200 OK
Date: Sun, 18 Oct 2026 07:40:48 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
Age: 14043
cf-cache-status: HIT
CF-RAY: a4c5e5a38e1d696b-BTS
alt-svc: h3=":443"; ma=86400

C# Socket HTTP GET request

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

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

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

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(host, port);

string request = $"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n";
await socket.SendAsync(Encoding.ASCII.GetBytes(request));

using var ms = new MemoryStream();
byte[] buf = new byte[1024];
int n;

while ((n = await socket.ReceiveAsync(buf)) > 0)
{
    ms.Write(buf, 0, n);
}

string response = Encoding.UTF8.GetString(ms.ToArray());

int idx = response.IndexOf("\r\n\r\n");
string headers = response[..idx];
string body = response[(idx + 4)..];

Console.WriteLine($"Received {ms.Length} bytes");
Console.WriteLine(headers.Split("\r\n")[0]);

int start = body.IndexOf("<title>") + "<title>".Length;
int end = body.IndexOf("</title>");
Console.WriteLine($"Title: {body[start..end]}");

The example reads the home page of example.com and prints the status line and the title of the page.

string request = $"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n";

This time we use HTTP/1.0. An HTTP/1.1 server may send the body in chunks, each preceded by its size, which we would have to decode. An HTTP/1.0 response contains the body as is, and the server closes the connection after sending it.

while ((n = await socket.ReceiveAsync(buf)) > 0)
{
    ms.Write(buf, 0, n);
}

We do not know the size of the response in advance, so we read it in chunks until ReceiveAsync returns 0, which means that the server has closed the connection. We collect the bytes in a memory stream and decode them at the end; decoding each chunk separately could split a multi-byte UTF-8 character.

int idx = response.IndexOf("\r\n\r\n");
string headers = response[..idx];
string body = response[(idx + 4)..];

The headers are separated from the body by an empty line.

$ dotnet run
Received 892 bytes
HTTP/1.1 200 OK
Title: Example Domain

The server replies with its own protocol version in the status line, but it follows the rules of HTTP/1.0 in the response.

C# Socket echo server

In the following example, we create a TCP server and a client. The server sends back each received message in uppercase.

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

using var listener = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp);

listener.Bind(new IPEndPoint(IPAddress.Loopback, 5050));
listener.Listen(backlog: 10);

Console.WriteLine($"Listening on {listener.LocalEndPoint}");

while (true)
{
    Socket client = await listener.AcceptAsync();
    _ = HandleClientAsync(client);
}

static async Task HandleClientAsync(Socket client)
{
    using (client)
    {
        Console.WriteLine($"Client connected: {client.RemoteEndPoint}");

        byte[] buf = new byte[1024];
        int n;

        while ((n = await client.ReceiveAsync(buf)) > 0)
        {
            string msg = Encoding.UTF8.GetString(buf, 0, n);
            Console.WriteLine($"Received: {msg}");

            byte[] reply = Encoding.UTF8.GetBytes(msg.ToUpperInvariant());
            await client.SendAsync(reply);
        }

        Console.WriteLine("Client disconnected");
    }
}

This is the server.

listener.Bind(new IPEndPoint(IPAddress.Loopback, 5050));
listener.Listen(backlog: 10);

We bind the socket to port 5050 on the loopback interface, so that only programs on the same machine can connect. The Listen method puts the socket into the listening state; the backlog is the maximum number of pending connections waiting to be accepted.

Socket client = await listener.AcceptAsync();
_ = HandleClientAsync(client);

The AcceptAsync method waits for a client and returns a new socket for the connection. We handle each client in a separate task without awaiting it, so the server can accept other clients in the meantime.

while ((n = await client.ReceiveAsync(buf)) > 0)

We read messages until the client closes the connection.

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

using var socket = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp);

await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5050));

string[] messages = ["hello", "an old falcon", "bye"];
byte[] buf = new byte[1024];

foreach (var msg in messages)
{
    await socket.SendAsync(Encoding.UTF8.GetBytes(msg));

    int n = await socket.ReceiveAsync(buf);
    Console.WriteLine($"{msg} -> {Encoding.UTF8.GetString(buf, 0, n)}");
}

socket.Shutdown(SocketShutdown.Both);

This is the client. It sends three messages and prints the replies.

socket.Shutdown(SocketShutdown.Both);

The Shutdown method tells the other side that no more data will be sent; the server's ReceiveAsync then returns 0.

$ dotnet run
hello -> HELLO
an old falcon -> AN OLD FALCON
bye -> BYE

We start the server in one terminal and the client in another. This is the output of the server:

$ dotnet run
Listening on 127.0.0.1:5050
Client connected: 127.0.0.1:40290
Received: hello
Received: an old falcon
Received: bye
Client disconnected

TCP is a byte stream; it does not preserve message boundaries. Here each message is answered before the next one is sent, so the messages arrive separately. In general, two messages sent quickly one after another may be received in a single ReceiveAsync call, and a long message may be split into several. Real protocols therefore delimit messages, for instance with a newline character or a length prefix. The TcpListener article shows a line-based protocol built on TcpListener and streams.

Source

Socket class - language reference

In this article we have worked with sockets in C#.

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.