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 umIPAddress
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:
- AddressFamily.InterNetwork: para soquete IPv4.
- AddressFamily.InterNetworkV6: para soquete IPv6.
- AddressFamily.Unknown: isso tentará criar um soquete de pilha dupla, da mesma forma que o construtor padrã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 local IPEndPoint
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.