Uso de sockets para enviar y recibir datos a través de TCP

Para poder usar un socket a fin de comunicarse con dispositivos remotos, es necesario inicializarlo con la información del protocolo y de la dirección de red. El constructor para la clase Socket tiene parámetros que especifican la familia de direcciones, el tipo de socket y el tipo de protocolo que el socket usa para establecer conexiones. Al conectar un socket de cliente a un socket de servidor, el cliente usará un objeto IPEndPoint para especificar la dirección de red del servidor.

Creación de un punto de conexión IP

Cuando se usa System.Net.Sockets, un punto de conexión de red se representa como un objeto IPEndPoint. IPEndPoint se crea con un objeto IPAddress y con su correspondiente número de puerto. Antes de poder iniciar una conversación a través de un objeto Socket, hay que crear una canalización de datos entre la aplicación y el destino remoto.

TCP/IP usa una dirección de red y un número de puerto de servicio para identificar un servicio de forma única. La dirección de red identifica un destino de red específico y el número de puerto, el servicio específico en ese destino al que conectarse. La combinación de puerto de servicio y dirección de red se denomina "punto de conexión", que en .NET se representa mediante la clase EndPoint. Para cada familia de direcciones compatible se define un descendiente de EndPoint; para la familia de direcciones IP, la clase es IPEndPoint.

La clase Dns proporciona servicios de nombre de dominio a las aplicaciones que usan los servicios de Internet TCP/IP. El método GetHostEntryAsync consulta un servidor DNS para asignar un nombre de dominio descriptivo (por ejemplo, "host.contoso.com") a una dirección de Internet numérica (por ejemplo, 192.168.1.1). GetHostEntryAsync devuelve un objeto Task<IPHostEntry> que, cuando se le espera, contiene una lista de direcciones y alias del nombre solicitado. En la mayoría de los casos, puede usar la primera dirección devuelta en la matriz AddressList. El siguiente código obtiene un objeto IPAddress, que contiene la dirección IP del servidor host.contoso.com.

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

Sugerencia

Con fines de prueba y depuración manuales, normalmente puede usar el método GetHostEntryAsync con el nombre de host resultante del valor de Dns.GetHostName() para resolver el nombre de localhost en una dirección IP. Tenga en cuenta el fragmento de código siguiente:

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

Internet Assigned Numbers Authority (IANA) define los números de puerto de los servicios comunes. Para obtener más información, vea IANA: Registro de nombres de servicio y números de puerto de protocolo de transporte. Hay otros servicios que pueden tener registrados números de puerto en el intervalo comprendido entre 1024 y 65 535. En el siguiente código se combina la dirección IP de host.contoso.com con un número de puerto para crear un punto de conexión remoto de una conexión.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

Una vez determinada la dirección del dispositivo remoto y elegido un puerto que se usará para la conexión, la aplicación puede establecer una conexión con el dispositivo remoto.

Creación de un cliente Socket

Con el objeto endPoint creado, cree un socket de cliente para conectarse al servidor. Una vez conectado el socket, este puede enviar y recibir datos desde la conexión de socket de 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);

El código de C# anterior:

  • Crea una instancia de un nuevo objeto Socket con una determinada familia de direcciones de instancias de endPoint, SocketType.Stream y ProtocolType.Tcp.

  • Llama al método Socket.ConnectAsync con la instancia de endPoint como argumento.

  • En un bucle while:

    • Se codifica y envía un mensaje al servidor mediante Socket.SendAsync.
    • El mensaje enviado se escribe en la consola.
    • Se inicializa un búfer para recibir datos del servidor mediante Socket.ReceiveAsync.
    • Cuando response es una confirmación, se escribe en la consola y se sale del bucle.
  • Por último, el socket client llama a Socket.Shutdown según SocketShutdown.Both, lo que finaliza las operaciones tanto de envío como de recepción.

Creación de un servidor Socket

Para crear el socket de servidor, el objeto endPoint puede escuchar las conexiones entrantes en cualquier dirección IP, pero se debe especificar el número de puerto. Una vez creado el socket, el servidor puede aceptar conexiones entrantes y comunicarse con los 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|>"
}

El código de C# anterior:

  • Crea una instancia de un nuevo objeto Socket con una determinada familia de direcciones de instancias de endPoint, SocketType.Stream y ProtocolType.Tcp.

  • listener llama al método Socket.Bind con la instancia de endPoint como argumento para asociar el socket a la dirección de red.

  • Se llama al método Socket.Listen() para escuchar las conexiones entrantes.

  • listener llama al método Socket.AcceptAsync para aceptar una conexión entrante en el socket handler.

  • En un bucle while:

    • Llama a Socket.ReceiveAsync para recibir datos del cliente.
    • Cuando se reciben los datos, se descodifican y se escriben en la consola.
    • Si el mensaje response acaba con <|EOM|>, se envía una confirmación al cliente mediante Socket.SendAsync.

Ejecución del servidor y del cliente de ejemplo

Inicie primero la aplicación de servidor y, luego, inicie la aplicación 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...

La aplicación cliente enviará un mensaje al servidor y este responderá con una confirmación.

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...

Vea también