Networking and HttpClient

.NET wraps sockets in convenient classes: TcpListener (server: accepts connections), TcpClient (client: connects and exposes a NetworkStream), and UdpClient (sends and receives datagrams).

Concept

IP address Identifies a host: IPv4 (192.168.1.50) or IPv6 (::1). 127.0.0.1 / ::1 are the loopback addresses (this machine)

Identifies a service on a host (HTTP 80, HTTPS 443, DoIP 13400)

Resolves names (localhost, api.example.com) to IP addresses — Dns.GetHostAddressesAsync

Connection-oriented, reliable, ordered byte stream (HTTP, DoIP, databases)

Connectionless datagrams: fast, no delivery or ordering guarantee (discovery, streaming, some telemetry)

The OS endpoint for sending and receiving; System.Net.Sockets.Socket is the low-level API.NET wraps sockets in convenient classes: TcpListener (server: accepts connections), TcpClient (client: connects and exposes a NetworkStream), and UdpClient (sends and receives datagrams). All have async methods — networking is I/O-bound, so always use them (Module 31).

TCP is a *stream*: one WriteAsync of 8 bytes may arrive as two reads of 4 bytes. Real protocols therefore add framing — a length prefix or a delimiter (Module 61).

HTTP and HttpClient HTTP is a request/response protocol on top of TCP (TLS-encrypted for HTTPS):

GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove)

https://host/ecus/VCU?verbose=yes — path + query parameters (escape values with Uri.EscapeDataString)

User-Agent, Accept, Content-Type, Authorization: Bearer <token>

2xx success (200 OK, 201 Created, 204 No Content), 4xx client error (400, 401 Unauthorized, 403 Forbidden, 404), 5xx server error (500, 503)

Usually JSON: GetFromJsonAsync<T>, PostAsJsonAsync, PutAsJsonAsync, PatchAsync, ReadFromJsonAsync<T> Key rules for HttpClient:

Reuse one instance (or use IHttpClientFactory). Each instance owns a connection pool; creating one per request exhausts sockets.

EnsureSuccessStatusCode() throws HttpRequestException for non-2xx responses; IsSuccessStatusCode lets you branch instead.

Timeouts: HttpClient.Timeout (default 100 s) for all requests, plus a CancellationToken for each call.

Retries: retry only transient failures (timeouts, 408, 429, 5xx) with exponential backoff. Microsoft.Extensions.Http.Resilience provides standard policies (Module 58).

Authentication: send a token in the Authorization header (new AuthenticationHeaderValue("Bearer", token)). Never put secrets in URLs.

TLS/HTTPS: HttpClient validates the server certificate by default. Disabling validation (ServerCertificateCustomValidationCallback = (_, _, _, _) => true) is only acceptable against a local test server.

IHttpClientFactory (from Microsoft.Extensions.Http) manages handler lifetimes (so DNS changes are picked up), supports named and typed clients, and plugs into DI (Module 45).

Why It Matters

Test benches, diagnostic gateways (DoIP), cloud back-ends, CI dashboards and OTA update servers are all reached over TCP, UDP or HTTP. Misused HttpClient (socket exhaustion, no timeouts, blind retries) is one of the most common production bugs in .NET.

Syntax

var listener = new TcpListener(IPAddress.Loopback, 5000); listener.Start();
using TcpClient client = await listener.AcceptTcpClientAsync();
await tcp.ConnectAsync(IPAddress.Loopback, 5000); NetworkStream s = tcp.GetStream();
await udp.SendAsync(bytes, bytes.Length, endpoint);
Ecu? e = await http.GetFromJsonAsync<Ecu>("/ecus/VCU", token);
HttpResponseMessage r = await http.PostAsJsonAsync("/ecus", ecu);

Example

36.1 TCP and UDP on the loopback interface A TCP echo server answers a UDS-style request (22 F1 90 = read data identifier F190, the VIN), a UDP datagram is sent and received, and a connection to a closed port fails.

36.2 HttpClient against a real local REST API The program starts a tiny ASP.NET Core minimal API (#:sdk Microsoft.NET.Sdk.Web, Module 51) on a free loopback port, then exercises it with HttpClient: JSON GET/POST/DELETE, status codes, headers, query escaping, retries with backoff, cancellation, and IHttpClientFactory.

using System.Net;
using System.Net.Sockets;
using System.Text;

// IP addresses and DNS
IPAddress loopback = IPAddress.Parse("127.0.0.1");
Console.WriteLine($"IPv4 loopback: {loopback}, IsLoopback: {IPAddress.IsLoopback(loopback)}, IPv6 loopback: {IPAddress.IPv6Loopback}");
IPAddress[] resolved = await Dns.GetHostAddressesAsync("localhost");
Console.WriteLine($"DNS 'localhost' resolves to a loopback address: {resolved.Any(IPAddress.IsLoopback)}");
var endpoint = IPEndPoint.Parse("192.168.1.50:13400");      // e.g. a DoIP gateway
Console.WriteLine($"endpoint {endpoint.Address} port {endpoint.Port}, family {endpoint.AddressFamily}");

// ---- TCP: a tiny echo server and a client on the loopback interface ----
var listener = new TcpListener(IPAddress.Loopback, port: 0);   // port 0 = any free port
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;

Task server = Task.Run(async () =>
{
    using TcpClient conn = await listener.AcceptTcpClientAsync();
    using NetworkStream ns = conn.GetStream();
    byte[] buffer = new byte[256];
    int n = await ns.ReadAsync(buffer);
    string request = Encoding.ASCII.GetString(buffer, 0, n);
    string response = request == "2
…
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.Net;
using System.Net.Http.Json;

// A tiny local REST API to talk to (ASP.NET Core minimal API on a free loopback port)
var builder = WebApplication.CreateSlimBuilder();
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
var app = builder.Build();
var ecus = new Dictionary<string, Ecu> { ["VCU"] = new("VCU", "2.4.1"), ["BMS"] = new("BMS", "1.9.0") };
int flaky = 0;
app.MapGet("/ecus/{code}", (string code, HttpRequest req) =>
    ecus.TryGetValue(code, out var e) ? Results.Ok(e with { RequestedBy = req.Headers.UserAgent.ToString() }) : Results.NotFound());
app.MapPost("/ecus", (Ecu e) => { ecus[e.Code] = e; return Results.Created($"/ecus/{e.Code}", e); });
app.MapDelete("/ecus/{code}", (string code) => ecus.Remove(code) ? Results.NoContent() : Results.NotFound());
app.MapGet("/flaky", () => ++flaky < 3 ? Results.StatusCode(503) : Results.Ok("recovered"));
app.MapGet("/slow", async () => { await Task.Delay(2000); return "late"; });
await app.StartAsync();
string baseUrl = app.Urls.First();

// ---- HttpClient: reuse one instance ----
using var http = new HttpClient { BaseAddress = new 
…

Expected Result

The server echoed the User-Agent header back (RequestedBy), the /flaky endpoint failed twice with 503 before the retry loop succeeded, and the /slow request was cancelled by its 200 ms token long before the 10 s client timeout.

IPv4 loopback: 127.0.0.1, IsLoopback: True, IPv6 loopback: ::1
DNS 'localhost' resolves to a loopback address: True
endpoint 192.168.1.50 port 13400, family InterNetwork
TCP request '22 F1 90' -> response '62 F1 90 45 48 42'
UDP received 3 bytes: 021003 from a loopback sender: True
SocketException: ConnectionRefused
GET /ecus/VCU -> Ecu { Code = VCU, Firmware = 2.4.1, RequestedBy = SampleApp-Diag/1.0 }
GET /ecus/XYZ -> 404 NotFound, IsSuccess False
EnsureSuccessStatusCode -> HttpRequestException (NotFound)
POST /ecus -> 201 Created, Location: /ecus/ABS
DELETE /ecus/ABS -> 204 NoContent
escaped query: /ecus/BMS?verbose=yes%20%26%20more
  attempt 1 failed (503), retrying in 50 ms
  attempt 2 failed (503), retrying in 100 ms
GET /flaky after retries -> recovered
GET /slow cancelled after 200 ms
IHttpClientFactory client -> BMS 1.9.0

Common Mistakes

new HttpClient() inside a loop or a using block per request — socket exhaustion under load.

No timeout or cancellation — a hung server hangs your application.

Retrying non-idempotent POST requests blindly, which creates duplicates.

Assuming one TCP ReadAsync returns one whole message.

Building query strings by concatenation without escaping.

Disabling certificate validation "temporarily" and shipping it.

Calling .Result on network tasks (deadlocks and blocked threads — Module 31).

Best Practices

One long-lived HttpClient per base address, or IHttpClientFactory with typed clients.

Always pass a CancellationToken; set sensible timeouts.

Use the System.Net.Http.Json extensions for JSON APIs.

Retry only transient errors, with exponential backoff and jitter, and a limit.

Frame TCP messages (length prefix) and handle partial reads.

Keep tokens in configuration/secret stores, send them in the Authorization header over HTTPS only.

Interview Relevance

High: "TCP vs UDP?", "why should HttpClient be reused?", "what problem does IHttpClientFactory solve?", "idempotent HTTP methods?", "401 vs 403?", "how do you implement retries safely?".

Practice Questions

Extend the TCP server to handle several clients concurrently (one task per accepted client).

Implement a length-prefixed protocol: 2-byte big-endian length followed by the payload; handle partial reads.

Write a typed client EcuApiClient with methods GetAsync(code) and CreateAsync(ecu) that wraps HttpClient.

Add a PUT /ecus/{code} endpoint and call it with PutAsJsonAsync.

Quick Quiz

Q1. (Multiple choice) Which protocol guarantees ordered, reliable delivery? A) UDP B) TCP C) IP D) DNS Answer: B.

Q2. (Predict the output) GET /ecus/XYZ for an unknown ECU, then EnsureSuccessStatusCode().

Answer: Status 404 NotFound, and EnsureSuccessStatusCode throws HttpRequestException with StatusCode NotFound (verified).

Q3. (True/False) Creating a new HttpClient for each request is the recommended pattern.

Answer: False. Reuse clients or use IHttpClientFactory.

Q4. (Find the bug) var text = Encoding.ASCII.GetString(buffer); // after one ReadAsync of a 4 KB message

Answer: TCP may deliver the message in several reads; loop until the full framed length has been received, and decode only the bytes actually read (GetString(buffer, 0, n)).

Q5. (Multiple choice) Which status code does a successful POST that creates a resource usually return? A) 200 B) 201 C) 204 D) 302

Answer: B (verified: 201 Created with a Location header).

Coding Challenge

Write a TCP server on a free loopback port that receives a line of text and replies with it in upper case; connect a client, send ecu ok, and print the reply.

using System.Net;
using System.Net.Sockets;

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

Task server = Task.Run(async () =>
{
    using TcpClient conn = await listener.AcceptTcpClientAsync();
    using var reader = new StreamReader(conn.GetStream());
    using var writer = new StreamWriter(conn.GetStream()) { AutoFlush = true };
    string? line = await reader.ReadLineAsync();
    await writer.WriteLineAsync(line?.ToUpperInvariant());
});

using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
using var r = new StreamReader(client.GetStream());
using var w = new StreamWriter(client.GetStream()) { AutoFlush = true };
await w.WriteLineAsync("ecu ok");
Console.WriteLine($"reply: {await r.ReadLineAsync()}");
await server;
listener.Stop();