C# Story

Chapter #14 – Network Library

System.Net: HttpClient, TcpClient, UdpClient, Socket, WebSocket

14.0 Prologue

.NET's networking support lives primarily in the System.Net and System.Net.Sockets namespaces. This chapter covers the main APIs at each layer of the networking stack: high-level HTTP with HttpClient, reliable TCP streams with TcpClient / TcpListener, connectionless UDP with UdpClient, raw socket programming with Socket, full-duplex messaging with WebSocket, and the supporting address types IPAddress, IPEndPoint, and Dns.

14.1 HttpClient

HttpClient is the primary class for sending HTTP requests and receiving responses. It is designed to be instantiated once and reused for the lifetime of the application (or injected as a singleton via IHttpClientFactory). using var client = new HttpClient(); client.BaseAddress = new Uri("https://api.example.com/"); client.DefaultRequestHeaders.Add("Accept", "application/json"); // GET string json = await client.GetStringAsync("items/42"); // POST var payload = new StringContent( """{"name":"widget"}""", Encoding.UTF8, "application/json"); HttpResponseMessage resp = await client.PostAsync("items", payload); resp.EnsureSuccessStatusCode(); Key members:
  • GetAsync / PostAsync / PutAsync / DeleteAsync — verb-specific helpers
  • SendAsync(HttpRequestMessage) — full control over method, headers, content
  • GetStreamAsync — streams response body without buffering
  • GetFromJsonAsync<T> / PostAsJsonAsync (System.Net.Http.Json) — JSON helpers that avoid manual serialization
// streaming download await using Stream stream = await client.GetStreamAsync("large-file.bin"); await stream.CopyToAsync(File.OpenWrite("output.bin")); // JSON helper var item = await client.GetFromJsonAsync<Item>("items/42"); Use IHttpClientFactory in ASP.NET Core to manage HttpClient lifetimes and avoid socket exhaustion from excessive construction and disposal.

14.2 TcpClient and TcpListener

TcpClient and TcpListener wrap Socket for TCP streams. They expose a NetworkStream that supports both synchronous and asynchronous reads and writes. // --- server --- var listener = new TcpListener(IPAddress.Any, 5000); listener.Start(); TcpClient client = await listener.AcceptTcpClientAsync(); NetworkStream ns = client.GetStream(); using var reader = new StreamReader(ns); using var writer = new StreamWriter(ns) { AutoFlush = true }; string? line = await reader.ReadLineAsync(); await writer.WriteLineAsync("Echo: " + line); // --- client --- using var tcp = new TcpClient(); await tcp.ConnectAsync("localhost", 5000); NetworkStream ns = tcp.GetStream(); using var writer = new StreamWriter(ns) { AutoFlush = true }; using var reader = new StreamReader(ns); await writer.WriteLineAsync("hello"); Console.WriteLine(await reader.ReadLineAsync()); NetworkStream supports ReadAsync / WriteAsync with Memory<byte> overloads, making it easy to integrate with Span<T>-based parsing pipelines. Wrap the stream in SslStream (providing an X509Certificate2) for TLS. Call AuthenticateAsServerAsync / AuthenticateAsClientAsync before reading or writing.

14.3 UdpClient

UdpClient sends and receives datagrams. Unlike TCP it is connectionless — each Send / Receive is independent. Useful for low-latency telemetry, DNS queries, and multicast. // sender using var sender = new UdpClient(); byte[] data = Encoding.UTF8.GetBytes("ping"); await sender.SendAsync(data, data.Length, "localhost", 9000); // receiver using var recv = new UdpClient(9000); UdpReceiveResult result = await recv.ReceiveAsync(); string msg = Encoding.UTF8.GetString(result.Buffer); Console.WriteLine($"From {result.RemoteEndPoint}: {msg}"); For multicast, call JoinMulticastGroup(IPAddress) and bind to the multicast port. Use DropMulticastGroup to leave.

14.4 Socket

Socket in System.Net.Sockets provides direct access to the OS socket API. TcpClient and UdpClient are thin wrappers over it. Use Socket directly when you need control over socket options, raw protocols, or high-performance I/O. // async TCP client via Socket var ep = new IPEndPoint(IPAddress.Loopback, 5000); using var sock = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(ep); byte[] buf = Encoding.UTF8.GetBytes("hello\n"); await sock.SendAsync(buf, SocketFlags.None); byte[] recv = new byte[256]; int n = await sock.ReceiveAsync(recv, SocketFlags.None); Console.WriteLine(Encoding.UTF8.GetString(recv, 0, n)); Useful Socket members:
  • AcceptAsync — non-blocking accept on server sockets
  • SetSocketOption — control TCP_NODELAY, SO_REUSEADDR, timeouts, etc.
  • SendToAsync / ReceiveFromAsync — UDP-style with explicit endpoint
  • Poll — check readability / writeability / error state without blocking
For very high throughput, use SocketAsyncEventArgs to avoid per-operation allocations, or consider the System.Net.Http.SocketsHttpHandler pipeline that HttpClient uses internally.

14.5 WebSocket

ClientWebSocket (client side) and WebSocket (server side, obtained from HttpContext.WebSockets.AcceptWebSocketAsync() in ASP.NET Core) provide full-duplex message framing over a single HTTP upgrade connection. using var ws = new ClientWebSocket(); await ws.ConnectAsync(new Uri("wss://echo.websocket.org"), CancellationToken.None); // send byte[] msg = Encoding.UTF8.GetBytes("hello"); await ws.SendAsync(msg, WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); // receive byte[] buf = new byte[1024]; WebSocketReceiveResult res = await ws.ReceiveAsync(buf, CancellationToken.None); Console.WriteLine(Encoding.UTF8.GetString(buf, 0, res.Count)); await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); WebSocketMessageType is either Text or Binary. Messages larger than the receive buffer arrive across multiple calls — loop until result.EndOfMessage is true.

14.6 IPAddress, IPEndPoint, and Dns

These supporting types handle address representation and resolution: // IPAddress — parse or use constants IPAddress loopback = IPAddress.Loopback; // 127.0.0.1 IPAddress any = IPAddress.Any; // 0.0.0.0 IPAddress ip6 = IPAddress.IPv6Loopback; IPAddress parsed = IPAddress.Parse("192.168.1.10"); bool ok = IPAddress.TryParse("10.0.0.1", out IPAddress? addr); // IPEndPoint — address + port var ep = new IPEndPoint(IPAddress.Loopback, 8080); Console.WriteLine(ep); // 127.0.0.1:8080 // Dns — async hostname resolution IPHostEntry entry = await Dns.GetHostEntryAsync("example.com"); foreach (IPAddress a in entry.AddressList) Console.WriteLine(a); string host = await Dns.GetHostNameAsync(); // local machine name IPAddress.AddressFamily distinguishes IPv4 (AddressFamily.InterNetwork) from IPv6 (AddressFamily.InterNetworkV6). Pass the right family to Socket or TcpClient constructors.

14.7 Epilogue

.NET's networking stack scales from single HTTP calls with HttpClient to raw socket I/O with Socket. All APIs have async overloads that integrate cleanly with async / await and CancellationToken. For production services, prefer IHttpClientFactory for HTTP and System.IO.Pipelines for high-throughput socket work.

14.8 References

HttpClient — Microsoft docs
TcpClient — Microsoft docs
UdpClient — Microsoft docs
Socket — Microsoft docs
ClientWebSocket — Microsoft docs
Dns — Microsoft docs