Crea tu propio chat – Servidor

Publicado por Alejandro Escario en

Hasta ahora hemos estado hablando de funcionalidades genéricas del chat, requisitos, …pues bien hoy vamos a analizar todos los ficheros de los que se compone el servidor y no han sido comentados con anterioridad; al igual que en el artículo anterior, vamos a comentar el cometido de cada una de las funciones de manera genérica, y únicamente nos adentraremos en el contenido de estas en la función de manejo de los sockets y en las que tengan alguna curiosidad que comentar.

Database.c

Éste es un archivo de código fuente que sólo será incluido en el programa del servidor (en realidad también va a ser incluido en el programa que nos ayudará a crear la base de datos desde cero, pero la importancia de ese ejecutable es mínima y la comentaremos más adelante.

En este caso hemos decidido hacer uso del sistema de bases de dados SQLite dado que es una base de datos ligera portable y con características más que suficientes para utilizarla en nuestra pequeña aplicación.

sqlite3* db_open(char*);

El cometido de esta función es simple, simplemente abrirá la base de datos situada en el fichero que se le ha de pasar como argumento.

int db_prepare(sqlite3**);

Esta función no se utilizará en el servidor, pero será la que utilizará la aplicación que creará la base de datos en caso de que esta no exista, y su única función es la de crear las tablas necesarias en la base de datos así como el usuario administrador del chat.

int db_exec(sqlite3**, char*, int);

Esta función se encargará de ejecutar una sentencia SQL y de actuar en consecuencia dependiendo del flag que se le pase como último argumento.

int db_userExists(char*, sqlite3**);

Esta función comprueba si un usuario existe en la base de datos.

int db_checkUser(char*, char*, sqlite3**);

Esta función comprueba si un usuario asociado a una contraseña existe en la base de datos.

int db_addUser(char*, char*, int, sqlite3**);

Crea un nuevo usuario.

int db_deleteUser(char*, sqlite3**);

Elimina un usuario de la base de datos.

int db_listUser(int, sqlite3**);

Lista los usuarios en la base de datos

void db_close(sqlite3**);

Cierra la base de datos y todos los descriptores asociados a ella.

int db_addLog(sms, sqlite3**);

Añade a la tabla de logs un mensaje.

int db_getLog(int, sqlite3**);

Lista el contenido del log de la base de datos

int db_getLogPar(int, sqlite3**, char*);

Tiene un comportamiento similar al de la función anterior, sólo que esta admite parámetros para personalizar la búsqueda, como por ejemplo buscar entre dos fechas dadas.

static int callback(void*, int, char**, char**);

Función que será llamada para cada una de las filas obtenidas de una sentencia SELECT.

servFunctions.c

Este fichero es el que contiene las principales funciones del servidor de nuestro chat, en este caso el comentario de cada función está situado justo encima de su definición en un comentario.

// indica la siguiente posición libre del array de conexiones
 void nextPos(user*, int*, int*, int);
 // hace un broadcast de 'sms'
 void broadcast(user*, int*, sms, int, sqlite3*);
 // hace un multicast de 'sms'
 void multicast(user*, int*, sms, int, sqlite3*, int room);
 // autenticación
 void auth(user*, int*, int, sms, sqlite3*, room*, int);
 // autenticación con contraseña
 void authPassword(user*, int*, int, sms, sqlite3*, room*, int);
 // chequea si un nombre e usurio está en uso
 int checkName(user*, int, char*);
 // busca una conexión en el array dado un socket
 int searchConn(user*, int, int);
 // cierra una conexión dado un socket
 int closeConn(user*, int*, int, int, fd_set*, sqlite3*);
 // ejecuta el comando indicado en el servidor
 int execParams(user*, int, char*, int, int, sqlite3*, room*, int);
 //cierra todas las conexiones
 void closeAll(user*, int*);
 // enviando mesaje privado
 void mp(user*, int*, sms, int, sqlite3*);
 // buscar socket por nombre
 int searchName(user*, int, char*);
 // listar salas de chat
 void sendRoomList(SSL*, room*, int);
 // selección de una sala de chat
 void roomCheckIn(user*, int*, int, sms, sqlite3*, room*, int);
 // buscador de salas en el array
 int searchRoom(char*, room*, int);
 // añadimos una sala
 int addRoom(char*, room*, int);
 // borramos una sala
 int deleteRoom(char*, room*, int);
 // movemos todos los usuarios de una sala a otra
 int moveAllTo(user*, int*, char*, room*, int, sqlite3*, char*);

La función más extensa de este archivo es la función:

int execParams(user*, int, char*, int, int, sqlite3*, room*, int);

Esta función se encargad de, una vez comprobado que el usuario en cuestión tiene permisos para darle órdenes al servidor, analiza la petición y la ejecuta comparando el string que recibe en el campo del mensaje de la estructura sms que vimos recientemente.

serv5.c

El archivo serv5.c es uno de los pocos que vamos a analizar por completo, y es que en él podremos ver un ejemplo de servidor en c utilizando SSL mientras nos apoyamos en la librería openssl.h; a pesar de que voy a exponer todo el código de este archivo, este lo voy a poner en partes mientras añado comentarios entre medias para mejorar su comprehensión. Si quieres ejecutarlo para ver como se comporta, te recomiendo que te dirijas al primer artículo de la «saga» y te descargues el proyecto completo en lugar de copiar y pegar del código que voy a poner a continuación, pero como dicen por ahí, eres libre de hacer lo que quieras siempre y cuando cumplas con los términos de la licencia.

/**
 * @File: serv5.c
 * @Description: implements the server
 * @Group: 1
 * @Members:    Alejandro Escario Méndez
 */

#include <stdio.h>
#include <stdlib.h>
// librería para manejo de strings
#include <string.h>
// librería para el uso de los sockets y las correspondientes constantes
#include <sys/socket.h>
// librería para el uso de la constante IPPROTO_TCP, in_addr, ...
#include <netinet/in.h>
// librería para el uso de primitivas unix
#include <unistd.h>
// librería que nos permite hacer uso de la variable errno
#include <errno.h>
// librería para mostrar la traza del programa
#include "trace.h"
// librería para gestionar los paquetes enviados y recibidos
#include "sms.h"
// librería para gestionar las conexiones
#include "socket.h"
// librería con las flags que utilizaremos
#include "flags.h"
// librería con las funciones del servidor
#include "servFunctions.h"
// incuimos la librería de tipos de datos
#include "type.h"
// incluimos la librería que va a controlar la base de datos
#include "database.h"
// incluimos la librería que va a convertir el proceso en un demonio
#include "tools.h"
// incluimos las librerias de ssl
#include <openssl/ssl.h>
#include <openssl/err.h>

//tamaño de la cola de peticiones
#define Q_DIM 5 // es el valor recomendado por defecto

#define DIM 1024

Incluimos las librerías necesarias para una compilación correcta del programa y definimos un par de constantes, Q_DIM contiene el número de usuarios que puede haber en la cola de conexiones a ser aceptadas mientras que DIM nos servirá, simplemente para hacer que todos nuestros arrays de texto tengan las mismas dimensiones.

int main(int argc, char** argv){

 int sock = 0; // declaración del socket e inicializado a 0
 int error = 0; /** declaramos una variable que nos servirá para detectar
 * errores
 */
 socklen_t length = (socklen_t) sizeof (struct sockaddr_in); // tamaño del paquete
 struct sockaddr_in addr; // definimos el contenedor de la dirección
 unsigned int port = 5678; /** creamos la variable que identifica el puerto
 * de conexión, siendo el puerto por defecto 5678
 */
 int connPos = 0; // primera posición libre en el array de conexiones
 int connTam = 10; // tamaño actual del array de conexiones
 int connGrow = 10; // factor de crecimiento del array
 user* conn = NULL; // array de conexiones con los clientes
 room rooms[DIM];
 user auxConn;   // conexion auxiliar
 sms auxMsj;
 fd_set connList, connListCopy; // definimos un descriptor que contendrá nuestros sockets
 int nbytes = 0; // contador de bytes leidos y escritos
 int dbName = 0; // variable que nos permitirá configurar el nombre de la base de datos
 sqlite3* db = NULL; // descriptor de la base de datos

 char cert[DIM] = "cert"; // nombre del certificado del servidor
 char pkey[DIM] = "pkey"; // nombre del archivo con la clave privada

Declaramos una serie de variables que utilizaremos para hacer que el servidor funcione como deseemos.

La funcionalidad de cada una de las variables está más o menos explicada en los comentarios, aunque si tienes alguna duda, no dudes en preguntar.

 //analizamos los parámetros de entrada
 int i = 0;
 for(; i < argc; i++){
 if(strcmp(argv[i], "-p") == 0){ // leemos el puerto
 if(argc <= i + 1 || isNum(argv[i+1]) == 0){
 perror("Se esperaba un número después de -p");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó un puerton");
 i++;
 port = atoi(argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-ls") == 0){ // leemos el tamaño inicial de la lista
 if(argc <= i + 1 || isNum(argv[i+1]) == 0){
 perror("Se esperaba un número después de -ls");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó un tamaño inicialn");
 i++;
 connTam = atoi(argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-lg") == 0){ // leemos el factor de creciemiento de la lista de conexiones
 if(argc <= i + 1 || isNum(argv[i+1]) == 0){
 perror("Se esperaba un número después de -lgn");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó un crecimienton");
 i++;
 connGrow = atoi(argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-db") == 0){ // leemos el nombre de la base de datos que queremos utilizar
 if(argc <= i + 1){
 perror("Se esperaba una cadena depués de -dbn");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó un crecimienton");
 i++;
 dbName = i;
 }
 continue;
 }else if(strcmp(argv[i], "-cert") == 0){ // leemos el nombre del archivo del certificado
 if(argc <= i + 1){
 perror("Se esperaba una cadena depués de -certn");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó un certificadon");
 i++;
 strcpy(cert, argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-pkey") == 0){ // leemos el nombre del archivo de que contiene la clave privada
 if(argc <= i + 1){
 perror("Se esperaba una cadena depués de -pkeyn");
 exit(-1);
 }else{
 PDEBUG("ARGS: Se detectó una clave privadan");
 i++;
 strcpy(pkey, argv[i]);
 }
 continue;
 }
 }
 

Analizamos los parámetros de entrada, la forma en la cual lo analizamos es un poco burda pero funciona que a fin de cuentas es lo que nos interesa en este manual. Para una mayor especificación del comportamiento de cada uno de los comandos puedes dirigirte al archivo README y que cuyo contenido ya hemos comentado con anterioridad.

db = db_open(
 (dbName == 0) ? "chat.db" : argv[dbName]
 );

Creamos la conexión con la base de datos, la cual se llamará de una manera u otra en función de los parámetros que le hayamos pasado al ejecutable

PDEBUG("INFO: Convertimos el proceso en un demonion");
 make_daemon();

Creamos un demonio según el concepto Unix, esta función puedes analizarla en el post anterior en el archivo tools.c.

 /*******************************SSL****************************************/
 PDEBUG("INFO: Inicializando la libreria SSLn");
 SSL_library_init();
 PDEBUG("INFO: Cargamos los algoritmos SSL y los mensajes de errorn");
 OpenSSL_add_all_algorithms();
 SSL_load_error_strings();
 PDEBUG("INFO: Seleccionamos SSLv2, SSLv3 y TLSv1n");
 SSL_METHOD *method;
 method = SSLv23_server_method();
 PDEBUG("INFO: Creamos el nuevo contexton");
 SSL_CTX *ctx;
 ctx = SSL_CTX_new(method);
 if(ctx == NULL) { // error
 ERR_print_errors_fp(stderr);
 _exit(-1);
 }

PDEBUG("INFO: Comprobando el certificadon");
 if ( SSL_CTX_use_certificate_chain_file(ctx, cert) <= 0) {
 ERR_print_errors_fp(stderr);
 _exit(-1);
 }

PDEBUG("INFO: Comprobando la clav eprivadan");
 if ( SSL_CTX_use_PrivateKey_file(ctx, pkey, SSL_FILETYPE_PEM) <= 0) {
 ERR_print_errors_fp(stderr);
 _exit(-1);
 }

PDEBUG("INFO: Comprobando que las claves pueden trabajar juntasn");
 if ( !SSL_CTX_check_private_key(ctx) ) {
 fprintf(stderr, "Clave privada incorrecta.n");
 _exit(-1);
 }

/*******************************SSL****************************************/
 //Creamos el socket
 PDEBUG("INFO: Creando el socketn");
 sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
 //Comprobamos si ha ocurrido un error al crear el socket
 if(sock < 0){
 write(2, strcat("ERROR: creación del socket {{socket()}}: %sn", strerror(errno)), DIM);
 // terminamos la ejecución del programa
 exit(-1);
 }

PDEBUG("INFO: Estableciendo el puerto, origenes,...n");
 addr.sin_family = AF_INET; // familia AF_INET
 addr.sin_port = htons(port); // definimos el puerto de conexión
 addr.sin_addr.s_addr = htonl(INADDR_ANY); // permitimos conexion de cualquiera

/* hacemos este "apaño" porque según hemos leido, http://www.wlug.org.nz/EADDRINUSE
 * hay un timeout para liberar el socket
 */
 unsigned int opt = 1;
 if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))==-1) {
 write(2, "ERROR: al permitir la reutiización del puerto {{setsockopt()}}n", DIM);
 exit(-1);
 }

// le asignamos una dirección al socket
 PDEBUG("INFO: Asignando una dirección al socketn");
 error = bind(sock, (struct sockaddr *)&addr, length);
 //Comprobamos si ha ocurrido un error al hacer el bind
 if(error < 0){
 write(2, strcat("ERROR: {{bind()}}: %sn", strerror(errno)), DIM);
 // terminamos la ejecución del programa
 exit(-1);
 }

//Ponemos el servidor a escuchar para buscar nuevas conexiones
 PDEBUG("INFO: Comenzamos la escucha de l programan");
 error = listen(sock, Q_DIM);
 //Comprobamos si ha ocurrido un error al ponernos a escuchar
 if(error < 0){
 write(2, strcat("ERROR: al iniciar la escucha{{listen()}}: %sn", strerror(errno)), DIM);
 // terminamos la ejecución del programa
 exit(-1);
 }

En este último bloque de código, lo que hemos hecho es, a grandes rasgos, inicializar las librerías SSL, asignarle un certificado al servidor para que este pueda identificarse y que los clientes puedan comunicarse con él, hemos creado la conexión y le hemos asignado un puerto para la escucha de nuevas conexiones y de los mensajes de los clientes.

// realizamos la asignación inicial de memoria
 PDEBUG("INFO: Realizando asignación inicial de memoria, tamaño inicial 10n");
 connTam = 10;
 conn = malloc(connTam * sizeof(user));
 // rellenamos el array con -1
 memset(conn, 0, connTam * sizeof(user));

Creamos y asignamos la memoria necesaria para mantener un listado de todos los clientes conectados al servidor.

//inicializamos la lista de conexiones
 FD_ZERO(&connList);
 // Inicio del bit descriptor connList con el valor de sock
 FD_SET (sock, &connList);

 PDEBUG("INFO: Creamos la sala de chat generaln");
 bzero(rooms, DIM * sizeof(room));
 strcpy(rooms[0].name, "general");

Creamos el multiplexor gracias al cual, apoyándonos en la función select(), seremos capaces de hacer escuchas de más de un descriptor de fichero ( en este caso sockets ) con un único proceso y sin tener que preocuparnos de hacer pulling.

//comenzamos a analizar conexiones
 PDEBUG("INFO: Comenzamos a analizar los socketsn");
 while(1){
 // hacemos una copia de seguridad para asegurarnos de no perder los datos
 connListCopy = connList;
 // ¿Hay algún socket listo para leer?
 PDEBUG("INFO: ¿Hay algún socket listo para leer?n");
 error = select(connTam + 1, &connListCopy, NULL, NULL, NULL);
 //Comprobamos si ha ocurrido un error al ponernos a escuchar
 if(error < 0){
 write(2, strcat("ERROR: al realizar la selección {{select()}}: %sn"
 , strerror(errno)), DIM);
 // terminamos la ejecución del programa
 exit(-1);
 }

Llamamos a la función select(); para ver si hay algún descriptor de fichero listo para lectura ( también se puede ejecutar select(); para ver si hay algún descriptor de fichero listo para la escritura de datos en él, así como de otro tipo de descriptores, para más detalles visita la documentación de la función y de tu sistema operativo.

// recorriendo los sockets para ver los que están activos
 PDEBUG("INFO: recorriendo los sockets para ver los que están activosn");
 int i = 0; // definimos un índice
 for (; i <= connTam; i++){
 // este socket está preparado para leer los datos
 if(FD_ISSET(i, &connListCopy)){
 // vemos si el socket preparado para leer es el de aceptar peticiones
 if(i == sock){
 PDEBUG("INFO: Nuevo cliente detectado, comprobando...n");
 auxConn.sock = accept(sock, (struct sockaddr *) &addr, &length);
 if(auxConn.sock < 0){
 write(2, "ERROR: al realizar la aceptación {{accept()}}: %sn"
 , *strerror(errno));
 // terminamos la ejecución del programa
 exit(-1);
 }

Para cada socket, comprobamos si éste es uno de los que están listos para ser leídos

/************************SSL*******************************/
 PDEBUG("INFO: Creando conexion ssln");
 PDEBUG("INFO: Creando conexion SSLn");
 auxConn.ssl = SSL_new(ctx);
 PDEBUG("INFO: Asignando la conexión a SSLn");
 SSL_set_fd(auxConn.ssl, auxConn.sock);
 PDEBUG("INFO: Aceptando la conexión SSLn");
 error = SSL_accept(auxConn.ssl);
 if(error < 0){
 ERR_print_errors_fp(stderr);
 exit(-1);
 }
 /************************SSL*******************************/

 PDEBUG("INFO: Conexión establecida, autenticando...n");

 memset(&auxMsj, 0, sizeof(auxMsj)); // incializamos la estructura

 PDEBUG("INFO: Solicitando autenticaciónn");
 strcpy(auxMsj.text, "Usuario: "); // establecemos el texto que queremos que se muestre
 auxMsj.flag = REQ_TEXT;  // le indicamos que requerimos una respuesta con texto
 strcpy(auxMsj.name, SERVER); // nos identificamos como el servidor
 SSL_write(auxConn.ssl, &auxMsj, sizeof(sms)); // enviamos la información

 // metemos los datos de la conexión en nuestro array de conexiones
 strcpy((*(conn + connPos)).name, auxMsj.text);
 (*(conn + connPos)).sock = auxConn.sock;
 (*(conn + connPos)).ssl = auxConn.ssl;
 (*(conn + connPos)).prov = PROV;

 // Añadimos el socket a nuestra lista
 PDEBUG("INFO: Insertando socket en la lista de monitoreon");
 FD_SET (auxConn.sock, &connList);

 // como la peticion se ha aceptado incrementamos el contador de conexiones
 PDEBUG("INFO: Cálculo del nuevo offsetn");
 nextPos(conn, &connPos, &connTam, connGrow);

Si el socket que está listo para la lectura es el que hemos creado al levantar el servidor, quiere decir que hay un cliente que quiere conectarse, por lo que aceptamos la petición, añadimos el socket del cliente a la lista de clientes y le enviamos un mensaje de bienvenida a través de SSL.

}else{ // si no, es un cliente ya registrado

 PDEBUG("DATA: Nuevo mensaje detectadon");
 nbytes = SSL_read((*(conn+searchConn(conn, connTam, i))).ssl, &auxMsj, sizeof(sms));
 if(nbytes > 0){ // si hemos leido más d eun byte...

 switch(auxMsj.flag){

 case CLI_EXIT: // desconexión del cliente
 closeConn(conn, &connPos, connTam, i, &connList, db);
 break;
 case SERV_ADMIN: // parámetros que ha de ejecutr el servidor
 execParams(conn, connTam, auxMsj.text, i, sock, db, rooms, DIM);
 break;
 case MSJ:  // mensaje
 multicast(conn, &connTam, auxMsj, i, db,
 (*(conn+searchConn(conn, connTam, i))).room);
 break;
 case REQ_AUTH: // vamos a leer el nombre de usuario
 auth(conn, &connTam, i, auxMsj, db, rooms, DIM);
 break;
 case CHECK_ROOM: // vamos a leer el nombre de usuario
 roomCheckIn(conn, &connTam, i, auxMsj, db, rooms, DIM);
 break;
 case CHECK_PASS:
 authPassword(conn, &connTam, i, auxMsj, db, rooms, DIM);
 break;
 case MP:
 mp(conn, &connTam, auxMsj, i, db);
 break;
 default:
 write(2, "ERROR: Recibido un mensaje mal formadon", 39);
 break;

 }

Si el valor del descriptor no es el que se corresponde con el del socket del servidor, quiere decir que es un cliente el que nos está hablando, por lo que hemos de analizar que es lo que quiere hacer (en función del flag que nos haya llegado en el mensaje) y actuar en consecuencia.

}else{ // hemos detectado una desconexión por el cerrado de la conexión

 closeConn(conn, &connPos, connTam, i, &connList, db);

 }
 }
 }
 }
 }

 return 0;
}

Por último, en caso de que recibamos un número inferior a 0 quiere decir que ha habido un error con la conexión por lo que el socket que estamos analizando se ha caído y podemos darlo de baja en nuestra lista de clientes activos.

Recuerda que si quieres echarle un vistazo al código completo del chat (tanto cliente como servidor), puedes dirigirte al primer artículo de la serie de artículos que tratan el tema del chat.