Usar soquetes para enviar e receber dados por TCP

Antes de poder usar um soquete para se comunicar com dispositivos remotos, o soquete deve ser inicializado com as informações de protocolo e endereço de rede. O construtor da classe Socket tem parâmetros que especificam a família de endereços, o tipo de soquete e o tipo de protocolo usado pelo soquete para fazer conexões. Ao conectar um soquete de cliente a um soquete de servidor, o cliente usará um objeto IPEndPoint para especificar o endereço de rede do servidor.

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 cliente Socket

Com o objeto endPoint criado, crie um soquete de cliente para se conectar ao servidor. Depois que o soquete estiver conectado, ele poderá enviar e receber dados da conexão de soquete do servidor.

using Socket client = new(
    ipEndPoint.AddressFamily, 
    SocketType.Stream, 
    ProtocolType.Tcp);

await client.ConnectAsync(ipEndPoint);
while (true)
{
    // Send message.
    var message = "Hi friends 👋!<|EOM|>";
    var messageBytes = Encoding.UTF8.GetBytes(message);
    _ = await client.SendAsync(messageBytes, SocketFlags.None);
    Console.WriteLine($"Socket client sent message: \"{message}\"");

    // Receive ack.
    var buffer = new byte[1_024];
    var received = await client.ReceiveAsync(buffer, SocketFlags.None);
    var response = Encoding.UTF8.GetString(buffer, 0, received);
    if (response == "<|ACK|>")
    {
        Console.WriteLine(
            $"Socket client received acknowledgment: \"{response}\"");
        break;
    }
    // Sample output:
    //     Socket client sent message: "Hi friends 👋!<|EOM|>"
    //     Socket client received acknowledgment: "<|ACK|>"
}

client.Shutdown(SocketShutdown.Both);

O código anterior do C#:

  • Cria uma instância de um novo objeto Socket com uma determinada família de endereços de instâncias endPoint, o SocketType.Stream e o ProtocolType.Tcp.

  • Chama o método Socket.ConnectAsync com a instância endPoint como um argumento.

  • Em um loop while:

    • Codifica e envia uma mensagem para o servidor usando Socket.SendAsync.
    • Grava a mensagem enviada no console.
    • Inicializa um buffer para receber dados do servidor usando Socket.ReceiveAsync.
    • Quando o response é uma confirmação, ele é gravado no console e o loop é encerrado.
  • Por fim, o soquete client chama Socket.Shutdown dado SocketShutdown.Both, que desliga as operações de envio e recebimento.

Criar um servidor Socket

Para criar o soquete do servidor, o objeto endPoint pode escutar conexões de entrada em qualquer endereço IP, mas o número da porta deve ser especificado. Depois que o soquete é criado, o servidor pode aceitar conexões de entrada e se comunicar com clientes.

using Socket listener = new(
    ipEndPoint.AddressFamily,
    SocketType.Stream,
    ProtocolType.Tcp);

listener.Bind(ipEndPoint);
listener.Listen(100);

var handler = await listener.AcceptAsync();
while (true)
{
    // Receive message.
    var buffer = new byte[1_024];
    var received = await handler.ReceiveAsync(buffer, SocketFlags.None);
    var response = Encoding.UTF8.GetString(buffer, 0, received);
    
    var eom = "<|EOM|>";
    if (response.IndexOf(eom) > -1 /* is end of message */)
    {
        Console.WriteLine(
            $"Socket server received message: \"{response.Replace(eom, "")}\"");

        var ackMessage = "<|ACK|>";
        var echoBytes = Encoding.UTF8.GetBytes(ackMessage);
        await handler.SendAsync(echoBytes, 0);
        Console.WriteLine(
            $"Socket server sent acknowledgment: \"{ackMessage}\"");

        break;
    }
    // Sample output:
    //    Socket server received message: "Hi friends 👋!"
    //    Socket server sent acknowledgment: "<|ACK|>"
}

O código anterior do C#:

  • Cria uma instância de um novo objeto Socket com uma determinada família de endereços de instâncias endPoint, o SocketType.Stream e o ProtocolType.Tcp.

  • O listener chama o método Socket.Bind com a instância endPoint como um argumento para associar o soquete ao endereço de rede.

  • O método Socket.Listen() é chamado para escutar conexões de entrada.

  • O listener chama o método Socket.AcceptAsync para aceitar uma conexão de entrada no soquete handler.

  • Em um loop while:

    • Chama Socket.ReceiveAsync para receber dados do cliente.
    • Quando os dados são recebidos, eles são decodificados e gravados no console.
    • Se a mensagem response terminar com <|EOM|>, uma confirmação será enviada ao cliente usando o Socket.SendAsync.

Executar o cliente e o servidor de exemplo

Inicie o aplicativo de servidor primeiro e, em seguida, o aplicativo cliente.

dotnet run --project socket-server
Socket server starting...
Found: 172.23.64.1 available on port 9000.
Socket server received message: "Hi friends 👋!"
Socket server sent acknowledgment: "<|ACK|>"
Press ENTER to continue...

O aplicativo cliente enviará uma mensagem ao servidor e o servidor responderá com uma confirmação.

dotnet run --project socket-client
Socket client starting...
Found: 172.23.64.1 available on port 9000.
Socket client sent message: "Hi friends 👋!<|EOM|>"
Socket client received acknowledgment: "<|ACK|>"
Press ENTER to continue...

Confira também