El trabajo práctico tiene como objetivo dominar el manejo de sockets, archivos y cadenas en C. Para ello se deberá implementar parte del protocolo DBUS (Desktop Bus) que comunica procesos dentro del sistema operativo. Además, se deben conseguir implementaciones abstractas y encapsuladas.
Los comandos serán leídos desde un archivo cuya ruta fue pasada por parámetro (o del stdin
si ninguno fue especificado). Cada comando se encuentra en una línea distinta, es decir están separados por el carácter \n
o salto de línea. Un primer acercamiento para poder obtener el comando es: leer el comando carácter por carácter hasta toparse con el salto de línea. En ese caso, copiar esa información a un buffer y luego transmitirla con el protocolo. Sin embargo, esto no es eficiente, como la función read
para leer de un archivo, es una función costosa, como el resto de las syscalls
, se espera ejecutarla el menor número de veces posibles.
Otra manera de encarar el problema, reduciendo las llamadas a read
, es leer de más de un byte, en este caso se elije leer 32 bytes. Esos 32 bytes se almacenará en un buffer estático (siempre tiene espacio para 32 bytes), una vez leído, se analiza si se llega al final de línea. En el caso de que no se haya encontrado, se almacenan esos 32 bytes en un buffer dinámico (este irá creciendo a medida que se agregan caracteres, en esta implementación, que se puede encontrar en common_dinamicbuffer
, con un factor REDIM_PROP = 1.5
) y se sigue leyendo. Caso contrario, se encontró el carácter de salto de línea:
\n
en el buffer dinámico.En la siguiente iteración, si hay algo en el segundo buffer estático, se almacena su contenido en el dinámico y se sigue con el bucle.
El encargado de manejar la correcta lectura del comando será el TDA common_commandlist
e interactuará con los buffers de la siguiente manera:
Una vez obtenido un comando, el mismo se codifica según el protocolo y se envía al servidor.
En el trabajo se implementó una librería dedicada exclusivamente a los sockets (common_socket
), la cual fue diseñada para que esté abstraída de este tp en particular y pueda ser usada en futuras oportunidades.
La misma cuenta con funciones genéricas: constructor y destructor, una función para recibir y otra para enviar, funciones para la conexión tanto del cliente (connect
), como para el servidor (accept
, bind_and_listen
).
Este TDA, permite abstraerse de las consideraciones que hay que tener al momento de trabajar con sockets: resolver nombres de dominio, resolver nombres de servicios, intentar conectarse por múltiples caminos (hasta establecer la conexión), enviar el mensaje la cantidad de veces necesarias para que el mensaje enviado sea el correcto.
Como una capa mayor de abstracción, se implementaron los TDA client (clientlib
) y de server (serverlib
), evitando así trabajar directamente con el TDA socket
.
El protocolo brinda una serie de reglas que al momento de codificar las mensajes que serán intercambiados entre los procesos. El mismo cuanta con un header
y un body
.
El header
contiene:
En los primeros 4 bytes, información del protocolo y el endianness del mensaje (esto es, si es little endian o big endian). En este tp se trató completamete la información en little endian (aunque el programa es compatible en ambas endianness).
Ejemplo de de primeros cuatro bytes del header
:
|6C 01 00 01 | | |l... |
[Aclaración: En el panel de la izquierda, se encuentra los bytes expresados en hexadecimal y a la derecha su correspondiente equivalente en ASCII (si el carácter no tiene significado en la explicación, se representa con '.'). Formato similar al de el editor hexed ].
Donde el primer 6C
corresponde al carácter en ASCII 'l'. Luego, 01 00 01
corresponden a llamada a método
, sin flags
y versión del 1 protocolo
respectivamente. Estos primeros bytes permanecerán constantes para esta implementación del protoco.
|6C 01 00 01 D6 00 00 00 | 06 00 00 00 A6 00 00 00 | |l................|
Los primeros 4 bytes fueron analizados previamente, los siguientes D6 00 00 00
expresa el número D616 (expresado en little endian) que equivale a 21410, esto indica que el cuerpo del mensaje (el que contiene los parámetros), tiene dicha longitud medida en bytes. Los siguientes 4 bytes o palabra 06 00 00 00
, se trata del id del mensaje, en este caso 616 = 610.
|06 01 73 00 12 00 00 00 | | |..s..... |
Donde el primer byte, 06
, indica que es un argumento de tipo Destino
, el caracter 's' (7316 en ASCII) indica que será un argumento de tipo cadena (para esta implementación siempre será el caso). Los siguientes 4 bytes mostrarán la longitud del argumento, en este caso 1216 = 1810. Luego de estas dos palabras, se encuentra el argumento en sí:
|06 01 73 00 12 00 00 00 | 74 61 6C 6C 65 72 2E 64 | |..s.....taller.d|
|62 75 73 2E 70 61 72 61 | 6D 73 00 00 00 00 00 00 | |bus.params......|
Como se ve, se completa con ceros hasta completar dos palabras (completar a 8 bytes), esto es un requerimiento que establece el protocolo y deben ser contados en la longitud del header
salvo para el último argumento.
Para los siguientes argumentos, Ruta
, Interfaz
y Método
el procedimiento es análogo. Finalmente, la Firma
, que es opcional, cambia ligeramente el formato, los primeros 4 bytes se forman de la misma manera, pero los siguientes bytes consisten en un byte indicando la cantidad de parametros que tiene el método y un carácter 's', por cada uno.
El body
cuenta con un estructura más simple, una palabra indicando la longitud del parámetro y el parámetro en cuestión finalizado con un carácter nulo.
El protocolo se encuentra más detallado en la documentación DBUS, o en el enunciado del tp.
De este análisis del protocolo, se tomaron las siguientes decisiones:
recv
de esos primeros bytes en un buffer estático y luego en base a esa información ya sé cuántos bytes tengo que recibir después, sin tener que esperar a que cierren el socket del otro lado. Además de poder reservar la memoria justa para almacenar los mensajes. Como se muestra en el siguiente diagrama:
Todos los campos fijos detallados anteriormente son de 8 bytes o múltiplos de 8, entonces, solamente debo alinear los argumentos y además no me tengo que fijar en qué posición estoy escribiendo porque puedo estar seguro que lo anterior tiene que estar alineado.
Todos los argumentos siguen el mismo patrón, con lo cual al momento de implementar un interprete (en el código server_dbusinterpreter
) la extracción de datos es bastante directa:
El manejo de sockets conlleva a tener varias consideraciones, en todo punto de la ejecución puede llegar a fallar, con lo cual leer las cosas lo antes posibles y liberar los recursos de red para evitar inconvenientes. En cuanto al protocolo DBUS, si bien el padding al momento de codificar no es trivial en un principio, luego para decodificar hace que la lectura sea más fácil, porque es más simple segmentar el mensaje. Durante el tp, se emplearon varias veces buffers, con lo cual fue un acierto implementarlo con un TDA, esto hace que sea más cómodo para utilizar abstrayendose de la implementación y el manejo de memoria directo.
Especificaciones DBUS: https://www.freedesktop.org/wiki/Software/dbus/
Tabla ASCII: https://www.rapidtables.com/code/text/ascii-table.html