Networking con C#
.NET offre uno stack di rete moderno e particolarmente curato sul fronte delle prestazioni: il namespace System.Net.Sockets espone sia API di basso livello sia classi di comodo, async/await è integrato in ogni operazione di I/O, e librerie come System.IO.Pipelines permettono di scrivere parser di protocollo efficienti senza allocazioni superflue. In questo articolo costruiremo server e client TCP, gestiremo cancellazione e timeout con CancellationToken, lavoreremo con UDP, DNS e TLS e vedremo come configurare correttamente HttpClient.
Il modello asincrono di .NET
In .NET tutte le operazioni di rete hanno una variante asincrona che restituisce un Task o un ValueTask. Quando un'operazione è in attesa di dati, nessun thread resta bloccato: il thread torna al pool e la continuazione viene eseguita quando il sistema operativo segnala il completamento. La cancellazione è uniforme in tutto il framework grazie al CancellationToken, che è anche lo strumento principale per implementare i timeout.
Un server TCP con TcpListener
TcpListener è una classe di comodo sopra Socket. Per ogni connessione accettata si avvia un task indipendente che gestisce il client:
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace NetworkingDemo;
public static class EchoServer
{
private static readonly TimeSpan IdleTimeout = TimeSpan.FromSeconds(30);
public static async Task RunAsync(int port, CancellationToken cancellationToken)
{
var listener = new TcpListener(IPAddress.Any, port);
listener.Start();
Console.WriteLine($"Server in ascolto sulla porta {port}");
try
{
while (!cancellationToken.IsCancellationRequested)
{
TcpClient client = await listener.AcceptTcpClientAsync(cancellationToken);
// Ogni client viene gestito senza attendere il completamento
_ = HandleClientAsync(client, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Arresto richiesto
}
finally
{
listener.Stop();
}
}
private static async Task HandleClientAsync(TcpClient client, CancellationToken serverToken)
{
string peer = client.Client.RemoteEndPoint?.ToString() ?? "sconosciuto";
Console.WriteLine($"Nuova connessione da {peer}");
using (client)
{
NetworkStream stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.UTF8);
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
try
{
while (true)
{
// Token combinato: arresto del server oppure timeout di inattività
using var idleCts = CancellationTokenSource.CreateLinkedTokenSource(serverToken);
idleCts.CancelAfter(IdleTimeout);
string? line = await reader.ReadLineAsync(idleCts.Token);
if (line is null)
{
break;
}
await writer.WriteLineAsync($"echo: {line}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"{peer}: timeout di inattività o arresto del server");
}
catch (IOException ex)
{
Console.WriteLine($"{peer}: {ex.Message}");
}
}
Console.WriteLine($"{peer} disconnesso");
}
}
Il pattern CreateLinkedTokenSource più CancelAfter è il modo idiomatico per combinare un timeout locale con la cancellazione globale: l'operazione si interrompe al verificarsi del primo dei due eventi. Il UTF8Encoding(false) passato allo StreamWriter evita l'invio del byte order mark all'inizio dello stream.
Il punto di ingresso collega la cancellazione alla pressione di Ctrl+C:
using NetworkingDemo;
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
// Si impedisce la terminazione immediata per consentire lo shutdown ordinato
eventArgs.Cancel = true;
cts.Cancel();
};
await EchoServer.RunAsync(9000, cts.Token);
Framing con prefisso di lunghezza
Come in ogni protocollo su TCP, serve un meccanismo di framing per ricostruire i confini dei messaggi. BinaryPrimitives permette di codificare la lunghezza in big-endian, e da .NET 7 il metodo ReadExactlyAsync legge esattamente il numero di byte richiesto, eliminando il classico ciclo di letture parziali.
using System.Buffers;
using System.Buffers.Binary;
namespace NetworkingDemo;
public static class FrameCodec
{
private const int HeaderSize = 4;
private const int MaxFrameSize = 16 * 1024 * 1024;
public static async ValueTask WriteFrameAsync(Stream stream, ReadOnlyMemory<byte> payload, CancellationToken cancellationToken)
{
if (payload.Length > MaxFrameSize)
{
throw new InvalidDataException($"Payload troppo grande: {payload.Length} byte");
}
byte[] buffer = ArrayPool<byte>.Shared.Rent(HeaderSize + payload.Length);
try
{
// Lunghezza in network byte order seguita dal payload, in un'unica scrittura
BinaryPrimitives.WriteInt32BigEndian(buffer, payload.Length);
payload.CopyTo(buffer.AsMemory(HeaderSize));
await stream.WriteAsync(buffer.AsMemory(0, HeaderSize + payload.Length), cancellationToken);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public static async ValueTask<byte[]> ReadFrameAsync(Stream stream, CancellationToken cancellationToken)
{
byte[] header = new byte[HeaderSize];
// ReadExactlyAsync solleva EndOfStreamException se lo stream termina prima
await stream.ReadExactlyAsync(header, cancellationToken);
int length = BinaryPrimitives.ReadInt32BigEndian(header);
if (length is < 0 or > MaxFrameSize)
{
throw new InvalidDataException($"Lunghezza del frame non valida: {length}");
}
byte[] payload = new byte[length];
await stream.ReadExactlyAsync(payload, cancellationToken);
return payload;
}
}
L'uso di ArrayPool in scrittura riduce la pressione sul garbage collector quando il server invia molti messaggi: il buffer viene preso in prestito e restituito invece di essere allocato ogni volta.
Un server JSON con System.IO.Pipelines
Per server ad alte prestazioni Microsoft ha sviluppato System.IO.Pipelines, la stessa libreria su cui si basa Kestrel. Una pipe gestisce automaticamente i buffer, la backpressure e il problema dei messaggi spezzati: il parser esamina i dati disponibili e comunica alla pipe quanti byte ha consumato e quanti ne ha solo esaminati.
using System.Buffers;
using System.Buffers.Binary;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
namespace NetworkingDemo;
public sealed record CommandRequest(int Id, string Command, int[]? Values);
public sealed record CommandResponse(int Id, object? Result = null, string? Error = null);
public sealed class PipelineServer
{
private const int MaxFrameSize = 1024 * 1024;
public async Task RunAsync(int port, CancellationToken cancellationToken)
{
using var listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
// Socket dual-stack: accetta connessioni IPv4 e IPv6
listener.DualMode = true;
listener.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
listener.Listen(backlog: 512);
Console.WriteLine($"Server con pipelines in ascolto sulla porta {port}");
while (!cancellationToken.IsCancellationRequested)
{
Socket client = await listener.AcceptAsync(cancellationToken);
client.NoDelay = true;
_ = ProcessClientAsync(client, cancellationToken);
}
}
private static async Task ProcessClientAsync(Socket socket, CancellationToken cancellationToken)
{
await using var stream = new NetworkStream(socket, ownsSocket: true);
PipeReader reader = PipeReader.Create(stream);
PipeWriter writer = PipeWriter.Create(stream);
try
{
while (true)
{
ReadResult result = await reader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> buffer = result.Buffer;
// Si elaborano tutti i frame completi presenti nel buffer
while (TryReadFrame(ref buffer, out ReadOnlySequence<byte> frame))
{
CommandResponse response = Dispatch(frame);
WriteFrame(writer, JsonSerializer.SerializeToUtf8Bytes(response));
}
// Consumati i frame completi, esaminato tutto il resto
reader.AdvanceTo(buffer.Start, buffer.End);
FlushResult flush = await writer.FlushAsync(cancellationToken);
if (result.IsCompleted || flush.IsCompleted)
{
break;
}
}
}
catch (Exception ex) when (ex is IOException or InvalidDataException or OperationCanceledException)
{
Console.WriteLine($"Connessione terminata: {ex.Message}");
}
finally
{
await reader.CompleteAsync();
await writer.CompleteAsync();
}
}
private static bool TryReadFrame(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> frame)
{
frame = default;
if (buffer.Length < 4)
{
return false;
}
Span<byte> header = stackalloc byte[4];
buffer.Slice(0, 4).CopyTo(header);
int length = BinaryPrimitives.ReadInt32BigEndian(header);
if (length is < 0 or > MaxFrameSize)
{
throw new InvalidDataException($"Frame non valido: {length} byte");
}
if (buffer.Length < 4 + length)
{
// Frame incompleto: si attendono altri dati
return false;
}
frame = buffer.Slice(4, length);
buffer = buffer.Slice(4 + length);
return true;
}
private static void WriteFrame(PipeWriter writer, byte[] payload)
{
Span<byte> span = writer.GetSpan(4 + payload.Length);
BinaryPrimitives.WriteInt32BigEndian(span, payload.Length);
payload.CopyTo(span[4..]);
writer.Advance(4 + payload.Length);
}
private static CommandResponse Dispatch(ReadOnlySequence<byte> frame)
{
CommandRequest? request;
try
{
var jsonReader = new Utf8JsonReader(frame);
request = JsonSerializer.Deserialize<CommandRequest>(ref jsonReader, JsonSerializerOptions.Web);
}
catch (JsonException)
{
return new CommandResponse(0, Error: "JSON non valido");
}
if (request is null)
{
return new CommandResponse(0, Error: "Richiesta vuota");
}
return request.Command switch
{
"ping" => new CommandResponse(request.Id, Result: "pong"),
"sum" => new CommandResponse(request.Id, Result: request.Values?.Sum() ?? 0),
"time" => new CommandResponse(request.Id, Result: DateTimeOffset.UtcNow),
_ => new CommandResponse(request.Id, Error: $"Comando sconosciuto: {request.Command}"),
};
}
}
La chiamata reader.AdvanceTo(buffer.Start, buffer.End) è il cuore del meccanismo: il primo argomento indica fin dove i dati sono stati consumati (e possono essere liberati), il secondo fin dove sono stati esaminati. Se è rimasto un frame incompleto, la pipe lo conserva e la successiva ReadAsync non si completa finché non arrivano nuovi byte.
Il client con timeout di connessione e di richiesta
Il client si connette con TcpClient.ConnectAsync, che accetta un CancellationToken. Un unico token con scadenza può coprire sia la connessione sia lo scambio dei messaggi:
using System.Net.Sockets;
using System.Text.Json;
namespace NetworkingDemo;
public sealed class CommandClient : IAsyncDisposable
{
private readonly TcpClient _client = new();
private NetworkStream? _stream;
private int _nextId;
public async Task ConnectAsync(string host, int port, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
await _client.ConnectAsync(host, port, cts.Token);
}
catch (OperationCanceledException)
{
throw new TimeoutException($"Connessione a {host}:{port} scaduta dopo {timeout.TotalSeconds} secondi");
}
_client.NoDelay = true;
_stream = _client.GetStream();
}
public async Task<JsonElement> CallAsync(string command, int[]? values = null, TimeSpan? timeout = null)
{
if (_stream is null)
{
throw new InvalidOperationException("Client non connesso");
}
using var cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(5));
var request = new { id = Interlocked.Increment(ref _nextId), command, values };
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(request);
await FrameCodec.WriteFrameAsync(_stream, payload, cts.Token);
byte[] responseBytes = await FrameCodec.ReadFrameAsync(_stream, cts.Token);
using JsonDocument document = JsonDocument.Parse(responseBytes);
JsonElement root = document.RootElement;
if (root.TryGetProperty("error", out JsonElement error) && error.ValueKind == JsonValueKind.String)
{
throw new InvalidOperationException($"Errore remoto: {error.GetString()}");
}
// Clone rende l'elemento indipendente dal documento che viene rilasciato
return root.GetProperty("result").Clone();
}
public ValueTask DisposeAsync()
{
_client.Dispose();
return ValueTask.CompletedTask;
}
}
using NetworkingDemo;
await using var client = new CommandClient();
await client.ConnectAsync("127.0.0.1", 9000, TimeSpan.FromSeconds(3));
Console.WriteLine(await client.CallAsync("ping"));
Console.WriteLine(await client.CallAsync("sum", [1, 2, 3, 4]));
Console.WriteLine(await client.CallAsync("time"));
Un port scanner con Parallel.ForEachAsync
Parallel.ForEachAsync esegue operazioni asincrone in parallelo con un grado di concorrenza limitato, ed è perfetto per uno strumento diagnostico che verifica molte porte:
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net.Sockets;
namespace NetworkingDemo;
public static class PortScanner
{
public static async Task<IReadOnlyList<(int Port, double LatencyMs)>> ScanAsync(
string host, IEnumerable<int> ports, int concurrency = 100, int timeoutMs = 500)
{
var open = new ConcurrentBag<(int Port, double LatencyMs)>();
var options = new ParallelOptions { MaxDegreeOfParallelism = concurrency };
await Parallel.ForEachAsync(ports, options, async (port, token) =>
{
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
cts.CancelAfter(timeoutMs);
long start = Stopwatch.GetTimestamp();
try
{
await socket.ConnectAsync(host, port, cts.Token);
open.Add((port, Stopwatch.GetElapsedTime(start).TotalMilliseconds));
}
catch (Exception ex) when (ex is SocketException or OperationCanceledException)
{
// Porta chiusa, filtrata o timeout scaduto
}
});
return open.OrderBy(result => result.Port).ToList();
}
}
using NetworkingDemo;
var results = await PortScanner.ScanAsync("127.0.0.1", Enumerable.Range(1, 1024));
foreach (var (port, latency) in results)
{
Console.WriteLine($"{port,5} aperta ({latency:F2} ms)");
}
UDP con UdpClient
UdpClient semplifica l'invio e la ricezione di datagrammi. ReceiveAsync restituisce un UdpReceiveResult con il contenuto e l'indirizzo del mittente. Ecco un server di heartbeat e un client che misura il round-trip time gestendo i pacchetti persi:
using System.Buffers.Binary;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
namespace NetworkingDemo;
public static class Heartbeat
{
public static async Task RunServerAsync(int port, CancellationToken cancellationToken)
{
using var server = new UdpClient(port);
Console.WriteLine($"Server heartbeat UDP sulla porta {port}");
while (!cancellationToken.IsCancellationRequested)
{
UdpReceiveResult received = await server.ReceiveAsync(cancellationToken);
// Il datagramma viene rispedito invariato al mittente
await server.SendAsync(received.Buffer, received.RemoteEndPoint, cancellationToken);
}
}
public static async Task RunClientAsync(string host, int port, int count)
{
using var client = new UdpClient();
client.Connect(host, port);
byte[] payload = new byte[12];
for (int sequence = 0; sequence < count; sequence++)
{
// Payload: numero di sequenza e timestamp di invio
BinaryPrimitives.WriteInt32BigEndian(payload, sequence);
BinaryPrimitives.WriteInt64BigEndian(payload.AsSpan(4), Stopwatch.GetTimestamp());
await client.SendAsync(payload);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
try
{
UdpReceiveResult reply = await client.ReceiveAsync(cts.Token);
int replySequence = BinaryPrimitives.ReadInt32BigEndian(reply.Buffer);
long sentAt = BinaryPrimitives.ReadInt64BigEndian(reply.Buffer.AsSpan(4));
Console.WriteLine($"seq={replySequence} rtt={Stopwatch.GetElapsedTime(sentAt).TotalMilliseconds:F3} ms");
}
catch (OperationCanceledException)
{
// UDP non garantisce la consegna: il pacchetto viene considerato perso
Console.WriteLine($"seq={sequence} perso");
}
await Task.Delay(500);
}
}
}
Risoluzione DNS
La classe statica Dns espone la risoluzione tramite il resolver del sistema operativo. Da .NET 6 i metodi asincroni accettano un CancellationToken e permettono di filtrare per famiglia di indirizzi:
using System.Net;
using System.Net.Sockets;
namespace NetworkingDemo;
public static class DnsInspector
{
public static async Task<(IPAddress[] IPv4, IPAddress[] IPv6)> ResolveAsync(string host, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
// Le due query vengono eseguite in parallelo
Task<IPAddress[]> ipv4 = Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cts.Token);
Task<IPAddress[]> ipv6 = Dns.GetHostAddressesAsync(host, AddressFamily.InterNetworkV6, cts.Token);
IPAddress[] v4 = await SafeAsync(ipv4);
IPAddress[] v6 = await SafeAsync(ipv6);
return (v4, v6);
}
public static async Task<string?> ReverseAsync(string ip)
{
try
{
IPHostEntry entry = await Dns.GetHostEntryAsync(IPAddress.Parse(ip));
return entry.HostName;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.HostNotFound)
{
return null;
}
}
private static async Task<IPAddress[]> SafeAsync(Task<IPAddress[]> task)
{
try
{
return await task;
}
catch (SocketException)
{
// Nessun record per quella famiglia di indirizzi
return [];
}
}
}
using NetworkingDemo;
foreach (string host in new[] { "dotnet.microsoft.com", "example.com" })
{
var (ipv4, ipv6) = await DnsInspector.ResolveAsync(host, TimeSpan.FromSeconds(3));
Console.WriteLine($"{host}");
Console.WriteLine($" IPv4: {string.Join(", ", ipv4.Select(a => a.ToString()))}");
Console.WriteLine($" IPv6: {string.Join(", ", ipv6.Select(a => a.ToString()))}");
}
Console.WriteLine(await DnsInspector.ReverseAsync("8.8.8.8"));
Per record come MX, TXT o SRV la libreria standard non offre un'API pubblica: la soluzione più diffusa è il pacchetto DnsClient, che fornisce un resolver completo e asincrono.
TLS con SslStream
SslStream avvolge un qualsiasi stream aggiungendo TLS. Con SslClientAuthenticationOptions si configurano nome del server per SNI, protocolli ammessi e un'eventuale validazione personalizzata del certificato:
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace NetworkingDemo;
public sealed record CertificateInfo(
string Host, SslProtocols Protocol, string Cipher, string Subject,
string Issuer, DateTime ExpiresUtc, int DaysLeft, SslPolicyErrors PolicyErrors);
public static class CertificateInspector
{
public static async Task<CertificateInfo> InspectAsync(string host, int port = 443, int timeoutMs = 5000)
{
using var cts = new CancellationTokenSource(timeoutMs);
using var tcp = new TcpClient();
await tcp.ConnectAsync(host, port, cts.Token);
var policyErrors = SslPolicyErrors.None;
await using var ssl = new SslStream(tcp.GetStream(), leaveInnerStreamOpen: false);
var options = new SslClientAuthenticationOptions
{
// Nome usato sia per SNI sia per la verifica del certificato
TargetHost = host,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
RemoteCertificateValidationCallback = (_, _, _, errors) =>
{
policyErrors = errors;
// Solo un certificato senza errori viene accettato
return errors == SslPolicyErrors.None;
},
};
await ssl.AuthenticateAsClientAsync(options, cts.Token);
var certificate = new X509Certificate2(ssl.RemoteCertificate!);
DateTime expires = certificate.NotAfter.ToUniversalTime();
return new CertificateInfo(
host,
ssl.SslProtocol,
ssl.NegotiatedCipherSuite.ToString(),
certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false),
certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: true),
expires,
(int)(expires - DateTime.UtcNow).TotalDays,
policyErrors);
}
}
using System.Security.Authentication;
using NetworkingDemo;
foreach (string host in new[] { "github.com", "expired.badssl.com" })
{
try
{
Console.WriteLine(await CertificateInspector.InspectAsync(host));
}
catch (AuthenticationException ex)
{
Console.WriteLine($"{host}: certificato rifiutato ({ex.Message})");
}
}
La callback di validazione è il punto in cui molti sviluppatori, per risolvere un problema in sviluppo, scrivono return true; accettando qualsiasi certificato. È una scorciatoia che non deve mai arrivare in produzione: annulla completamente la protezione di TLS.
HttpClient: istanza condivisa e SocketsHttpHandler
HttpClient è progettato per essere riutilizzato. Creare una nuova istanza per ogni richiesta, magari dentro un blocco using, porta all'esaurimento delle porte locali, perché le connessioni chiuse restano nello stato TIME_WAIT. D'altra parte, un'istanza statica che vive per sempre non si accorge dei cambiamenti DNS. La soluzione è configurare un SocketsHttpHandler con una durata massima delle connessioni nel pool:
using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
namespace NetworkingDemo;
public sealed record NuGetIndex(string Version, NuGetResource[] Resources);
// Le proprietà JSON dell'indice NuGet usano il prefisso @
public sealed record NuGetResource(
[property: JsonPropertyName("@id")] string Id,
[property: JsonPropertyName("@type")] string Type);
public static class HttpDemo
{
private static readonly HttpClient Client = new(new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(3),
// Le connessioni vengono rinnovate periodicamente per rispettare i cambi DNS
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
MaxConnectionsPerServer = 50,
AutomaticDecompression = DecompressionMethods.All,
EnableMultipleHttp2Connections = true,
})
{
// Timeout complessivo della richiesta
Timeout = TimeSpan.FromSeconds(15),
DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
};
private static readonly HashSet<HttpStatusCode> RetryableStatus =
[
HttpStatusCode.TooManyRequests,
HttpStatusCode.BadGateway,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.GatewayTimeout,
];
public static async Task<HttpResponseMessage> GetWithRetryAsync(
string url, int maxRetries = 3, CancellationToken cancellationToken = default)
{
Exception? lastError = null;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
HttpResponseMessage response = await Client.GetAsync(url, cancellationToken);
if (!RetryableStatus.Contains(response.StatusCode) || attempt == maxRetries)
{
return response;
}
response.Dispose();
}
catch (HttpRequestException ex)
{
lastError = ex;
}
catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
// Timeout dell'HttpClient, non cancellazione richiesta dal chiamante
lastError = ex;
}
// Backoff esponenziale con jitter
int delay = (int)(200 * Math.Pow(2, attempt)) + Random.Shared.Next(200);
await Task.Delay(delay, cancellationToken);
}
throw new HttpRequestException($"Tentativi esauriti per {url}", lastError);
}
public static async Task RunAsync()
{
using HttpResponseMessage response = await GetWithRetryAsync("https://api.nuget.org/v3/index.json");
response.EnsureSuccessStatusCode();
NuGetIndex? index = await response.Content.ReadFromJsonAsync<NuGetIndex>();
Console.WriteLine($"HTTP/{response.Version} - versione indice {index?.Version}");
foreach (NuGetResource resource in index?.Resources.Take(5) ?? [])
{
Console.WriteLine($" {resource.Type}: {resource.Id}");
}
}
}
Nelle applicazioni ASP.NET Core, invece di gestire manualmente l'istanza condivisa, conviene registrare client tipizzati con IHttpClientFactory e aggiungere le politiche di resilienza con il pacchetto Microsoft.Extensions.Http.Resilience:
// Program.cs di un'applicazione ASP.NET Core
builder.Services
.AddHttpClient<InventoryClient>(client =>
{
client.BaseAddress = new Uri("https://inventory.example.com/api/");
client.Timeout = TimeSpan.FromSeconds(10);
})
// Ritentativi, circuit breaker e timeout con impostazioni predefinite sensate
.AddStandardResilienceHandler();
Buone pratiche
- Propagare sempre il
CancellationTokene usareCancelAftero token collegati per implementare i timeout. - Usare
ReadExactlyAsyncoSystem.IO.Pipelinesper il framing, invece di gestire a mano le letture parziali. - Riutilizzare
HttpClientconfigurandoPooledConnectionLifetime, oppure affidarsi aIHttpClientFactory. - Non accettare mai certificati non validi nella callback di validazione TLS.
- Limitare la concorrenza con
Parallel.ForEachAsyncoSemaphoreSlimquando si aprono molte connessioni in uscita.
Conclusioni
.NET combina un'API di rete completa con un modello asincrono maturo e strumenti orientati alle prestazioni come System.IO.Pipelines e ArrayPool. Dal semplice TcpListener al server con pipe, da UdpClient a SslStream fino a HttpClient con le politiche di resilienza, la piattaforma offre il livello di astrazione adatto a ogni esigenza. Timeout tramite cancellazione, framing robusto e attenzione al ciclo di vita delle connessioni sono gli ingredienti che trasformano questo potenziale in servizi di rete affidabili.