Estructura de un socket - GandalFran/Sockets-de-berkeley GitHub Wiki
En los sockets, la estructura difiere según el rol que juegue nuestro programa (cliente o servidor) y el protocolo de transporte (TCP o UDP) sobre el que nos apoyemos. Hay dos imágenes muy descriptivas, que nos indican las llamadas que hay que hacer para implementar cada uno:
Comunicación TCP:
Comunicación UDP:
No obstante, dichas imágenes no estan completas, pues no nos indican qué parámetros usar para invocar cada una de estas funciones. Es debido a eso, que abajo se presentan la estructura que debe seguir tu codigo para poder inicializar un socket y comenzar a trabajar con él tanto en un cliente como en un servidor en TCP y UDP:
Socket TCP cliente
//Los valores de port y hostName, serían los del servidor con el que queremos conectar
int s;
int puerto = 80;
char hostName[] = "ejemplo.es";
struct addrinfo resolutionConfig, *resolutionData= NULL;
struct sockaddr_in serverData;
//Limpiar las estructuras que vamos a usar durante el programa
// NOTA: esto lo hacemos para evitar problemas por los campos que no rellenamos y dejamos por defecto
memset (&resolutionConfig, 0, sizeof (resolutionConfig));
memset (&serverData, 0, sizeof(serverData));
//Resolución de la dirección IP del host servidor
// NOTA: para realizar la conexión necesitamos la IP, no el nombre de dominio, por eso
// se realiza una resolución, para obtener dicha dirección a partir del nombre del servidor.
resolutionConfig.ai_family = AF_INET;
if(0 != getaddrinfo (hostName, NULL, &resolutionConfig, &resolutionData)){
exit(EXIT_FAILURE);
}
//Creación del socket
// Con esta operación no estamos conectando, sino solicitando a el sistema operativo un socket (al igual
// que se solicitan otros recursos como semaforos o buzones), en este caso de TCP (SOCK_STREAM), y para
// trabajar con IPv4 (AF_INET)
if(-1 == (s = socket (AF_INET, SOCK_STREAM, 0))){
exit(EXIT_FAILURE);
}
//Rellenar la estructura del servidor
// NOTA: en familia establecemos que trabajaremos con IPv4 (AF_INET). En la dirección, la IP del servidor
// con el que buscamos comunicarnos (está en la estructura con la que hicimos la resolución). Y por ultimo en
// el puerto, el puerto en el que se encuentra el servicio en el host servidor.
serverData.sin_family = AF_INET;
serverData.sin_addr = ((struct sockaddr_in *) resolutionData->ai_addr)->sin_addr;
serverData.sin_port = htons(port);
//Una vez rellena la estructura del servidor, podemos liberar la memoria
// utilizada para la resolución de su dirección IP
freeaddrinfo(resolutionData);
resolutionData = NULL;
//Ahora se establece la conexión con el servidor
// NOTA: aquí estamos estableciendo la conexion en si de TCP
// (Cliente: SYN -> Servidor: SYN,ACK -> Cliente: ACK), tras lo cual, se podrá comenzar
// a intercambiar mensajes.
if(-1 == connect(s, (const struct sockaddr *)&serverData, sizeof(serverData))){
exit(EXIT_FAILURE);
}
//A partir de aquí vendría la implementación del protocolo deseado
Socket UDP cliente
int s;
int puerto = 80;
char hostName[] = "ejemplo.es";
struct addrinfo resolutionConfig, *resolutionData= NULL;
struct sockaddr_in serverData, clientData;
//Limpiar las estructuras que vamos a usar durante el programa
// NOTA: esto lo hacemos para evitar problemas por los campos que no rellenamos y dejamos por defecto
memset (&resolutionConfig, 0, sizeof (resolutionConfig));
memset (&serverData, 0, sizeof(serverData));
memset (&clientData, 0, sizeof(clientData));
//Resolución de la dirección IP del host a conectar
// NOTA: para realizar la conexión necesitamos la IP, no el nombre de dominio, por eso necesitamos
// realizar una resolución, para obtener dicha dirección a partir del nombre del servidor.
resolutionConfig.ai_family = AF_INET;
if(0 != getaddrinfo (hostName, NULL, &resolutionConfig, &resolutionData)){
exit(EXIT_FAILURE);
}
//Creación del socket
// Con esta operación no estamos conectando, sino solicitando a el sistema operativo un socket (al igual
// que se solicitan otros recursos como semaforos o buzones), en este caso de UDP (SOCK_DGRAM), y para
// trabajar con IPv4 (AF_INET)
if(-1 == (s = socket (AF_INET, SOCK_DGRAM, 0))){
exit(EXIT_FAILURE);
}
//Rellenar la estructura del servidor
// NOTA: en familia establecemos que trabajaremos con IPv4 (AF_INET). En la dirección, la IP del servidor
// con el que buscamos comunicarnos (está en la estructura con la que hicimos la resolución). Y por ultimo en
// el puerto, el puerto en el que se encuentra el servicio en el host servidor.
serverData.sin_family = AF_INET;
serverData.sin_addr = ((struct sockaddr_in *) resolutionData->ai_addr)->sin_addr;
serverData.sin_port = htons(port);
//Una vez rellena la estructura del servidor, podemos liberar la memoria de la estructura utilizada para
// la resolución
freeaddrinfo(resolutionData);
resolutionData = NULL;
//Rellenar la estructura del cliente para poder hacer bind
// NOTA: en dirección ponemos la constante INADDR_ANY, para que al ejecutarlo en cualquier dispositivo,
// este campo se rellene con la IP del propio dispositivo, ya transformada a binario.
// NOTA2: en el puerto ponemos 0, para que el sistema operativo nos asigne un puerto libre, para realizar
// las comunicaciones (puerto efímero).
clientData.sin_family = AF_INET;
clientData.sin_addr = INADDR_ANY;
clientData.sin_port = 0;
//Bindear el socket a nuestra dirección
// NOTA: esto es para unir el socket a nuestra dirección IP, y que el sistema operativo nos asigne un
// puerto efímero.
if (bind(s, (const struct sockaddr *) &clientData, sizeof(clientData)) == -1) {
exit(EXIT_FAILURE);
}
//A partir de aquí vendría la implementación del protocolo deseado
Socket TCP servidor
A partir de aquí, entramos en terreno de servidores, por lo que se complica un poco más el funcionamiento. Esto es debido a que los servidores necesitan por lo general, recibir varias peticiones y tratarlas, muy a menudo de forma concurrente. En el caso del servidor de TCP, el mecanismo usado es crear un socket para recibir las peticiones (sListen), y cada peticion, tratarla en un socket por separado, generado en la funcion accept (para conocer mas sobre su funcionamiento ir a la pagina de Conceptos básicos sobre transporte y la biblioteca de sockets de berkeley).
Una práctica muy común en los servidores es, tras obtener el socket con el que se realizaran las comunicaciones (mediante la función accept, en el caso de TCP), es crear un proceso hijo, el cual atendera la peticion (esto se tratara en la seccion Sockets: Implementación, consejos y pasos recomendados).
Otro punto a tener en cuenta en los servidores, es que necesitan entrar en un bucle para recibir las peticiones y tratarlas, por lo cual, se suele declarar un bucle, sin condición de salida, para que el servidor atienda peticiones de continuo.
int sListen, s;
int port = 80;
int sockaddrSize;
struct sockaddr_in serverData, clientData;
//Limpiar las estructuras que vamos a usar durante el programa
// NOTA: esto lo hacemos para evitar problemas por los campos que no rellenamos y dejamos por defecto
memset (&clientData, 0, sizeof(clientData));
memset (&serverData, 0, sizeof(serverData));
//Creación del socket de escucha
// Con esta operación no estamos conectando, sino solicitando a el sistema operativo un socket (al igual
// que se solicitan otros recursos como semaforos o buzones), en este caso de UDP (SOCK_DGRAM), y para
// trabajar con IPv4 (AF_INET)
if(-1 == (sListen= socket (AF_INET, SOCK_STREAM, 0))){
exit(EXIT_FAILURE);
}
//Rellenar la estructura del servidor
// NOTA: en familia establecemos que trabajaremos con IPv4 (AF_INET), y en la dirección IP del servidor,
// establecemos INADDR_ANY, la cual es una dirección especial, que se corresponde con cualquiera,
// permitiéndonos no tener que hallar el valor de la dirección IP de nuestro servidor para configurar
// esta estructura.
// NOTA2: en puerto hemos puesto un valor determinado, y no 0, porque no queremos que el sistema operativo
// nos asigne un puerto disponible, sino que queremos escuchar en un puerto establecido por nosotros,
// para que los clientes conozcan con que puerto conectar.
serverData.sin_family = AF_INET;
serverData.sin_addr.s_addr = INADDR_ANY;
serverData.sin_port = htons(port);
//Bindear el socket a nuestra dirección
// NOTA: esto es para indicar a el sistema operativo, que este socket escucha por la IP y puerto que hemos indicado
if (-1 == bind(sListen, (const struct sockaddr *) &serverData, sizeof(serverData))) {
exit(EXIT_FAILURE);
}
//Creamos la cola de clientes en escucha
// NOTA: esto crea una cola, para que los clientes según van realizando peticiones, vayan siendo
// encolados, para ser atendidos posteriormente
// En este caso hemos establecido 5, que es el número máximo de clientes en espera, ya que solo se
// pueden asignar valores entre 1 y 5.
if (-1 == listen(sListen, 5)) {
exit(EXIT_FAILURE);
}
//A partir de aquí, ya hemos terminado de crear el socket de escucha para el servidor, y entramos en el
// bucle, en el cual iremos recibiendo y tratando las peticiones
while(1){
//Bloquearse hasta recibir una conexión
// NOTA: Con accept, nos bloqueamos hasta recibir una conexión de un cliente, y para comunicarnos
// con el, crea un nuevo socket ya preparado para realizar el intercambio de mensajes con el cliente.
// Tambien rellena clientData, con los datos del cliente para que podamos
// consultar dichos datos.
if(-1 == (s= accept(sListen, (struct sockaddr *) &clientData, &sockaddrSize))){
exit(EXIT_FAILURE);
}
//A partir de aquí vendría la implementación del protocolo deseado
}
Socket UDP servidor
En el caso de que no hayas leído el punto anterior (Socket TCP servidor), leelo antes de proceder con este, pues ahí se explican conceptos básicos para poder entender cómo es las estructura con la que implementar un servidor (tanto TCP como UDP).
Una vez dicho esto, hay que destacar una diferencia entre TCP y UDP, a la hora de tratar múltiples peticiones de forma concurrente. En TCP, con la función accept, nos quedamos bloqueados hasta que se establece una conexión, pero en UDP no existe tal función, porque no se mantiene sesion y por tanto no se puede establecer conexión.
Esto nos supone un problema a la hora de tratar múltiples peticiones, pues ahora no tenemos un socket de escucha para recibir peticiones, y un socket para tratar cada una, por lo que se nos podrían solapar mensajes de varios clientes durante el tratamiento de una petición. No obstante, tiene una facil solucion , la cual consiste en: tras recibir el primer mensaje, se crea un nuevo socket (en un puerto efímero), y responderemos a la peticion a través de ese socket, con lo cual habremos separado la "sesión mantenida desde el nivel aplicacion", para atender dicha petición, del socket de escucha de peticiones.
IMPORTANTE: esta solucion sera tratada mas a fondo en Sockets: Implementación, consejos y pasos recomendados.
int sListen;
int port = 80;
int sockaddrSize;
char buffer[512];
struct sockaddr_in serverData, clientData;
//Limpiar las estructuras que vamos a usar durante el programa
// NOTA: esto lo hacemos para evitar problemas por los campos que no rellenamos y dejamos por defecto
memset (&clientData, 0, sizeof(clientData));
memset (&serverData, 0, sizeof(serverData));
//Creación del socket
// Con esta operación no estamos conectando, sino solicitando a el sistema operativo un socket (al igual
// que se solicitan otros recursos como semaforos o buzones), en este caso de UDP (SOCK_DGRAM), y para
// trabajar con IPv4 (AF_INET).
if(-1 == (sListen= socket (AF_INET, SOCK_DGRAM, 0))){
exit(EXIT_FAILURE);
}
//Rellenar la estructura del servidor
// NOTA: en familia establecemos que trabajaremos con IPv4 (AF_INET), y en la dirección IP del servidor,
// establecemos INADDR_ANY, la cual es una macro, que indica que se le asigne la direccion IP de la
// maquina donde se encuentra el servidor.
// NOTA2: en puerto hemos puesto un valor determinado, y no 0, porque no queremos que el sistema operativo
// nos asigne un puerto disponible, sino que queremos escuchar en un puerto establecido por nosotros,
// para que los clientes sepan con que puerto conectar.
serverData.sin_family = AF_INET;
serverData.sin_addr.s_addr = INADDR_ANY;
serverData.sin_port = htons(port);
//Bindear el socket a nuestra dirección
// NOTA: esto es para indicar a el sistema operativo, que este socket escucha por la IP y puerto que hemos indicado
if (-1 == bind(sListen, (const struct sockaddr *) &serverData, sizeof(serverData))) {
exit(EXIT_FAILURE);
}
//En esta estructura, NO creamos una cola de escucha con listen, pues UDP no es orientado a
// a conexión, y por tanto, no mantiene una sesion en nivel de transporte, sino que se delega
// esta tarea a el nivel de aplicacion. Por tanto no se necesita una cola de clientes.
//A partir de aquí, ya hemos terminado de crear el socket de escucha para el servidor, y entramos en el
// bucle, en el cual iremos recibiendo y tratando las peticiones
while(1){
//Aquí nos bloqueamos hasta recibir un mensaje, ya que como en UDP no se mantiene
// sesión, no se espera a recibir una peticion de conexion, sino que directamente se recibe
// un mensaje.
// NOTA: aquí recibiremos en mensaje en un array de char llamado buffer, que tiene 512 elementos char,
// o lo que es lo mismo, 512 bytes. No obstante el tamaño del buffer, debe ser el tamaño máximo
// que necesite un mensaje de vuestro protocolo, para que os llegue el que os llegue, se pueda
// recibir por completo.
if(-1 == recvfrom(sListen, buffer, 512, 0, (struct sockaddr*)&clientData, &sockaddrSize)){
exit(EXIT_FAILURE);
}
//A partir de aquí vendría la implementación del protocolo deseado
}