Panoramica sul TCP

Importante

La classe Socket è altamente raccomandata per gli utenti avanzati, al posto di TcpClient e TcpListener.

Per lavorare con il protocollo TCP (Transmission Control Protocol), sono disponibili due opzioni: usare Socket per il massimo controllo e prestazioni, oppure utilizzare le classi helper TcpClient e TcpListener. TcpClient e TcpListener sono basati sulla classe System.Net.Sockets.Socket e si occupano dei dettagli del trasferimento dei dati per semplificare l'uso.

Le classi di protocollo utilizzano la classe Socket sottostante per fornire un accesso semplice ai servizi di rete senza il sovraccarico di gestione delle informazioni sullo stato o conoscere i dettagli della configurazione di socket specifici del protocollo. Se si desidera usare i metodi Socket asincroni, è possibile usare quelli forniti dalla classe NetworkStream. Per accedere alle funzionalità della classe Socket non esposte dalle classi di protocollo, è necessario utilizzare la classe Socket.

TcpClient e TcpListener rappresentano la rete utilizzando la classe NetworkStream. Si usa il metodo GetStream per restituire il flusso di rete e quindi si chiamano i metodi NetworkStream.ReadAsync e NetworkStream.WriteAsync del flusso. NetworkStream non possiede il socket sottostante delle classi di protocollo, quindi la sua chiusura non influisce sul socket.

Usare TcpClient ed TcpListener

La classe TcpClient richiede dati da una risorsa Internet tramite TCP. I metodi e le proprietà di TcpClient astraggono i dettagli per la creazione di un Socket per la richiesta e la ricezione di dati tramite TCP. Dato che la connessione al dispositivo remoto è rappresentata come flusso, i dati possono essere letti e scritti con le tecniche di gestione dei flussi di .NET Framework.

Il protocollo TCP stabilisce una connessione con un endpoint remoto e quindi usa tale connessione per inviare e ricevere pacchetti di dati. TCP è responsabile di garantire che i pacchetti di dati vengano inviati all'endpoint e assemblati nell'ordine corretto all'arrivo.

Creare un endpoint IP

Quando si usa System.Net.Sockets, si rappresenta un endpoint di rete come oggetto IPEndPoint. IPEndPoint viene costruito con un oggetto IPAddress e il numero di porta corrispondente. Prima di poter avviare una conversazione tramite un Socket, creare una pipe di dati tra l'app e la destinazione remota.

TCP/IP usa un indirizzo di rete e un numero di porta del servizio per identificare in modo univoco un servizio. L'indirizzo di rete identifica una destinazione di rete specifica. Il numero di porta identifica il servizio specifico in tale dispositivo a cui connettersi. La combinazione di indirizzo di rete e porta del servizio viene denominata endpoint, rappresentato in .NET dalla classe EndPoint. Viene definito un discendente di EndPoint per ogni famiglia di indirizzi supportata. Per la famiglia di indirizzi IP, la classe è IPEndPoint.

La classe Dns fornisce servizi DNS alle app che usano i servizi TCP/IP Internet. Il metodo GetHostEntryAsync richiede a un server DNS di eseguire il mapping di un nome descrittivo di dominio (ad esempio "host.contoso.com") a un indirizzo Internet numerico (ad esempio 192.168.1.1). GetHostEntryAsync restituisce una voce Task<IPHostEntry> che, quando è attesa, contiene un elenco di indirizzi e alias per il nome richiesto. Nella maggior parte dei casi, è possibile usare il primo indirizzo restituito nella matrice AddressList. Il codice seguente ottiene un IPAddress contenente l'indirizzo IP del server host.contoso.com.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];

Suggerimento

Per scopi di test e debug manuali, in genere è possibile usare il metodo GetHostEntryAsync con il nome host risultante derivante dal valore Dns.GetHostName() per risolvere il nome localhost in un indirizzo IP. Si consideri il frammento di codice seguente:

var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];

IANA (Internet Assigned Numbers Authority) definisce i numeri di porta per i servizi comuni. Per altre informazioni, vedere IANA: Nome del servizio e registro dei numeri di porta del protocollo di trasporto. Altri servizi possono avere numeri di porta registrati nell'intervallo da 1024 a 65.535. Il codice seguente combina l'indirizzo IP per host.contoso.com con un numero di porta per creare un endpoint remoto per una connessione.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

Dopo aver determinato l'indirizzo del dispositivo remoto e aver scelto la porta da usare per la connessione, l'applicazione può stabilire una connessione con il dispositivo remoto.

Creare un TcpClient

La TcpClient classe fornisce servizi TCP a un livello di astrazione più elevato rispetto alla classe Socket. TcpClient viene usato per creare una connessione client a un host remoto. Sapendo come ottenere un IPEndPoint, si supponga di avere un IPAddress da associare al numero di porta desiderato. L'esempio seguente illustra la configurazione di un TcpClient per connettersi a un server orario sulla porta TCP 13:

var ipEndPoint = new IPEndPoint(ipAddress, 13);

using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();

var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);

var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
//     Message received: "📅 8/22/2022 9:07:17 AM 🕛"

Il codice C# precedente:

  • Crea un IPEndPoint da un IPAddress e una porta noti.
  • Crea un'istanza di un nuovo oggetto TcpClient.
  • Connette il client al server orario TCP remoto sulla porta 13 usando TcpClient.ConnectAsync.
  • Utilizza un NetworkStream per leggere i dati dall'host remoto.
  • Dichiara un buffer di lettura di 1_024 byte.
  • Legge i dati dall'oggetto stream nel buffer di lettura.
  • Scrive i risultati come stringa nella console.

Poiché il client sa che il messaggio è di dimensioni ridotte, l'intero messaggio può essere letto nel buffer di lettura in un'unica operazione. Con messaggi più grandi o di una lunghezza indeterminata, il client deve usare il buffer in modo più appropriato e leggere in un ciclo while.

Importante

Durante l'invio e la ricezione di messaggi, Encoding deve essere noto in anticipo sia al server che al client. Ad esempio, se il server comunica utilizzando ASCIIEncoding ma il client tenta di utilizzare UTF8Encoding, i messaggi saranno in formato non valido.

Creare un TcpListener

Il tipo TcpListener viene utilizzato per monitorare una porta TCP per le richieste in entrata e quindi creare un Socket o un TcpClient che gestisce la connessione al client. Il metodo Start abilita l'ascolto sulla porta e il metodo Stop lo disabilita. Il metodo AcceptTcpClientAsync accetta le richieste di connessione in entrata e crea un TcpClient per gestire la richiesta, mentre il metodo AcceptSocketAsync accetta le richieste di connessione in entrata e crea un Socket per gestire la richiesta.

L'esempio seguente illustra la creazione di un server orario di rete utilizzando un TcpListener per monitorare la porta TCP 13. Quando viene accettata una richiesta di connessione in ingresso, il server di riferimento ora risponde con la data e ora correnti dal server host.

var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);

try
{    
    listener.Start();

    using TcpClient handler = await listener.AcceptTcpClientAsync();
    await using NetworkStream stream = handler.GetStream();

    var message = $"📅 {DateTime.Now} 🕛";
    var dateTimeBytes = Encoding.UTF8.GetBytes(message);
    await stream.WriteAsync(dateTimeBytes);

    Console.WriteLine($"Sent message: \"{message}\"");
    // Sample output:
    //     Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
    listener.Stop();
}

Il codice C# precedente:

  • Crea un IPEndPoint con IPAddress.Any e la porta.
  • Crea un'istanza di un nuovo oggetto TcpListener.
  • Chiama il metodo Start per avviare l'ascolto sulla porta.
  • Utilizza un TcpClient del metodo AcceptTcpClientAsync per accettare le richieste di connessione in entrata.
  • Codifica la data e l'ora correnti come messaggio di stringa.
  • Utilizza un NetworkStream per scrivere dati al client connesso.
  • Scrive il messaggio inviato nella console.
  • Infine, chiama il metodo Stop per interrompere l'ascolto sulla porta.

Controllo TCP finito con la classe Socket

Sia TcpClient che TcpListener si basano internamente sulla classe Socket, il che significa che tutto ciò che si può fare con queste classi può essere ottenuto utilizzando direttamente i socket. Questa sezione illustra diversi casi d'uso TcpClient e TcpListener, insieme alla loro controparte Socket che è funzionalmente equivalente.

Creare un socket del client

Il costruttore predefinito di TcpClient tenta di creare un socket dual-stack tramite il costruttore Socket (SocketType, ProtocolType). Questo costruttore crea un socket dual-stack se IPv6 è supportato altrimenti ricorre a IPv4.

Si consideri il seguente codice client TCP:

using var client = new TcpClient();

Il precedente codice client TCP è funzionalmente equivalente al seguente codice socket:

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

Il costruttore TcpClient(AddressFamily)

Questo costruttore accetta solo tre valori AddressFamily, altrimenti genererà un ArgumentException. I valori validi sono:

Si consideri il seguente codice client TCP:

using var client = new TcpClient(AddressFamily.InterNetwork);

Il precedente codice client TCP è funzionalmente equivalente al seguente codice socket:

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

Il costruttore TcpClient(IPEndPoint)

Al momento della creazione del socket, questo costruttore verrà associato anche al localeIPEndPoint fornito. La proprietà IPEndPoint.AddressFamily viene utilizzata per determinare la famiglia di indirizzi del socket.

Si consideri il seguente codice client TCP:

var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);

Il precedente codice client TCP è funzionalmente equivalente al seguente codice socket:

// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);

Il costruttore TcpClient(String, Int32)

Questo costruttore tenterà di creare un doppio stack simile al costruttore predefinito e di connetterlo all'endpoint DNS remoto definito dalla coppia hostname e port.

Si consideri il seguente codice client TCP:

using var client = new TcpClient("www.example.com", 80);

Il precedente codice client TCP è funzionalmente equivalente al seguente codice socket:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Connettiti al server

Tutti gli overload Connect, ConnectAsync, BeginConnect e EndConnect in TcpClient sono funzionalmente equivalenti ai metodi Socket corrispondenti.

Si consideri il seguente codice client TCP:

using var client = new TcpClient();
client.Connect("www.example.com", 80);

Il codice TcpClient sopra è equivalente al seguente codice socket:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Creare un socket del server

Proprio come le istanze TcpClient che hanno equivalenza funzionale con le loro controparti Socket grezze, questa sezione associa i costruttori TcpListener al loro codice socket corrispondente. Il primo costruttore da considerare è TcpListener(IPAddress localaddr, int port).

var listener = new TcpListener(IPAddress.Loopback, 5000);

Il precedente codice listener TCP è funzionalmente equivalente al seguente codice socket:

var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Avviare l'ascolto sul server

Il metodo Start() è un wrapper che combina le funzionalità Bind e Listen() di Socket.

Si consideri il seguente codice listener TCP:

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);

Il precedente codice listener TCP è funzionalmente equivalente al seguente codice socket:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
    socket.Listen(10);
}
catch (SocketException)
{
    socket.Dispose();
}

Accettare una connessione al server

Dietro le quinte, le connessioni TCP in entrata creano sempre un nuovo socket quando vengono accettate. TcpListener può accettare un'istanza Socket direttamente (tramite AcceptSocket() o AcceptSocketAsync()) oppure può accettare una TcpClient (tramite AcceptTcpClient() e AcceptTcpClientAsync()).

Si consideri il seguente codice TcpListener:

var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();

// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();

Il precedente codice listener TCP è funzionalmente equivalente al seguente codice socket:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();

// Synchronous alternative
// var acceptedSocket = socket.Accept();

Creare un NetworkStream per inviare e ricevere dati

Con TcpClient è necessario creare un'istanza di NetworkStream con il metodo GetStream() per poter inviare e ricevere dati. Con Socket, è necessario creare NetworkStream manualmente.

Si consideri il seguente codice TcpClient:

using var client = new TcpClient();
using NetworkStream stream = client.GetStream();

Il codice sopra è equivalente al seguente codice socket:

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

// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);

Suggerimento

Se non è necessario che il codice funzioni con un'istanza Stream, ci si può affidare direttamente ai metodi invia/ricevi del Socket (Send, SendAsync, Receive e ReceiveAsync) anziché creare un NetworkStream.

Vedi anche