Visão geral do TCP

Importante

A classe Socket é altamente recomendada para usuários avançados, em vez de TcpClient e TcpListener.

Para trabalhar com TCP (Protocolo de Controle de Transmissão), você tem duas opções: usar Socket para o máximo controle e desempenho ou usar as classes auxiliares TcpClient e TcpListener. TcpClient e TcpListener são compilados com base na classe System.Net.Sockets.Socket e cuidam dos detalhes da transferência de dados para facilitar o uso.

As classes de protocolo usam a classe Socket subjacente para fornecer acesso simples aos serviços de rede, sem a sobrecarga de manter informações de estado ou de conhecer os detalhes da configuração de soquetes específicos ao protocolo. Para usar métodos Socket assíncronos, use os métodos assíncronos fornecidos pela classe NetworkStream. Para acessar os recursos da classe Socket não expostos pelas classes de protocolo, use a classe Socket.

TcpClient e TcpListener representam a rede que usa a classe NetworkStream. Use o método GetStream para retornar o fluxo de rede e, em seguida, chamar os métodos NetworkStream.ReadAsync e NetworkStream.WriteAsync do fluxo. O NetworkStream não possui o soquete subjacente das classes de protocolo e, portanto, seu fechamento não afeta o soquete.

Usar TcpClient e TcpListener

A classe TcpClient solicita dados de um recurso da Internet usando o TCP. Os métodos e as propriedades de TcpClient abstraem os detalhes para criar um Socket para solicitar e receber dados usando o TCP. Como a conexão com o dispositivo remoto é representada como um fluxo, os dados podem ser lidos e gravados com técnicas de manipulação de fluxo do .NET Framework.

O protocolo TCP estabelece uma conexão com um ponto de extremidade remoto e, em seguida, usa essa conexão para enviar e receber pacotes de dados. O TCP é responsável por garantir que os pacotes de dados são enviados para o ponto de extremidade e montados na ordem correta quando chegarem.

Criar um ponto de extremidade IP

Ao trabalhar com System.Net.Sockets, você representa um ponto de extremidade de rede como um objeto IPEndPoint. O IPEndPoint é construído com um IPAddress e seu número de porta correspondente. Antes de iniciar uma conversa por meio de um Socket, crie um pipe de dados entre o aplicativo e o destino remoto.

O TCP/IP usa um endereço de rede e um número da porta de serviço para identificar um serviço exclusivamente. O endereço de rede identifica um destino de rede específico. O número da porta identifica o serviço específico nesse dispositivo ao qual se conectar. A combinação do endereço de rede e da porta de serviço é chamada de ponto de extremidade, que é representado no .NET pela classe EndPoint. Um descendente de EndPoint está definido para cada família de endereços com suporte; para a família de endereços IP, a classe é IPEndPoint.

A classe Dns fornece serviços de nome de domínio para aplicativos que usam os serviços de Internet TCP/IP. O método GetHostEntryAsync consulta um servidor DNS para mapear um nome de domínio amigável (como “host.contoso.com”) para um endereço numérico na Internet (por exemplo, 192.168.1.1). O GetHostEntryAsync retorna um Task<IPHostEntry> que é aguardando e contém uma lista de endereços e aliases para o nome solicitado. Na maioria dos casos, é possível usar o primeiro endereço retornado na matriz AddressList. O código a seguir obtém um IPAddress que contém o endereço IP do servidor host.contoso.com.

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

Dica

Para fins de teste manual e depuração, normalmente você pode usar o método GetHostEntryAsync com o nome do host resultante do valor Dns.GetHostName() para resolver o nome do host local para um endereço IP. Considere o seguinte snippet de código:

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

A IANA (Internet Assigned Numbers Authority) define os números de porta de serviços comuns. Para obter mais informações, confira IANA: registro de número da porta do protocolo de transporte e nome do serviço). Outros serviços podem ter números de porta registrados no intervalo de 1.024 a 65.535. O código a seguir combina o endereço IP de host.contoso.com com um número da porta para criar um ponto de extremidade remoto para uma conexão.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

Depois de determinar o endereço do dispositivo remoto e escolher uma porta a ser usada para a conexão, o aplicativo pode estabelecer uma conexão com o dispositivo remoto.

Criar um TcpClient

A classe TcpClient fornece serviços TCP em um nível mais alto de abstração do que a classe Socket. O TcpClient é usado para criar uma conexão de cliente com um host remoto. Sabendo como obter um IPEndPoint, vamos supor que você tenha um IPAddress para emparelhar com o número de porta desejado. O exemplo a seguir demonstra a configuração de um TcpClient para se conectar a um servidor de horário na 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 🕛"

O código anterior do C#:

  • Cria um IPEndPoint de um IPAddress conhecido e uma porta.
  • Inspecione um novo objeto TcpClient.
  • Conecta o client ao servidor de tempo TCP remoto na porta 13 usando TcpClient.ConnectAsync.
  • Usa um NetworkStream para ler dados do host remoto.
  • Declara um buffer de leitura de 1_024 bytes.
  • Lê dados do stream no buffer de leitura.
  • Grava os resultados como uma cadeia de caracteres no console.

Como o cliente sabe que a mensagem é pequena, toda a mensagem pode ser lida no buffer de leitura em uma operação. Com mensagens maiores ou mensagens com um comprimento indeterminado, o cliente deve usar o buffer de forma mais adequada e ler em um loop while.

Importante

Ao enviar e receber mensagens, o Encoding deverá ser conhecido antecipadamente para o servidor e o cliente. Por exemplo, se o servidor se comunicar usando ASCIIEncoding, mas o cliente tentar usar UTF8Encoding, as mensagens serão malformadas.

Criar um TcpListener

O tipo TcpListener é usado para monitorar uma porta TCP para solicitações de entrada e criar um Socket ou um TcpClient que gerencia a conexão com o cliente. O método Start habilita a escuta e o método Stop desabilita a escuta na porta. O método AcceptTcpClientAsync aceita solicitações de conexão de entrada e cria um TcpClient para manipular a solicitação e o método AcceptSocketAsync aceita solicitações de conexão de entrada e cria um Socket para manipular a solicitação.

O exemplo a seguir demonstra como criar um servidor de horário da rede usando um TcpListener para monitorar a porta TCP 13. Quando uma solicitação de conexão de entrada é aceita, o servidor de horário responde com a data e hora atuais do servidor 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();
}

O código anterior do C#:

  • Cria uma IPEndPoint com IPAddress.Any e porta.
  • Crie uma instância de um novo objeto TcpListener.
  • Chama o método Start para começar a escutar na porta.
  • Usa um TcpClient do método AcceptTcpClientAsync para aceitar solicitações de conexão de entrada.
  • Codifica a data e a hora atuais como uma mensagem de cadeia de caracteres.
  • Usa um NetworkStream para gravar dados no cliente conectado.
  • Grava a mensagem enviada no console.
  • Por fim, chama o método Stop para parar de escutar na porta.

Controle TCP finito com a classe Socket

Tanto TcpClient quanto TcpListener dependem internamente da classe Socket, o que significa que qualquer coisa que você possa fazer com essas classes pode ser obtida usando soquetes diretamente. Esta seção demonstra vários casos de uso TcpClient e TcpListener, juntamente com seu Socket que é funcionalmente equivalente.

Criar um soquete do cliente

O construtor padrão TcpClient tenta criar um soquete de pilha dupla por meio do construtor Socket(SocketType, ProtocolType). Esse construtor cria um soquete de pilha dupla se houver suporte para IPv6. Caso contrário, ele retornará para IPv4.

Considere o seguinte código de cliente TCP:

using var client = new TcpClient();

O código do cliente TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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

O construtor TcpClient(AddressFamily)

Esse construtor aceita apenas três valores AddressFamily. Caso contrário, ele gerará um ArgumentException. Os valores válidos são:

Considere o seguinte código de cliente TCP:

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

O código do cliente TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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

O construtor TcpClient(IPEndPoint)

Ao criar o soquete, esse construtor também será associado ao localIPEndPoint fornecido. A propriedade IPEndPoint.AddressFamily é usada para determinar a família de endereços do soquete.

Considere o seguinte código de cliente TCP:

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

O código do cliente TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

// 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);

O construtor TcpClient(String, Int32)

Esse construtor tentará criar uma pilha dupla semelhante ao construtor padrão e conectar ao ponto de extremidade DNS remoto definido pelo par hostname e port.

Considere o seguinte código de cliente TCP:

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

O código do cliente TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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

Conectar-se ao servidor

Todas as sobrecargas Connect, ConnectAsync, BeginConnect e EndConnect no TcpClient são funcionalmente equivalentes aos métodos Socket correspondentes.

Considere o seguinte código de cliente TCP:

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

O código TcpClient acima é equivalente ao seguinte código de soquete:

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

Criar um soquete de servidor

Assim como as instâncias TcpClient que têm equivalência funcional com seus equivalentes Socket brutos, esta seção mapeia construtores TcpListener para seu código de soquete correspondente. O primeiro construtor a considerar é o TcpListener(IPAddress localaddr, int port).

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

O código do ouvinte TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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

Começar a escutar no servidor

O método Start() é um wrapper que combina as funcionalidades Bind e Listen() de Socket.

Considere o seguinte código de ouvinte TCP:

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

O código do ouvinte TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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();
}

Aceitar uma conexão de servidor

Sob o capô, as conexões TCP de entrada estão sempre criando um novo soquete quando aceitas. TcpListener pode aceitar uma instância Socket diretamente (via AcceptSocket() ou AcceptSocketAsync()) ou pode aceitar um TcpClient (via AcceptTcpClient() e AcceptTcpClientAsync()).

Considere o seguinte código TcpListener:

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

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

O código do ouvinte TCP anterior é funcionalmente equivalente ao seguinte código de soquete:

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();

Criar um NetworkStream usado para enviar e receber dados

Com TcpClient, você precisa instanciar um NetworkStream com o método GetStream() para poder enviar e receber dados. Com Socket, você precisa fazer a criação NetworkStream manualmente.

Considere o seguinte código TcpClient:

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

Que é equivalente ao seguinte código de soquete:

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);

Dica

Se o código não precisar trabalhar com uma instância Stream, você poderá contar com os métodos Enviar/Receber de Socket(Send, SendAsync, Receive e ReceiveAsync) diretamente em vez de criar um NetworkStream.

Confira também