Skip to content

Librería para implementar comunicación entre un servidor y cliente

Notifications You must be signed in to change notification settings

mauro-balades/socket-library-c

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Socket Library para C

Esta librería permite implementar la comunicación entre procesos con arquitectura cliente-servidor a través de sockets de un modo bastante simple.

Para comprender el funcionamiento de la librería y cómo funcionan los sockets en C, recomiendo esta guía la cual me ayudo a realizar esta librería.

Este repositorio cuenta con 2 proyectos los cuales son el cliente y el servidor, y con un proyecto shared library "Includes", donde se encuentra la libreria de socket.c la cual implementan tanto el cliente como el servidor. Además hay una librería util.c, la cual contiene un ejemplo de cómo realizar serialización y un método para implementar sincronización bastante simple.

Lógica de funcionamiento

El funcionamiento es algo similar a la arquitectura SOA, en donde tanto el servidor y cliente van a tener declaradas funciones, las cuales podrán ser llamadas por los procesos que estén conectados. Es decir, el cliente va a poder llamar a las funciones del servidor y el servidor va a poder llamar a las funciones del cliente. Claramente estas funciones no tienen un return, para eso la misma función si tiene que devolverle un resultado al proceso que lo llamo, tiene que hacer llamado a una función con el mismo procedimiento. (En la arquitectura SOA, solo el cliente "consume" del servidor "servicios" (no al revés) y estos si tienen un return; sería interesante modificar la librería para que permita un "return", en algún futuro con tiempo lo voy a desarrollar).

En el ejemplo de este proyecto, se inicializa un proceso servidor, el cual se pone a la escucha definiendo una única función client_saludar() (la cual podrá ser llamada por los clientes que se conecten). Luego se inicializan los clientes, los cuales se conectan al proceso anteriormente inicializado definiendo una función server_saludar() (la cual podrá ser llamada por el servidor). Una vez que un cliente se conecta llama a la función client_saludar del servidor, la cual recibe 3 parámetros. El servidor al recibir esto lo imprime por pantalla y a continuación llama a la función server_saludar del cliente que recibe 2 parámetros. El cliente imprime lo que le envía el servidor, y vuelve a llamar a la función saludar del servidor. Esto provoca un bucle infinito de saludos mutuos.

Para comprender 100% a utilizar esta librería recomiendo modificar este ejemplo haciendo un chat.

Procedimiento para realizar la prueba

  • Importar al eclipse los 3 proyectos.
  • Instalar la Commons Library y configurarla en los 3 proyectos.
  • Configurar en los 3 proyectos la librería pthread.
  • Configurar la shared library. (Video paso a paso youtube)
  • Compilar la shared library (entrar en socket.c o útil.c y compilar), el cliente y el servidor.
  • Finalmente se ejecutar una instancia de servidor y una o varias instancias de clientes.

Funciones

El proceso que se quiera poner a la escucha utiliza la función:
createListen(portServer, &newClient, fns, &client_connectionClosed, data);
portServer = puerto en el que se pone a la escucha.
newClient = referencia a función que se quiere que se llame cuando se conecta un proceso. (puede ingresarse NULL para que no se llame ninguna función).
fns = referencias de funciones del proceso que los otros procesos conectados van a poder llamar.
client_connectionClosed = referencia a función que se quiere que se llame cuando se desconecta un proceso. (puede ingresarse NULL para que no se llame ninguna función).
data = estructura de datos que se quiere mantener compartida entre las funciones del proceso.

Devuelve 1 en caso de éxito, caso contrario devuelve -1.

El proceso que se quiere conectar al que está en escucha utiliza:
connectServer(ip, port, fns, &server_connectionClosed, data);
IP = IP del proceso a conectarse
Port = Puerto del proceso a conectarse
fns = referencias de funciones del proceso que los otros procesos conectados van a poder llamar
server_connectionClosed = referencia a función que se quiere que se llame cuando se desconecta un proceso. (puede ingresarse NULL para que no se llame ninguna función).
data = estructura de datos que se quiere mantener compartida entre las funciones del proceso. (puede ser NULL)

Devuelve el número de socket de la conexión.

Para que un proceso ejecute el proceso de otro proceso se utiliza la función:
runFunction(socket_server, nombre_funcion, N, arg1, arg2, arg3, ..., argN);
socket_server = Numero de socket del proceso al que se le quiere correr la función.
nombre_funcion = nombre de la función que se quiere llamar. (este string deberá coincidir con el string declarado en el diccionario de funciones del proceso receptor).
N = Número de argumentos que se envian a la funcion a llamar.
argN = Argumentos que se envian a la funcion a llamar.

Devuelve true en caso de éxito, caso contrario devuelve false.

Para finalizar la conexión con otro proceso:
close(socket);
socket = Numero de socket destinado al proceso que se quiere finalizar

Estructura 'fns'

Tanto las funciones createListen() como connectServer() necesitan recibir una lista de todas las funciones que podrán ser llamadas desde otros procesos con la función runFunction(). Esta lista esta hecha con la estructura “t_dictionary” de la Commons library.

Se debe inicializar de la siguiente manera:
t_dictionary * fns;
fns = dictionary_create();

Luego se agrega todas las funciones que necesitemos de la siguiente manera:
dictionary_put(fns, "miFuncion1", &miFuncion1);
dictionary_put(fns, "miFuncion2", &miFuncion2);
.
.
dictionary_put(fns, "miFuncion2", &miFuncionN);

El &nombreDeUnaFuncion lo que hace es traer el puntero a la referencia de la función, gracias a esto la librería socket internamente va poder llamarla cuando sea necesario.

Las funciones “miFuncionN” son funciones que tienen que estar desarrolladas dentro del proyecto, las cuales reciben de forma obligatoria 2 parámetros:

  • socket_connection * connection
  • char ** args

La estructura connection contiene los siguientes campos:
int socket = Número de socket asignado por el sistema operativo a la conexión con el proceso que llamo a esta función. char * ip = IP del proceso que llamo a esta función.
int port = Puerto del proceso que llamo a esta función.
void * data = Estructura de datos que se mantiene durante toda la sesión de conexión con el proceso que llamo a esta función.
bool run_fn_connectionClosed = Determina si al finalizar la conexión con el proceso que lo llamo se tiene que llamar a la función connectionClosed (ya explicada) en caso de que exista.

La estructura args contiene un array de strings los cuales serían los “parametros” de esta función, que fueron trazados en el proceso que llamo a esta función a través de la función runFunction().

Veamos un ejemplo: el proceso A ejecuta la siguiente instrucción:
runFunction(numero_socket_procesoB, "procesoB_saludar", 3, "hola", "como", "estas");

A continuación en el proceso B se produce una invocación a la función procesoB_saludar(), el array args[] contendrá la siguiente información:
args[0] = “hola”
args[1] = “como”
args[2] = “estas”

¿Se puede enviar otra cosa que no sean strings? La respuesta es NO, para eso uno tiene que parsear el string de acuerdo a la necesidad que tengamos. Si el parámetro args[0] tendría que recibir un entero, por ejemplo el número 4, en realidad recibiría un “4” como string y habría que convertirlo a entero a través del método atoi() y desde el lado del proceso que lo llamo a la función habría que pasar de entero a string antes de pasar el dato a la función runFunction() con el método string_itoa() de la commons library (recordar que luego hay que realizar un free).

¿Cómo enviar un array? Para esto hay que serializar el array que se quiere enviar y luego en el proceso que lo recibe deserializarlo.

Estructura 'data'

La estructura data es simplemente para mantener a mano una estructura de datos por cada conexión. Veamos un ejemplo. El servidor lo que hace es sumar y multiplicar números que el cliente le va dando. Es decir que por cada cliente se tiene que tener 2 contadores uno para las sumas y otra para las multiplicaciones. Entonces la estructura tendría estos dos contadores y cada vez que un cliente llama a la función “tomaElNumerito” del servidor, dicha función ya tiene a mano la estructura del cliente para hacer:
data->contadorSuma = data->contadorSuma + Numero;
data->contadorMult = data->contadorMult * Numero;
Depende de lo que necesiten hacer los va a ayudar simplemente para reducir código. Ya que la segunda alternativa seria tener una lista de estas estructuras y buscar la estructura del cliente a través del número de socket o IP+PUERTO.

Hilos

La librería maneja de forma interna un hilo por cada nueva conexión.

Ejemplos:

  • 1 servidor con 10 clientes conectados, va a tener su hilo principal de ejecución y 10 hilos más (uno por cada cliente).
  • 1 cliente conectado a un servidor, va tener su hilo principal de ejecución y 1 hilo más por la conexión con el servidor.
  • 1 cliente conectado a 2 servidores distintos. Va tener su hilo principal de ejecución y 2 hilos más por cada conexión al servidor.

Tener en cuenta que si 2 clientes de forma simultanea llaman a una función del servidor, las mismas se van a ejecutar de forma paralela por lo que hay que implementar sincronización. Ahora bien si un cliente llama a una función del servidor y a continuación llama a otra del mismo servidor, la segunda función no se va a ejecutar hasta que no termine la primera.

Sincronización

En el archivo útil.c están las funciones necesarias para implementar sincronización a través del problema lectura-escritura
Es necesario inicializar el semáforo antes que nada.

About

Librería para implementar comunicación entre un servidor y cliente

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 83.3%
  • Makefile 16.7%