Fondamenti di rete in .NET

Fondamenti di rete in .NET

Apriamo una nuova sezione dedicata al networking, ovvero alla comunicazione tra programmi attraverso una rete. Che si tratti di consumare un'API remota, costruire un server, scambiare messaggi in tempo reale o collegare microservizi, la capacità di comunicare in rete è centrale nello sviluppo moderno. La piattaforma .NET offre un ricco insieme di classi, organizzate principalmente nei namespace System.Net e System.Net.Sockets. In questo primo articolo poniamo le basi concettuali e vediamo gli strumenti fondamentali, prima di salire di livello negli articoli successivi.

Un modello a strati

La comunicazione di rete è organizzata in livelli, ciascuno costruito sopra il precedente. Alla base ci sono i protocolli di trasporto, principalmente TCP e UDP, che gestiscono l'invio dei dati tra due macchine. Sopra di essi si collocano i protocolli applicativi come HTTP, che struttura lo scambio in richieste e risposte. .NET fornisce astrazioni per ogni livello: dai socket a basso livello, che lavorano direttamente sul trasporto, fino a classi di alto livello come HttpClient, che nascondono la complessità sottostante. Scegliere il livello giusto è il primo passo di ogni progetto di rete.

Indirizzi IP e porte

Ogni dispositivo su una rete è identificato da un indirizzo IP, e ogni servizio in ascolto su quel dispositivo da una porta, un numero che distingue i diversi punti di comunicazione. In .NET, la classe IPAddress rappresenta un indirizzo, mentre IPEndPoint combina un indirizzo e una porta, individuando univocamente un punto di connessione.

using System.Net;

// Un indirizzo IP a partire da una stringa
IPAddress address = IPAddress.Parse("192.168.1.10");

// Indirizzi speciali predefiniti
IPAddress loopback = IPAddress.Loopback;   // 127.0.0.1, la macchina locale
IPAddress any = IPAddress.Any;             // ascolto su tutte le interfacce

// Un endpoint combina indirizzo e porta
var endpoint = new IPEndPoint(address, 8080);
Console.WriteLine(endpoint); // 192.168.1.10:8080

La risoluzione dei nomi con il DNS

Gli esseri umani usano nomi come example.com, non indirizzi numerici. Il sistema DNS traduce i nomi in indirizzi IP. La classe Dns espone questa funzionalità, con metodi asincroni che sfruttano il modello async studiato in precedenza, poiché una query DNS comporta a sua volta una comunicazione di rete.

using System.Net;

// Risolve un nome host nei suoi indirizzi IP
IPAddress[] addresses = await Dns.GetHostAddressesAsync("example.com");

foreach (IPAddress ip in addresses)
{
    Console.WriteLine(ip);
}

// Informazioni complete sull'host, inclusi eventuali alias
IPHostEntry entry = await Dns.GetHostEntryAsync("example.com");
Console.WriteLine(entry.HostName);

TCP e UDP a confronto

I due protocolli di trasporto principali offrono garanzie diverse, e la scelta tra loro dipende dalle esigenze dell'applicazione. Comprendere questa differenza è essenziale per progettare correttamente la comunicazione.

CaratteristicaTCPUDP
ConnessioneOrientato alla connessioneSenza connessione
AffidabilitaConsegna garantita e ordinataNessuna garanzia
VelocitaMaggiore latenza, piu robustoPiu veloce, piu leggero
Uso tipicoWeb, email, trasferimento fileStreaming, giochi, DNS

In sintesi, TCP garantisce che i dati arrivino integri e nell'ordine corretto, al prezzo di un maggiore sovraccarico; UDP privilegia la velocità e la leggerezza, accettando la possibilità di perdite. Approfondiremo entrambi negli articoli dedicati ai socket.

Verificare la raggiungibilità con Ping

Un'operazione di rete elementare è verificare se un host è raggiungibile. La classe Ping invia un pacchetto di controllo e misura il tempo di risposta, replicando la funzione dell'omonimo strumento da riga di comando.

using System.Net.NetworkInformation;

using var ping = new Ping();
PingReply reply = await ping.SendPingAsync("example.com");

if (reply.Status == IPStatus.Success)
{
    Console.WriteLine($"Host raggiungibile in {reply.RoundtripTime} ms");
}
else
{
    Console.WriteLine($"Host non raggiungibile: {reply.Status}");
}

Costruire e analizzare gli URI

Nella comunicazione applicativa gli indirizzi assumono la forma di URI, che identificano risorse specificando schema, host, porta e percorso. La classe Uri costruisce e analizza questi indirizzi in modo affidabile, evitando gli errori tipici della manipolazione manuale delle stringhe.

var uri = new Uri("https://api.example.com:443/products?page=2");

Console.WriteLine(uri.Scheme);   // "https"
Console.WriteLine(uri.Host);     // "api.example.com"
Console.WriteLine(uri.Port);     // 443
Console.WriteLine(uri.AbsolutePath); // "/products"
Console.WriteLine(uri.Query);    // "?page=2"

La natura asincrona del networking

Un principio attraversa tutta questa sezione: le operazioni di rete sono intrinsecamente lente e imprevedibili, poiché dipendono da fattori esterni al programma. Per questo motivo l'intera infrastruttura di rete di .NET è pensata per essere usata in modo asincrono, con i metodi che terminano in Async e si attendono con await. Questo evita di bloccare i thread durante l'attesa, come abbiamo visto nella serie intermedia, ed è la modalità corretta in ogni applicazione di rete.

Conclusione

Abbiamo posto le fondamenta del networking in .NET: il modello a strati e i namespace coinvolti, la rappresentazione di indirizzi e porte con IPAddress e IPEndPoint, la risoluzione dei nomi tramite Dns, la distinzione tra TCP e UDP, la verifica della raggiungibilità con Ping e la costruzione di URI. Abbiamo ribadito la natura asincrona di ogni operazione di rete. Nel prossimo articolo saliamo al livello applicativo con HttpClient, lo strumento per consumare servizi web e API REST.