Introducción a TCP

Importante

La clase Socket es muy recomendable para usuarios avanzados, en lugar de TcpClient y TcpListener.

Para trabajar con el Protocolo de control de transmisión (TCP), tiene dos opciones: usar Socket para un control y rendimiento máximos, o bien usar las clases auxiliares TcpClient y TcpListener. TcpClient y TcpListener se basan en la clase System.Net.Sockets.Socket y se encargan de los detalles de la transferencia de datos para facilitar el uso.

Las clases del protocolo usan la clase Socket subyacente para proporcionar un acceso sencillo a los servicios de red sin la sobrecarga de mantener la información de estado o de conocer los detalles de la configuración de los sockets específicos del protocolo. Para usar métodos Socket asincrónicos, puede usar los métodos asincrónicos proporcionados por la clase NetworkStream. Para obtener acceso a las características de la clase Socket no expuestas por las clases de protocolo, debe usar la clase Socket.

TcpClient y TcpListener representan la red mediante la clase NetworkStream. Use el método GetStream para devolver la secuencia de red y, luego, llame a los métodos NetworkStream.ReadAsync y NetworkStream.WriteAsync de la secuencia. La clase NetworkStream no posee el socket subyacente de las clases de protocolo, por lo que si la cierra no afectará al socket.

Uso de TcpClient y TcpListener

La clase TcpClient solicita datos de un recurso de Internet mediante TCP. Los métodos y propiedades de TcpClient abstraen los detalles para crear un Socket a fin de solicitar y recibir datos mediante TCP. Dado que la conexión al dispositivo remoto se representa como una secuencia, los datos se pueden leer y escribir empleando técnicas de control de secuencias de .NET Framework.

El protocolo TCP establece una conexión con un punto de conexión remoto y luego usa esa conexión para enviar y recibir paquetes de datos. El protocolo TCP es responsable de garantizar que los paquetes de datos se envíen al punto de conexión y que se monten en el orden correcto cuando lleguen.

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 una clase TcpClient

La clase TcpClient proporciona servicios TCP en un nivel superior de abstracción que la clase Socket. TcpClient se usa para crear una conexión de cliente a un host remoto. Sabiendo cómo obtener un elemento IPEndPoint, supongamos que tiene un elemento IPAddress para emparejar con el número de puerto deseado. En el ejemplo siguiente se muestra cómo configurar un objeto TcpClient para conectarse a un servidor horario en el puerto 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 🕛"

El código de C# anterior:

  • Crea un objeto IPEndPoint a partir de un puerto y un objeto IPAddress conocidos.
  • Crea una instancia de un nuevo objeto TcpClient.
  • Conecta el objeto client al servidor de hora de TCP remoto en el puerto 13 mediante TcpClient.ConnectAsync.
  • Usa una instancia de NetworkStream para leer datos del host remoto.
  • Declara un búfer de lectura de 1_024 bytes.
  • Lee los datos del elemento stream en el búfer de lectura.
  • Escribe los resultados como una cadena en la consola.

Dado que el cliente sabe que el mensaje es pequeño, todo el mensaje se puede leer en el búfer de lectura en una operación. Con mensajes más grandes o mensajes con una longitud indeterminada, el cliente debe usar el búfer de forma más adecuada y leer en un bucle while.

Importante

Al enviar y recibir mensajes, el elemento Encoding se debe conocer con antelación tanto en el servidor como en el cliente. Por ejemplo, si el servidor se comunica con ASCIIEncoding, pero el cliente intenta usar UTF8Encoding, los mensajes tendrán un formato incorrecto.

Creación de una clase TcpListener

El tipo TcpListener se usa para supervisar un puerto TCP en busca de solicitudes entrantes y luego crear un objeto Socket o un objeto TcpClient que administre la conexión con el cliente. El método Start habilita las escuchas, mientras que el método Stop deshabilita las escuchas en el puerto. El método AcceptTcpClientAsync acepta las solicitudes de conexión entrantes y crea un TcpClient para gestionar la solicitud, mientras que el método AcceptSocketAsync acepta las solicitudes de conexión entrantes y crea un Socket para gestionar la solicitud.

En el ejemplo siguiente se muestra cómo crear un servidor horario de red mediante un TcpListener para supervisar el puerto TCP 13. Cuando se acepta una solicitud de conexión entrante, el servidor horario responde con la fecha y hora actuales del 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();
}

El código de C# anterior:

  • Crea un objeto IPEndPoint con un objeto IPAddress.Any y un puerto.
  • Crea una instancia de un nuevo objeto TcpListener.
  • Llama al método Start para empezar a escuchar en el puerto.
  • Usa un objeto TcpClient del método AcceptTcpClientAsync para aceptar solicitudes de conexión entrantes.
  • Codifica la fecha y hora actuales como un mensaje de cadena.
  • Usa NetworkStream para escribir datos en el cliente conectado.
  • Escribe el mensaje enviado en la consola.
  • Por último, llama al método Stop para dejar de escuchar en el puerto.

Control TCP finito con la clase Socket

Tanto TcpClient como TcpListener se basan internamente en la clase Socket, lo que significa que todo lo que se puede hacer con estas clases se puede lograr mediante sockets directamente. En esta sección, se muestran varios casos de uso de TcpClient y TcpListener, junto con su homólogo Socket, que es equivalente funcionalmente.

Creación de un socket de cliente

El constructor predeterminado de TcpClient intenta crear un socket de doble pila mediante el constructor Socket(SocketType, ProtocolType). Este constructor crea un socket de doble pila si se admite IPv6; de lo contrario, revierte a IPv4.

Tenga en cuenta el siguiente código de cliente TCP:

using var client = new TcpClient();

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de socket:

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

Constructor TcpClient(AddressFamily)

Este constructor solo acepta tres valores AddressFamily; de lo contrario, producirá una excepción ArgumentException. Los valores válidos son:

Tenga en cuenta el siguiente código de cliente TCP:

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de socket:

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

Constructor TcpClient(IPEndPoint)

Al crear el socket, este constructor también se enlazará al elemento IPEndPoint local proporcionado. La propiedad IPEndPoint.AddressFamily se usa para determinar la familia de direcciones del socket.

Tenga en cuenta el siguiente código de cliente TCP:

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de 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);

Constructor TcpClient(String, Int32)

Este constructor intentará crear una pila doble de forma similar al constructor predeterminado y conectarla al punto de conexión DNS remoto definido por el par hostname y port.

Tenga en cuenta el siguiente código de cliente TCP:

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de socket:

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

Conectarse al servidor

Todas las sobrecargas de Connect, ConnectAsync, BeginConnect y EndConnect de TcpClient son funcionalmente equivalentes a los métodos de Socket correspondientes.

Tenga en cuenta el siguiente código de cliente TCP:

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

El código de TcpClient anterior es equivalente al código de socket siguiente:

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

Creación de un socket de servidor

Al igual que las instancias de TcpClient tienen equivalencia funcional con sus homólogas de Socket sin procesar, esta sección asigna constructores TcpListener a su código de socket correspondiente. El primer constructor que se debe tener en cuenta es TcpListener(IPAddress localaddr, int port).

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de socket:

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

Inicio de la escucha en el servidor

El método Start() es un contenedor que combina la funcionalidad de Bind y Listen() del objeto Socket.

Tenga en cuenta el siguiente código de cliente de escucha TCP:

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de 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();
}

Aceptar una conexión de servidor

En segundo plano, las conexiones TCP entrantes siempre crean un nuevo socket cuando se aceptan. TcpListener puede aceptar una instancia de Socket directamente (mediante AcceptSocket() o AcceptSocketAsync()) o puede aceptar un objeto TcpClient (mediante AcceptTcpClient() y AcceptTcpClientAsync()).

Tenga en cuenta el siguiente código de TcpListener:

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

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

El código de cliente TCP anterior es funcionalmente equivalente al siguiente código de 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();

Creación de un objeto NetworkStream para enviar y recibir datos

Con TcpClient, debe crear una instancia de NetworkStream con el método GetStream() para poder enviar y recibir datos. Con Socket, debe realizar la creación de NetworkStream manualmente.

Tenga en cuenta el siguiente código de TcpClient:

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

Que es equivalente al código de socket siguiente:

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

Sugerencia

Si el código no tiene que trabajar con una instancia de Stream, puede confiar en los métodos de envío y recepción de Socket (Send, SendAsync, Receive y ReceiveAsync) directamente en lugar de crear un objeto NetworkStream.

Consulte también