Charla no-charla de Sockets!

By | 12 abril, 2019

Hola a tod@s!

Como habrán notado el último sábado, no llegamos a dar la charla de sockets. No obstante, no queríamos dejarlos sin orientación alguna sobre este tema, por lo que aprovecharemos este espacio para dar lo que se habría explicado en la charla.

Entonces… qué es un Socket?

Antes de meternos de lleno a explicar que es un socket, queremos repasar unos conceptos previos, por ejemplo IPC.

IPC – Inter-Process Communication

IPC es un mecanismo el cual permite la comunicación y sincronización de acciones entre procesos de naturaleza cooperativa.

Esto nos permite, entre otras cosas compartir información. Además, nos da ventajas como el aumento de la velocidad de cómputo, modularidad y nos permite tener procesamiento distribuido y múltiples usuarios en un mismo sistema.

Esto nos es beneficioso para nuestro TP, al ser un sistema distribuido.

Tipos de IPC

Memoria Compartida

Múltiples procesos acceden al mismo bloque de memoria, lo que crea un buffer compartido para que dichos procesos se comuniquen entre ellos.

Es un problema, porque el sistema operativo no se entera de la interacción entre estos procesos. Y esto rompe con la estructura del SO como administrador del sistema. Además, el programador tiene que programar la forma de comunicación.

Intercambio de mensajes (Local)

En este tipo de IPC, dos procesos comparten información a través del Sistema Operativo, que provee un sistema de comunicación. Este intercambio de mensajes, se lleva a cabo localmente, por esto, restringe a los procesos a estar en el mismo entorno que el SO.

Es por dicho anteriormente, que dichas formas de IPC no nos sirven para implementarlas en el TP y por ende tenemos que recurrir a:

Sockets

Los sockets son mecanismos de comunicación entre procesos que permiten que un proceso hable ( emita o reciba información ) con otro proceso incluso estando en distintas máquinas. Es otra manera de realizar intercambio de mensajes, pero en red.

En linux, un socket (como todo en Linux) es un archivo, el cual se puede identificar con un «file descriptor» que no es más que un identificador que apunta a ese archivo. Pero no es cualquier tipo de archivo, es un archivo especial que necesita conectarse.

Nos permite implementar una arquitectura cliente -servidor, necesaria para este Trabajo Práctico.

La arquitectura cliente-servidor es un modelo de diseño de software en el que las tareas se reparten entre los proveedores de recursos o servicios, llamados servidores, y los demandantes, llamados clientes. Un cliente realiza peticiones a otro programa, el servidor, quien le da respuesta

Ejemplo Práctico

Imaginemos que queremos levantar un servidor, y que hace servidor, se quieren conectar varios clientes, para eso debemos:

  • Configurar el servidor
    • Crear la estructura de la dirección del servidor: para esto necesitamos usar las estructuras de sock_addr, con la info del servidor.
    • Crear un socket: para eso, basta con llamar a la función socket(), pasándole una serie de parámetros, explicados acá. Esa función nos devolverá el file descriptor del socket.
    • Asociar un puerto: esto quiere decir, reservarnos un puerto para nuestro socket, al cual los clientes podrán, junto con nuestra dirección, conectarse a nosotros. Para hacer asociarlo, sólo necesitamos llamar a la función bind().
    • Escuchar conexiones entrantes: Luego de asociar nuestro puerto, debemos quedarnos a la escucha de conexiones entrantes, para eso, utilizaremos la función listen().

      Y así es como, a grandes rasgos, levantamos un servidor listo para recibir nuevas conexiones de futuros clientes.
  • Configurar el cliente:
    • Crear un socket: Es igual que en el servidor, solo que necesitaremos los datos del servidor al que nos conectaremos.
  • Conectarnos al servidor: Desde el cliente, llamaremos a la función connect(), pasandole, entre otras cosas el socket que configuramos previamente, para conectarnos al servidor.

  • Aceptar conexiones entrantes: Desde el servidor, necesitamos aceptar la conexión entrante, usando la función accept(). Esta función crea un socket implícitamente, el cual utilizaremos cuando necesitemos responderle al cliente que se conectó en ese momento.

  • Enviar datos al servidor: Luego de habernos conectado, utilizaremos la función send() para enviar los datos al mismo, utilizando el socket que creamos para conectarnos.

  • Recibir los datos del cliente: Para recibir datos, utilizaremos, desde el lado del servidor, la función recv(). Cabe aclarar que esta función es bloqueante, por lo que una vez que la llamemos, el servidor quedará bloqueado a la espera de datos. Esto puede evitarse, usando hilos, select(), poll() (Estos dos últimos explicados más adelante) o haciendo que el socket sea no bloqueante.

Y esto es, a alto nivel, lo mínimo y esencial que necesitamos para implementar un cliente y un servidor utilizando sockets!

De todas maneras, tenemos un tutorial mas completo acá:

Sockets (parte 1): Crear un servidor – Link

Sockets (parte 2): Aceptar conexiones y send(…) – Link

Sockets (parte 3): Recv(…) y mensajes variables – Link (Si llegaste hasta acá, quizás te convenga leerte este issue en nuestro foro de github: Link )

Sockets (parte 4): Crear un cliente – Link

Ahora, si bien ya sabemos cómo comunicar distintos procesos, si no tenemos alguna suerte de «guía» que nos orienten para saber que mandar y que recibir, la comunicación se va a complicar. Por lo que necesitamos…

Protocolos

Un protocolo se puede definir como un conjunto de reglas y pautas para llevar a cabo una comunicación. Dichas reglas se definen en cada paso y/o proceso de comunicación entre dos o más computadoras. Las redes necesitan seguir estas reglas para transmitir datos de manera satisfactoria.

Para ser más claros, pasemos a un ejemplo práctico:

Imaginemos que el tablero en la imagen es en realidad un servidor, el cual se encarga de hacer los movimientos de nuestro personaje. Necesitamos definir qué movimientos puede hacer y cómo comunicarlo al servidor.

Entonces, utilizaremos el formato <tipoDeAccion> <contenidoDeLaAccion> para especificarle al servidor que hacer con nuestro personaje.

De esta manera, concluimos teniendo el siguiente conjunto de reglas:

Y así, es como definimos nuestro protocolo. Este protocolo nos va a permitir comunicarle al servidor que queremos mover a nuestro personaje, tomar un objeto (por ejemplo la espada) o salir por la puerta, y que el servidor nos entienda.

Ahora, cómo lo comunicamos en bytes, a fin de que el servidor lo reciba?

Podemos especificar que enviaremos, primero un uint8_t, el cual contendrá el tipo de acción y luego, un char para las direcciones, un string para el nombre del objeto a tomar, o nada si queremos salir.

Por ejemplo, si queremos mover nuestro jugador en alguna dirección, enviaremos al servidor los siguientes bytes:

En cambio, si queremos que el jugador en el servidor tome un objeto:

Y si nos encontramos en una salida, y queremos salir:

De esta manera, tenemos definido nuestro protocolo, por ende, el servidor sabrá que primero debe recibir el entero y dependiendo de lo que reciba, sabrá luego que más tiene que recibir y qué hacer con lo que recibe.

Para ir cerrando, en el trabajo práctico vamos a tener múltiples clientes para un mismo servidor. Ahora, cómo hacemos para que el servidor pueda administrar múltiples conexiones concurrentes y responder a cada una?

La multiplexación nos permite monitorear múltiples conexiones sobre el mismo socket, y poder manejarlos cada uno por separado, ser notificados si alguno tiene datos para procesar, y llevar a cabo operaciones con esos datos. Para multiplexar podemos utilizar dos alternativas, select o poll, aunque solo nos limitaremos al select.

Select

El select nos da la posibilidad de monitorear varios file descriptors de conexiones recibidas al mismo tiempo. Nos dice cuales están listos para ser leídos, o cuales para escribir, y si alguno devolvió un error.

Monitorea tres sets de file descriptors, uno de lectura, otro de escritura y otro de excepciones. Y para modificar dichos sets fds, utiliza una serie de funciones.

FD_SET =>  agrega un fd al set

FD_CLR ⇒ saca un fd del set

FD_ISSET ⇒ chequea si un fd está en el set.

FD_ZERO ⇒ vacía el set.

Y eso fue todo por ahora! Esperamos que les sirva para entender un poco más las implicancias de la comunicación entre procesos, y cómo llevarla a cabo.

Saludos!