Programación de sockets. ¿Qué es un enchufe? Funciones del socket del servidor

Me gusta mucho toda la serie de artículos y además siempre quise probarme como traductor. Quizás el artículo les parezca demasiado obvio a los desarrolladores experimentados, pero a mí me parece que será útil en cualquier caso.
Primer artículo: http://habrahabr.ru/post/209144/

Recepción y transmisión de paquetes de datos.

Introducción
Hola, mi nombre es Glenn Fiedler y les doy la bienvenida a mi segundo artículo de la serie "Programación de redes para desarrolladores de juegos".

En el artículo anterior, discutimos varias formas de transferir datos entre computadoras a través de una red y al final decidimos usar el protocolo UDP en lugar de TCP. Decidimos utilizar UDP para poder enviar datos sin demoras asociadas con la espera de que se reenvíen los paquetes.

Ahora les voy a contar cómo usar UDP para enviar y recibir paquetes en la práctica.

Zócalos BSD
La mayoría de los sistemas operativos modernos tienen algún tipo de implementación de socket basada en sockets BSD (sockets Berkeley).

Los sockets BSD operan con funciones simples como “socket”, “bind”, “sendto” y “recvfrom”. Por supuesto, puede acceder a estas funciones directamente, pero en este caso su código dependerá de la plataforma, ya que sus implementaciones pueden diferir ligeramente en diferentes sistemas operativos.

Por lo tanto, aunque daré más adelante el primer ejemplo simple de interacción con sockets BSD, a continuación no los usaremos directamente. En cambio, después de dominar la funcionalidad básica, escribiremos varias clases que abstraigan todo el trabajo con sockets, de modo que en el futuro nuestro código sea independiente de la plataforma.

Características de diferentes sistemas operativos
Primero, escribamos el código que determinará el sistema operativo actual para que podamos tener en cuenta las diferencias en el funcionamiento de los sockets:

// detección de plataforma #define PLATFORM_WINDOWS 1 #define PLATFORM_MAC 2 #define PLATFORM_UNIX 3 #si está definido(_WIN32) #define PLATFORM PLATFORM_WINDOWS #elif definido(__APPLE__) #define PLATFORM PLATFORM_MAC #else #define PLATFORM PLATFORM_UNIX #endif
Ahora incluyamos los archivos de encabezado necesarios para trabajar con sockets. Dado que el conjunto de archivos de encabezado requeridos depende del sistema operativo actual, aquí usamos el código #define escrito anteriormente para determinar qué archivos deben incluirse.

#si PLATAFORMA == PLATAFORMA_WINDOWS #incluir #elif PLATAFORMA == PLATAFORMA_MAC || PLATAFORMA == PLATAFORMA_UNIX #incluir #incluir #incluir #endif
En los sistemas UNIX, las funciones para trabajar con sockets están incluidas en las bibliotecas estándar del sistema, por lo que en este caso no necesitamos bibliotecas de terceros. Sin embargo, en Windows, para estos fines necesitamos incluir la biblioteca winsock.

Aquí tienes un pequeño truco sobre cómo puedes hacer esto sin cambiar el proyecto o el archivo MAKE:

#if PLATAFORMA == PLATAFORMA_WINDOWS #pragma comentario(lib, "wsock32.lib") #endif
Me gusta este truco porque soy vago. Por supuesto, puede incluir la biblioteca en un proyecto o en un archivo MAKE.

Inicializando sockets
En la mayoría de los sistemas operativos tipo Unix (incluido macOSx), no se requieren pasos especiales para inicializar la funcionalidad del socket, pero en Windows primero debe realizar un par de pasos: debe llamar a la función "WSAStartup" antes de usar cualquier función del socket. y después de terminar el trabajo - llame a "WSACleanup".

Agreguemos dos nuevas características:

bool en línea InitializeSockets() ( #if PLATFORM == PLATFORM_WINDOWS WSADATA WsaData; return WSAStartup(MAKEWORD(2,2), &WsaData) == NO_ERROR; #else return true; #endif ) inline void ShutdownSockets() ( #if PLATFORM == PLATAFORMA_WINDOWS WSACleanup(); #endif )
Ahora tenemos un código de inicialización y apagado de socket independiente de la plataforma. En plataformas que no requieren inicialización, este código simplemente no hace nada.

crear un enchufe
Ahora podemos crear un socket UDP. Esto se hace así:

Mango int = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);<= 0) { printf("failed to create socket\n"); return false; }
si (manejar

A continuación, debemos vincular el socket a un número de puerto específico (por ejemplo, 30000). Cada socket debe tener su propio puerto único, porque cuando llega un nuevo paquete, el número de puerto determina a qué socket se envía. No utilice números de puerto inferiores a 1024: están reservados por el sistema.

Si no le importa qué número de puerto usar para el socket, simplemente puede pasar "0" a la función y luego el sistema le asignará algún puerto desocupado.< 0) { printf("failed to bind socket\n"); return false; }
Dirección Sockaddr_in;

Pero, ¿cómo se llama en el código esta misteriosa función “htons”? Esta es solo una pequeña función auxiliar que convierte el orden de bytes de un entero de 16 bits del actual (little-o big-endian) al big-endian que se utiliza para la comunicación de red. Es necesario llamarlo cada vez que utilice números enteros cuando trabaje directamente con sockets.

Verás la función “htons” y su contraparte de 32 bits “htonl” varias veces más en este artículo, así que presta atención.

Cambiar el enchufe al modo sin bloqueo
De forma predeterminada, los sockets están en lo que se llama "modo de bloqueo". Esto significa que si intenta leer datos usando "recvfrom", la función no regresará hasta que el socket reciba un paquete con datos que se puedan leer. Este comportamiento no nos conviene en absoluto. Los juegos son aplicaciones en tiempo real que se ejecutan a entre 30 y 60 fotogramas por segundo, ¡y el juego no puede simplemente detenerse y esperar a que llegue un paquete de datos!

Puede resolver este problema cambiando el socket al "modo sin bloqueo" después de su creación. En este modo, la función "recvfrom", si no hay datos para leer del socket, devuelve inmediatamente un valor determinado que indica que será necesario volver a llamarla cuando haya datos en el socket.

Puede configurar el socket en modo sin bloqueo de la siguiente manera:

#if PLATAFORMA == PLATAFORMA_MAC || PLATAFORMA == PLATFORM_UNIX int sin bloqueo = 1;
if (fcntl(handle, F_SETFL, O_NONBLOCK, nonBlocking) == -1) ( printf("no se pudo establecer el socket sin bloqueo\n"); return false; ) #elif PLATFORM == PLATFORM_WINDOWS DWORD nonBlocking = 1;

if (ioctlsocket(handle, FIONBIO, &nonBlocking) != 0) ( printf("no se pudo establecer el socket sin bloqueo\n"); return false; ) #endif
Como puede ver, no existe la función "fcntl" en Windows, por lo que usamos "ioctlsocket" en su lugar.

Enviando paquetes

UDP es un protocolo sin conexión, por lo que cada vez que enviamos un paquete debemos especificar la dirección del destinatario. Puede utilizar el mismo socket UDP para enviar paquetes a diferentes direcciones IP; el otro extremo del socket no tiene que ser la misma computadora.
Tenga en cuenta que el valor de retorno de la función "enviar a" solo indica si el paquete se envió correctamente desde la computadora local. ¡Pero no indica si el paquete fue recibido por el destino! UDP no tiene medios para determinar si un paquete ha llegado a su destino previsto o no.

En el código anterior, pasamos la estructura "sockaddr_in" como dirección de destino. ¿Cómo conseguimos esta estructura?

Digamos que queremos enviar un paquete a 207.45.186.98:30000.

Escribamos la dirección de la siguiente forma:

Ent sin signo a = 207;
int sin signo b = 45;

int sin signo c = 186;<< 24) | (b << 16) | (c << 8) | d; unsigned short destination_port = port; sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(destination_address); address.sin_port = htons(destination_port);
int sin signo d = 98;

puerto corto sin firmar = 30000;

Y necesitas hacer un par de transformaciones más para darle un formato que “sendto” entienda:
Unsigned int dirección_destino = (a

Como puede ver, primero combinamos los números a, b, c, d (que están en el rango) en un número entero, en el que cada byte es uno de los números originales. Luego inicializamos la estructura "sockaddr_in" con nuestra dirección y puerto de destino, recordando convertir el orden de bytes usando las funciones "htonl" y "htons".

Vale la pena resaltar por separado el caso en el que necesita enviarse un paquete a usted mismo: en este caso, no necesita averiguar la dirección IP de la máquina local, sino que simplemente puede usar 127.0.0.1 como dirección (bucle invertido local dirección), y el paquete se enviará a la computadora local.

Recibir paquetes<= 0) break; unsigned int from_address = ntohl(from.sin_addr.s_addr); unsigned int from_port = ntohs(from.sin_port); // process received packet }
Los paquetes que sean mayores que el tamaño del búfer de recepción simplemente se eliminarán silenciosamente de la cola. Entonces, si usa un búfer de 256 bytes como en el ejemplo anterior, y alguien le envía un paquete de 300 bytes, se descartará. No obtendrá sólo los primeros 256 bytes del paquete.

Pero como escribimos nuestro propio protocolo, esto no será un problema para nosotros. Siempre tenga cuidado y asegúrese de que el tamaño del búfer de recepción sea lo suficientemente grande como para acomodar el paquete más grande que se le pueda enviar.

Cerrar un enchufe
En la mayoría de los sistemas tipo Unix, los sockets son descriptores de archivos, por lo que para cerrar los sockets después de su uso, puede utilizar la función estándar "cerrar". Sin embargo, Windows, como siempre, destaca, y en él necesitamos utilizar “closesocket”.

#if PLATAFORMA == PLATAFORMA_MAC || PLATAFORMA == PLATFORM_UNIX cerrar(socket);
#elif PLATAFORMA == PLATAFORMA_WINDOWS closesocket(socket);

#endif
¡Sigue así, Windows!

clase de zócalo

Entonces, hemos descubierto todas las operaciones básicas: crear un socket, vincularlo a un puerto, cambiar al modo sin bloqueo, enviar y recibir paquetes y finalmente cerrar el socket.

Pero, como habrás notado, todas estas operaciones difieren ligeramente de una plataforma a otra y, por supuesto, es difícil recordar las características de diferentes plataformas y escribir todos estos #ifdefs cada vez que se trabaja con sockets.

Por lo tanto, crearemos una clase contenedora "Socket" para todas estas operaciones. También crearemos una clase de "Dirección" para facilitar el trabajo con direcciones IP. Nos permitirá no realizar todas las manipulaciones con “sockaddr_in” cada vez que queramos enviar o recibir un paquete.
Entonces, nuestra clase Socket:

Class Socket ( public: Socket(); ~Socket(); bool Open(puerto corto sin firmar); void Close(); bool IsOpen() const; bool Send(const Dirección y destino, const void * data, int size); int Recibir (Dirección y remitente, datos anulados *, tamaño int);
Debe utilizarlos para recepción y transmisión de la siguiente manera:

// crear socket const int port = 30000;
Toma de corriente;

if (!socket.Open(port)) ( printf("¡no se pudo crear el socket!\n"); return false; ) // envía un paquete const char data = "¡hola mundo!";
socket.Send(Dirección(127,0,0,1,puerto), datos, tamaño de(datos));

// recibir paquetes mientras (verdadero) ( Dirección del remitente; búfer de caracteres sin firmar; int bytes_read = socket.Receive(remitente, búfer, tamaño de (búfer)); if (!bytes_read) break; // procesar paquete)

Como puede ver, esto es mucho más fácil que trabajar directamente con sockets BSD. Y también este código será el mismo para todos los sistemas operativos, porque toda la funcionalidad dependiente de la plataforma se encuentra dentro de las clases Socket y Address.

Conclusión
Ahora tenemos una herramienta independiente de la plataforma para enviar y recibir paquetes UDP.
UDP no admite conexiones y quería hacer un ejemplo que lo mostrara claramente. Así que escribí un pequeño programa que lee una lista de direcciones IP de un archivo de texto y les envía paquetes, uno por segundo. Cada vez que el programa recibe un paquete, muestra la dirección y el puerto de la computadora emisora ​​y el tamaño del paquete recibido en la consola.
Puede configurar fácilmente el programa para que incluso en una máquina local pueda obtener varios nodos intercambiando paquetes entre sí. Para hacer esto, simplemente asigne diferentes puertos a diferentes instancias del programa, por ejemplo:

>Nodo 30000

>Nodo 30001

>Nodo 30002

Etc...

Cada nodo reenviará paquetes a todos los demás nodos, formando algo así como un mini sistema peer-to-peer. Desarrollé este programa en MacOSX, pero debería compilarse en cualquier sistema operativo tipo Unix y en Windows, pero si necesita realizar alguna modificación, hágamelo saber.

Los sockets se desarrollaron originalmente para UNIX en la Universidad de California, Berkeley. En UNIX, el método de E/S de comunicación sigue el algoritmo de apertura/lectura/escritura/cierre. Antes de poder utilizar un recurso, se debe abrir con los permisos adecuados y otras configuraciones. Una vez que un recurso está abierto, se pueden leer o escribir datos. Después de usar un recurso, el usuario debe llamar al método Close() para indicarle al sistema operativo que ha terminado con el recurso.

¿Cuándo se agregaron funciones al sistema operativo UNIX? Comunicación entre procesos (IPC) y el intercambio de redes, se tomó prestado el conocido patrón de entrada-salida. Todos los recursos expuestos para la comunicación en UNIX y Windows se identifican mediante identificadores. Estos descriptores, o manijas, puede apuntar a un archivo, memoria o algún otro canal de comunicación, pero en realidad apunta a una estructura de datos interna utilizada por el sistema operativo. El socket, al ser el mismo recurso, también está representado por un descriptor. Por lo tanto, para los enchufes, la vida útil de una manija se puede dividir en tres fases: abrir (crear) el enchufe, recibir o enviar al enchufe y finalmente cerrar el enchufe.

La interfaz IPC para la comunicación entre diferentes procesos se basa en métodos de E/S. Facilitan que los sockets envíen y reciban datos. Cada objetivo se especifica mediante una dirección de socket, por lo que esta dirección se puede especificar en el cliente para establecer una conexión con el objetivo.

Tipos de enchufes

Hay dos tipos principales de sockets: sockets de flujo y sockets de datagramas.

Tomas de corriente

Un socket de flujo es un socket basado en conexión que consta de un flujo de bytes que puede ser bidireccional, lo que significa que una aplicación puede enviar y recibir datos a través de este punto final.

Un socket de flujo garantiza la corrección de errores, maneja la entrega y mantiene la coherencia de los datos. Se puede confiar en que entregará datos duplicados y ordenados. Un socket de flujo también es adecuado para transferir grandes cantidades de datos, ya que la sobrecarga de establecer una conexión separada para cada mensaje enviado puede resultar prohibitiva para pequeñas cantidades de datos. Los sockets de flujo alcanzan este nivel de calidad mediante el uso de un protocolo. Protocolo de control de transmisión (TCP). TCP garantiza que los datos lleguen al otro lado en la secuencia correcta y sin errores.

Para este tipo de socket, la ruta se forma antes de enviar los mensajes. Esto asegura que ambas partes involucradas en la interacción acepten y respondan. Si una aplicación envía dos mensajes a un destinatario, se garantiza que los mensajes se recibirán en la misma secuencia.

Sin embargo, los mensajes individuales pueden dividirse en paquetes y no hay forma de determinar los límites de los registros. Al utilizar TCP, este protocolo se encarga de dividir los datos transmitidos en paquetes del tamaño adecuado, enviarlos a la red y volver a ensamblarlos en el otro lado. La aplicación solo sabe que envía una cierta cantidad de bytes a la capa TCP y la otra parte recibe esos bytes. A su vez, TCP divide efectivamente estos datos en paquetes del tamaño adecuado, los recibe del otro lado, extrae los datos de ellos y los combina.

Las transmisiones se basan en conexiones explícitas: el socket A solicita una conexión al socket B y el socket B acepta o rechaza la solicitud de conexión.

Si se debe garantizar que los datos se entregarán al otro lado o el tamaño de los datos es grande, los sockets de flujo son preferibles a los sockets de datagramas. Por lo tanto, si la comunicación confiable entre dos aplicaciones es de suma importancia, elija stream sockets.

Un servidor de correo electrónico es un ejemplo de una aplicación que debe entregar contenido en el orden correcto, sin duplicaciones ni omisiones. El socket de transmisión depende de TCP para garantizar que los mensajes se entreguen a sus destinos.

Conectores de datagramas

Los sockets de datagramas a veces se denominan sockets sin conexión, es decir, no se establece ninguna conexión explícita entre ellos: el mensaje se envía al socket especificado y, en consecuencia, se puede recibir desde el socket especificado.

Los sockets de flujo proporcionan un método más confiable que los sockets de datagramas, pero para algunas aplicaciones la sobrecarga asociada con el establecimiento de una conexión explícita es inaceptable (por ejemplo, un servidor de hora del día que proporciona sincronización horaria a sus clientes). Después de todo, establecer una conexión confiable con el servidor lleva tiempo, lo que simplemente introduce retrasos en el servicio y la tarea de la aplicación del servidor falla. Para reducir la sobrecarga, debe utilizar sockets de datagramas.

El uso de sockets de datagramas requiere que la transferencia de datos del cliente al servidor sea manejada por Protocolo de datagramas de usuario (UDP). En este protocolo, se imponen algunas restricciones en el tamaño de los mensajes y, a diferencia de los sockets de flujo, que pueden enviar mensajes de manera confiable al servidor de destino, los sockets de datagramas no brindan confiabilidad. Si los datos se pierden en algún lugar de la red, el servidor no informará errores.

Además de los dos tipos discutidos, también existe una forma generalizada de encajes, que se denomina sin procesar o en bruto.

Enchufes crudos

El objetivo principal de utilizar sockets sin formato es evitar el mecanismo mediante el cual la computadora maneja TCP/IP. Esto se logra proporcionando una implementación especial de la pila TCP/IP que anula el mecanismo proporcionado por la pila TCP/IP en el kernel: el paquete se pasa directamente a la aplicación y, por lo tanto, se procesa de manera mucho más eficiente que cuando pasa a través del cliente. pila de protocolo principal.

Por definición, un socket sin formato es un socket que acepta paquetes, omite las capas TCP y UDP en la pila TCP/IP y los envía directamente a la aplicación.

Cuando se utilizan dichos sockets, el paquete no pasa a través del filtro TCP/IP, es decir. no se procesa de ninguna manera y aparece en su forma cruda. En este caso, es responsabilidad de la aplicación receptora procesar adecuadamente todos los datos y realizar acciones como eliminar encabezados y analizar campos, como incluir una pequeña pila TCP/IP en la aplicación.

Sin embargo, no es frecuente que necesite un programa que se ocupe de sockets sin formato. A menos que esté escribiendo software de sistema o un programa similar a un rastreador de paquetes, no necesitará entrar en tantos detalles. Los sockets sin formato se utilizan principalmente en el desarrollo de aplicaciones de protocolos especializados de bajo nivel. Por ejemplo, varias utilidades TCP/IP, como trace route, ping o arp, utilizan sockets sin formato.

Trabajar con sockets sin formato requiere un conocimiento sólido de los protocolos básicos TCP/UDP/IP.

Puertos

El puerto está definido para permitir el problema de la comunicación simultánea con múltiples aplicaciones. Básicamente, amplía el concepto de dirección IP. Una computadora que ejecuta varias aplicaciones al mismo tiempo y recibe un paquete de la red puede identificar el proceso de destino utilizando el número de puerto único especificado cuando se estableció la conexión.

El socket consta de la dirección IP de la máquina y el número de puerto utilizado por la aplicación TCP. Dado que una dirección IP es única en Internet y los números de puerto son únicos en una máquina individual, los números de socket también son únicos en todo Internet. Esta característica permite que un proceso se comunique a través de la red con otro proceso basándose únicamente en el número de socket.

Los números de puerto están reservados para ciertos servicios; son números de puerto bien conocidos, como el puerto 21, que se utiliza en FTP. Su aplicación puede utilizar cualquier número de puerto que no haya sido reservado y que aún no esté en uso. Agencia Autoridad de Números Asignados de Internet (IANA) mantiene una lista de números de puertos comúnmente conocidos.

Normalmente, una aplicación cliente-servidor que utiliza sockets consta de dos aplicaciones diferentes: un cliente que inicia una conexión con un destino (servidor) y un servidor que espera una conexión del cliente.

Por ejemplo, en el lado del cliente, la aplicación debe conocer la dirección de destino y el número de puerto. Al enviar una solicitud de conexión, el cliente intenta establecer una conexión con el servidor:

Si los eventos se desarrollan exitosamente, siempre que el servidor se inicie antes de que el cliente intente conectarse a él, el servidor acepta la conexión. Habiendo dado su consentimiento, la aplicación del servidor crea un nuevo socket para interactuar específicamente con el cliente que estableció la conexión:

Ahora el cliente y el servidor pueden interactuar entre sí, leyendo mensajes cada uno desde su propio socket y, en consecuencia, escribiendo mensajes.

Trabajar con sockets en .NET

La compatibilidad con sockets en .NET la proporcionan las clases en el espacio de nombres Sistema.Net.Sockets- comencemos con su breve descripción.

Clases para trabajar con enchufes.
Clase Descripción
Opción de multidifusión La clase MulticastOption establece el valor de la dirección IP para unirse o abandonar un grupo de IP.
Flujo de red La clase NetworkStream implementa una clase de flujo base desde la cual se envían y reciben datos. Esta es una abstracción de alto nivel que representa una conexión a un canal de comunicación TCP/IP.
Cliente TCP La clase TcpClient se basa en la clase Socket para proporcionar un servicio TCP de nivel superior. TcpClient proporciona varios métodos para enviar y recibir datos a través de la red.
TcpListener Esta clase también se basa en la clase Socket de bajo nivel. Su objetivo principal son las aplicaciones de servidor. Escucha las solicitudes de conexión entrantes de los clientes y notifica a la aplicación sobre cualquier conexión.
Cliente Udp UDP es un protocolo sin conexión, por lo que se requiere una funcionalidad diferente para implementar el servicio UDP en .NET.
Excepción de socket Esta excepción se produce cuando se produce un error en el socket.
Enchufe La última clase en el espacio de nombres System.Net.Sockets es la propia clase Socket. Proporciona la funcionalidad básica de una aplicación de socket.

clase de zócalo

La clase Socket juega un papel importante en la programación de redes, proporcionando funcionalidad tanto de cliente como de servidor. Principalmente, las llamadas a métodos de esta clase realizan las comprobaciones necesarias relacionadas con la seguridad, incluida la verificación de permisos de seguridad, después de lo cual se reenvían a las contrapartes de los métodos en la API de Windows Sockets.

Antes de pasar a un ejemplo del uso de la clase Socket, veamos algunas propiedades y métodos importantes de esta clase:

Propiedades y métodos de la clase Socket.
Propiedad o método Descripción
DirecciónFamilia Proporciona la familia de direcciones de socket: un valor de la enumeración Socket.AddressFamily.
Disponible Devuelve la cantidad de datos disponibles para lectura.
Bloqueo Obtiene o establece un valor que indica si el socket está en modo de bloqueo.
Conectado Devuelve un valor que indica si el socket está conectado al host remoto.
Punto final local Proporciona el punto final local.
Tipo de protocolo Proporciona el tipo de protocolo del socket.
Punto final remoto Proporciona el punto final del socket remoto.
Tipo de enchufe Da el tipo de enchufe.
Aceptar() Crea un nuevo socket para manejar una solicitud de conexión entrante.
Unir() Vincula un socket a un punto final local para escuchar las solicitudes de conexión entrantes.
Cerca() Fuerza el cierre del enchufe.
Conectar() Establece una conexión con un host remoto.
ObtenerOpciónSocket() Devuelve el valor de SocketOption.
IOControl() Establece modos de funcionamiento de bajo nivel para el enchufe. Este método proporciona acceso de bajo nivel a la clase Socket subyacente.
Escuchar() Pone el socket en modo de escucha (espera). Este método es sólo para aplicaciones de servidor.
Recibir() Recibe datos de un enchufe conectado.
Encuesta() Determina el estado del socket.
Seleccionar() Comprueba el estado de uno o más sockets.
Enviar() Envía datos al enchufe conectado.
EstablecerOpciónSocket() Establece la opción de socket.
Cerrar() Deshabilita las operaciones de envío y recepción en el socket.

Creando un enchufe

La llamada al sistema de socket crea un socket y devuelve un identificador que se puede usar para acceder al socket:

#incluir
#incluir
int socket(dominio int, tipo int, protocolo int);

El socket creado es un punto final de la línea de transmisión. El parámetro de dominio especifica la familia de direcciones, el parámetro de tipo especifica el tipo de comunicación utilizada con este socket y el protocolo especifica el protocolo utilizado.

en la mesa 15.1 muestra los nombres de dominio.

Tabla 15.1

Los dominios de socket más populares incluyen AF_UNIX, utilizado para sockets locales implementados por sistemas de archivos UNIX y Linux, y AF_INET, utilizado para sockets de red UNIX. Los sockets de dominio AF_INET pueden ser utilizados por programas que se comunican a través de redes basadas en el protocolo TCP/IP, incluido Internet. La interfaz de Windows Winsock también proporciona acceso a este dominio de socket.

El parámetro de tipo de socket especifica las características de comunicación que se aplicarán al nuevo socket. Los valores posibles son SOCK_STREAM y SOCK_DGRAM.

SOCK_STREAM es un flujo de bytes bidireccional, ordenado, confiable y basado en conexión. En el caso del dominio de socket AF_INET, este tipo de comunicación de forma predeterminada la proporciona una conexión TCP que se establece entre los dos puntos finales del socket de flujo al conectarse. Los datos se pueden transferir en dos direcciones a través de un enlace de socket. Los protocolos TCP incluyen un medio para fragmentar y luego reensamblar mensajes grandes y retransmitir cualquier parte que pueda haberse perdido en la red.

SOCK_DGRAM - servicio de datagramas. Puede utilizar dicho socket para enviar mensajes con un tamaño máximo fijo (normalmente pequeño), pero no hay garantía de que el mensaje se entregue o de que no se reordenen en la red. En el caso de los sockets de dominio AF_INET, este tipo de transferencia de datos se proporciona mediante datagramas UDP (User Datagram Protocol).

El protocolo utilizado para la comunicación suele estar determinado por el tipo de socket y el dominio. Como regla general, no hay elección. El parámetro de protocolo se utiliza en los casos en los que todavía se proporciona una opción. La configuración 0 le permite seleccionar el protocolo estándar utilizado en todos los ejemplos de este capítulo.

La llamada al sistema de socket devuelve un identificador muy parecido a un descriptor de archivo de bajo nivel. Cuando un socket está conectado al punto final de otro socket, puede usar las llamadas al sistema de lectura y escritura en el identificador del socket para enviar y recibir datos usando sockets. La llamada al sistema de cierre se utiliza para cerrar una conexión de socket.

VSEVOLOD STÁJOV

Programación de sockets

La gran mayoría de los programas de servidores de red se organizan mediante sockets. Esencialmente, los sockets son similares a los descriptores de archivos con una diferencia muy importante: los sockets se utilizan para la comunicación entre aplicaciones, ya sea en una red o en una máquina local. Por tanto, el programador no tiene ningún problema con la entrega de datos; Sólo necesita asegurarse de que los parámetros del socket de las dos aplicaciones coincidan.

Por tanto, los sockets de red son estructuras emparejadas que están estrictamente sincronizadas entre sí. Para crear sockets, cualquier sistema operativo que los admita utiliza la función socket (afortunadamente, los sockets están lo suficientemente estandarizados como para poder usarlos para transferir datos entre aplicaciones que se ejecutan en diferentes plataformas). El formato de la función es:

int socket(dominio int, tipo int, protocolo int);

El parámetro de dominio especifica el tipo de protocolo de transporte, es decir. Protocolo de entrega de paquetes de red. Actualmente se admiten los siguientes protocolos (pero tenga en cuenta que el tipo de estructura de dirección será diferente para los diferentes tipos de protocolo):

  • PF_UNIX o PF_LOCAL– comunicación local para SO UNIX (y similares).
  • PF_INET – IPv4, IP-Protocolo de Internet, el más común actualmente (dirección de 32 bits).
  • PF_INET6– IPv6, la próxima generación del protocolo IP (IPng) – Dirección de 128 bits.
  • PF_IPX – IPX– Protocolos Novell.

Se admiten otros protocolos, pero estos 4 son los más populares.

El parámetro de tipo significa el tipo de socket, es decir cómo se transferirán los datos: generalmente se usa la constante SOCK_STREAM; su uso significa una transferencia segura de datos en un flujo bidireccional con control de errores. Con este método de transferencia de datos, el programador no tiene que preocuparse por manejar errores de red, aunque esto no protege contra errores lógicos, lo cual es importante para un servidor de red.

El parámetro de protocolo especifica el tipo de protocolo específico para un dominio determinado, por ejemplo IPPROTO_TCP o IPPROTO_UDP (el parámetro de tipo debe ser SOCK_DGRAM en este caso).

La función de socket simplemente crea un punto final y devuelve un identificador de socket; Hasta que el enchufe esté conectado a la dirección remota mediante la función de conexión, ¡no se podrán enviar datos a través de él! Si se pierden paquetes en la red, p. Cuando ocurre una falla de comunicación, la señal Tubería Rota – SIGPIPE se envía a la aplicación que creó el socket, por lo que es recomendable asignar un manejador a esta señal usando la función de señal. Después de conectar un socket a otro mediante la función de conexión, los datos se pueden enviar a través de él mediante funciones estándar de lectura y escritura o funciones especializadas de recepción y envío. Una vez finalizado el trabajo, se debe cerrar el enchufe mediante la función de cierre. Para crear una aplicación cliente, simplemente conecte un socket local a un socket remoto (servidor) usando la función de conexión. El formato de esta función es:

int connect(int sock_fd, const struct *sockaddr serv_addr, socketlen_t addr_len);

Si hay un error, la función devuelve -1; el estado del error se puede obtener utilizando el sistema operativo. Si tiene éxito, se devuelve 0. Un socket, una vez conectado, normalmente no se puede volver a conectar, como ocurre, por ejemplo, en el protocolo IP. El parámetro sock_fd especifica el descriptor del socket, la estructura serv_addr asigna la dirección del punto final remoto, addr_len contiene la longitud de serv_addr (el tipo socketlen_t tiene orígenes históricos, generalmente es el mismo que el tipo int). El parámetro más importante de esta función es la dirección del socket remoto. Naturalmente, no es lo mismo para diferentes protocolos, por lo que describiré aquí la estructura de direcciones solo para el protocolo IP (v4). Para esto, se utiliza una estructura especializada sockaddr_in (debe convertirse directamente al tipo sockaddr al llamar a connect). Los campos de esta estructura se ven así:

estructura sockaddr_in(

Sa_familia_t sin_familia; – define la familia de direcciones, siempre debe ser AF_INET

U_int16_t sin_port; – puerto de socket en orden de bytes de red

Estructura in_addr sin_addr; – estructura que contiene la dirección IP

Estructura que describe una dirección IP:

estructura in_addr(

U_int32_t s_addr; – dirección IP del socket en orden de bytes de red

Tenga en cuenta el orden de bytes especial en todos los campos de números enteros. Para convertir el número de puerto al orden de bytes de la red, puede utilizar la macro htons (puerto corto sin firmar). Es muy importante utilizar este tipo particular de número entero: un entero corto sin signo.

Las direcciones IPv4 se dividen en únicas, de difusión (broadcast) y de grupo (multicast). Cada dirección única apunta a una interfaz de host, las direcciones de transmisión apuntan a todos los hosts de la red y las direcciones de multidifusión apuntan a todos los hosts de un grupo de multidifusión. La estructura in_addr se puede asignar a cualquiera de estas direcciones. Pero para los clientes de socket, en la gran mayoría de los casos se asigna una única dirección. La excepción es el caso cuando necesita escanear toda la red local en busca de un servidor, luego puede usar una dirección de transmisión. Entonces, lo más probable es que el servidor deba informar su dirección IP real y que se le deba conectar un socket para una mayor transferencia de datos. Transmitir datos a través de direcciones de difusión no es una buena idea, ya que no se sabe qué servidor está procesando la solicitud. Por lo tanto, los sockets actualmente orientados a la conexión solo pueden usar direcciones únicas. Para los servidores de socket que escuchan direcciones, la situación es diferente: aquí se permite utilizar direcciones de transmisión para responder inmediatamente a la solicitud del cliente sobre la ubicación del servidor. Pero primero lo primero. Como habrás notado, en la estructura sockaddr_in el campo de dirección IP se representa como un entero largo sin signo y estamos acostumbrados a direcciones en formato x.x.x.x (172.16.163.89) o en formato de caracteres (myhost.com). Para convertir el primero, use la función inet_addr (const char *ip_addr), y para el segundo, use la función gethostbyname (const char *host). Miremos a ambos:

u_int32_t inet_addr(const char *ip_addr)

– devuelve un entero inmediato adecuado para su uso en la estructura sockaddr_in en la dirección IP que se le pasa en el formato x.x.x.x. Si se produce un error, se devuelve INADDR_NONE.

estructura HOSTENT* gethostbyname(const char *nombre_host)

– devuelve la estructura de información sobre el host según su nombre. Si no tiene éxito, devuelve NULL. El nombre se busca primero en el archivo de hosts y luego en DNS. La estructura HOSTENT proporciona información sobre el host requerido. De todos sus campos, el más importante es el campo char **h_addr_list, que representa una lista de direcciones IP para un host determinado. Por lo general, se usa h_addr_list, que representa la primera dirección IP del host; también puede usar la expresión h_addr para esto. Después de ejecutar la función gethostbyname, la lista h_addr_list de la estructura HOSTENT contiene direcciones IP simbólicas simples, por lo que debe usar adicionalmente la función inet_addr para convertir al formato sockaddr_in.

Entonces, conectamos el socket del cliente con la función de conexión del servidor. Luego podrá utilizar las funciones de transferencia de datos. Para hacer esto, puede usar funciones de E/S estándar de bajo nivel para archivos, ya que un socket es esencialmente un descriptor de archivo. Desafortunadamente, las funciones de trabajo de bajo nivel con archivos pueden variar según los diferentes sistemas operativos, por lo que debe consultar el manual de su sistema operativo. Tenga en cuenta que la comunicación de red puede finalizar con una señal SIGPIPE y las funciones de lectura/escritura devolverán un error. Siempre debes recordar revisar si hay errores, además, no debes olvidar que la transferencia de datos a través de la red puede ser muy lenta, y las funciones de entrada/salida son síncronas, y esto puede causar retrasos importantes en el programa.

Para transferir datos entre sockets, existen funciones especiales que son comunes a todos los sistemas operativos: estas son las funciones de la familia recv y send. Su formato es muy similar:

int enviar(int sockfd, void *data, size_t len, int flags);

– envía el buffer de datos.

int recv(int sockfd, void *data, size_t len, int flags);

– acepta un buffer de datos.

El primer argumento es el descriptor del socket, el segundo es un puntero a los datos que se transferirán, el tercero es la longitud del búfer y el cuarto son las banderas. Si tiene éxito, se devuelve el número de bytes transferidos; si no, se devuelve un código de error negativo. Las banderas le permiten cambiar los parámetros de transferencia (por ejemplo, habilitar el modo de operación asincrónica), pero para la mayoría de las tareas es suficiente dejar el campo de banderas en cero para el modo de transferencia normal. Al enviar o recibir datos, las funciones bloquean la ejecución del programa antes de que se haya enviado todo el búfer. Y cuando se utiliza el protocolo tcp/ip, debe llegar una respuesta del socket remoto indicando el envío o recepción exitoso de datos; de lo contrario, el paquete se envía nuevamente. Al enviar datos, considere la MTU de la red (tamaño máximo de trama transmitida al mismo tiempo). Puede ser diferente para diferentes redes, por ejemplo, para una red Ethernet es 1500.

Entonces, para completar, daré el ejemplo más simple de un programa en C que implementa un cliente de socket:

#incluir /* Bibliotecas de sockets estándar para Linux */

#incluir /* Para el sistema operativo Windows use #include */

#incluir

int principal())(

Int calcetín = -1;

/* Descriptor de socket */

Char buf;

Char s = "Cliente listo";

HOSTENT *h = NULO;

Sockaddr_in dirección;

Puerto corto sin firmar = 80;

Addr.sin_family = AF_INET;

/* Crear un socket */

Si(calcetínfd == -1)

/* Si se ha creado el socket */

Devolver -1;

H = gethostbyname("www.myhost.com");

/* Obtener la dirección del host */

Si(h == NULO)

/* ¿Existe tal dirección? */

Devolver -1;

Addr.sin_addr.s_addr = inet_addr(h->h_addr_list);

/* Convierte la dirección IP a un número */

Si (conectar (sockfd, (sockaddr*) & dirección, tamaño de (dirección)))

/* Intentando conectarse a un enchufe remoto */

Devolver -1;

/* La conexión fue exitosa - continuar */

Si(enviar(sockfd, s, tamaño de(s), 0)< 0)

Devolver -1;

Si(recv(sockfd, buf, tamaño de(buf), 0)< 0)

Devolver -1;

Cerrar(sockfd);

/* Cerramos el socket */

/* Para Windows, se utiliza la función closesocket(s) */

Devuelve 0;

Verás, usar enchufes no es tan difícil. Las aplicaciones de servidor utilizan principios completamente diferentes para trabajar con sockets. Primero, se crea un socket, luego se le asigna una dirección local usando la función de vinculación, pero puede asignar una dirección de transmisión al socket. Luego, la función de escucha comienza a escuchar la dirección y las solicitudes de conexión se colocan en una cola. Es decir, la función de escucha inicializa el socket para recibir mensajes. Después de esto, debe usar la función de aceptación, que devuelve un nuevo socket ya asociado con el cliente. Es común que los servidores acepten muchas conexiones en intervalos cortos. Por lo tanto, debe verificar constantemente la cola de conexiones entrantes utilizando la función de aceptación. Para organizar este comportamiento, la mayoría de las veces recurren a las capacidades del sistema operativo. Para el sistema operativo Windows, se utiliza con mayor frecuencia una versión multiproceso de operación del servidor; después de aceptar la conexión, se crea un nuevo hilo en el programa, que procesa el socket. En sistemas *nix, se utiliza con mayor frecuencia la creación de un proceso hijo mediante la función fork. Al mismo tiempo, los costos generales se reducen debido al hecho de que en realidad hay una copia del proceso en el sistema de archivos proc. En este caso, todas las variables del proceso hijo son las mismas que las del proceso padre. Y el proceso hijo puede manejar inmediatamente la conexión entrante. El proceso principal continúa escuchando. Tenga en cuenta que los puertos numerados del 1 al 1024 son privilegiados y no siempre es posible escucharlos. Un punto más: ¡no puedes tener dos sockets diferentes escuchando el mismo puerto en la misma dirección! Primero, veamos los formatos de las funciones anteriores para crear un socket de servidor:

int bind(int sockfd, const struct *sockaddr, socklen_t addr_len);

– asigna una dirección local al socket para permitirle aceptar conexiones entrantes. Para una dirección, puede utilizar la constante INADDR_ANY, que le permite aceptar conexiones entrantes de todas las direcciones en una subred determinada. El formato de la función es similar a conectar. En caso de error, devuelve un valor negativo.

int escucha(int sockfd, int backlog);

– la función crea una cola de sockets entrantes (el número de conexiones está determinado por el parámetro backlog, no debe exceder el número SOMAXCONN, que depende del sistema operativo). Después de crear la cola, puede esperar una conexión usando la función de aceptación. Los sockets suelen estar bloqueando, por lo que la ejecución del programa se suspende hasta que se acepta la conexión. En caso de error, se devuelve -1.

int aceptar(int sockfd, estructura *sockaddr, socklen_t addr_len)

– la función espera una conexión entrante (o la elimina de la cola de conexiones) y devuelve un nuevo socket ya asociado con el cliente remoto. En este caso, el socket original sockfd permanece sin cambios. La estructura sockaddr se llena con valores del socket remoto. En caso de error, se devuelve -1.

Aquí hay un ejemplo de un servidor de socket simple que usa la función fork para crear un proceso hijo que maneja la conexión:

int principal())(

Pid_t pid;

/* ID del proceso hijo */

Int calcetín = -1;

/* Identificador del socket para escuchar */

Int s = -1;

/* Recibir descriptor de socket */

Char buf;

/* Puntero al buffer a recibir */

Char str = "Servidor listo";

/* Cadena para enviar al servidor */

HOSTENT *h = NULO;

/* Estructura para obtener una dirección IP */

Sockaddr_in dirección;

/* Estructura del protocolo tcp/ip */

Sockaddr_in raddr;

Puerto corto sin firmar = 80;

/* Completa los campos de la estructura: */

Addr.sin_family = AF_INET;

Addr.sin_port = htons(puerto);

sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

/* Crear un socket */

Si(calcetínfd == -1)

/* Si se ha creado el socket */

Devolver -1;

Addr.sin_addr.s_addr = INADDR_ANY;

/* Escuchar en todas las direcciones */

Si(bind(sockfd, (sockaddr*) &addr, sizeof(addr)))

/* Asigna una dirección local al socket */

Devolver -1;

Si(escucha(sockfd, 1))

/* Empezar a escuchar */

Devolver -1;

S = aceptar(sockfd, (sockaddr *) &raddr, sizeof(raddr));

/* Aceptar conexión */

Pid = bifurcación();

/* generar un proceso hijo */

Si(pid == 0)(

/* Este es un proceso hijo */

Si(recv(s, buf, tamaño de(buf), 0)< 0)

/* Envía la cadena s al socket remoto */

Devolver -1;

Si (enviar (s, cadena, tamaño de (cadena), 0)< 0)

/* Recibir una respuesta del servidor remoto */

Devolver -1;

Printf("La cadena recibida fue: %s", buf);

/* Imprimir buffer a salida estándar */

Cerrar(es);

/* Cerramos el socket */

Devuelve 0;

/* Salir del proceso hijo */

Cerrar(sockfd);

/* Cerrar la toma de escucha */

Devuelve 0;

Al crear un subproceso para procesar un socket, consulte el manual del sistema operativo, ya que llamar a la función de creación de subprocesos puede variar significativamente en diferentes sistemas. Pero los principios de procesamiento del hilo siguen siendo los mismos. La función de procesamiento solo necesita pasar un puntero de socket como argumento (generalmente cualquier tipo de datos en formato void * se puede pasar a la función de flujo, lo que requiere el uso de una conversión de tipos).

Nota importante para sistemas Windows. Noté que el sistema de sockets no funciona sin usar la función WSAStartup para inicializar la biblioteca de sockets. Un programa de socket de Windows debería comenzar así:

WSADATA wsaData;

WSAStartup(0x0101, &wsaData);

Y al salir del programa escribe lo siguiente:

WSACleanup();

Dado que la mayoría de las operaciones de socket se bloquean, la ejecución de la tarea a menudo debe interrumpirse esperando la sincronización. Por lo tanto, los sistemas tipo *nix a menudo evitan bloquear la consola creando un tipo especial de programa: un demonio. El demonio no pertenece a las consolas virtuales y ocurre cuando un proceso hijo llama a una bifurcación y el proceso padre termina antes que el segundo hijo (y esto siempre sucede de esta manera). Después de esto, el segundo proceso hijo se convierte en el proceso principal y no bloquea la consola. A continuación se muestra un ejemplo del comportamiento de este programa:

pid = bifurcación();

/* Crea el primer proceso hijo */

si (pid<0){

/* Error al llamar a la bifurcación */

Printf("Error de bifurcación:) ");

Salir(-1);

)de lo contrario si (pid!=0)(

/* ¡Este es el primer padre! */

Printf("Este es un Padre 1 ");

)demás(

Pid = bifurcación();

/* El trabajo del 1er padre termina */

/* Y llamamos a otro proceso hijo */

si (pid<0){

Printf("Error de bifurcación:) ");

Salir(-1);

)de lo contrario si (pid!=0)(

/* Este es el segundo padre */

Printf("Este es un padre 2 ");

)demás(

/* Y este es el mismo proceso del segundo hijo */

/* Cambiar al modo demonio "estándar" */

Conjuntosid();

/* Ejecutar el demonio en modo superusuario */

Máscara U(0); /* Máscara de archivo estándar */

Chdir("/"); /* Ir al directorio raíz */

Código demoníaco(); /* En realidad, el código del demonio en sí */

/* Cuando bifurcas un demonio, aparece un demonio hijo */

Eso es todo. Creo que esto es suficiente para crear un servidor de socket simple.




Arriba