Crea tu propio chat – Cliente

Publicado por Alejandro Escario en

Con este artículo termina la serie de artículos en las que describimos los principios básicos que hay que tener en cuenta para realizar un chat y en los que también se describe como hacer una pequeña aproximación a uno de estos programas que a muchos de nosotros nos acompañan en nuestro día a día.

En el artículo de hoy vamos a tratar el único tema que nos falta, el cual es el programa cliente que nos servirá a los usuarios para comunicarnos con el servidor y así, poder comunicarnos con el resto de usuarios que se encuentren conectados en un momento dado.

Si bien el cliente se basta de un sólo ejecutable, hoy vamos a tratar dicho ejecutable y otro que nos permita, mediante la redirección de descriptores de ficheros, añadirle una interfaz gráfica sencilla.

cli.c

Nos encontramos ante uno de los ficheros con la lógica esencial del cliente del chat, vamos a analizarlo paso a paso:


#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 que nos permite hacer uso de la variable errno
#include <errno.h>
// librería de base de datos de red
#include <netdb.h>
// librería para el uso de primitivas unix
#include <unistd.h>
// librería para obtener acceso a variables como pid
#include <sys/types.h>
// librería para el manejo de señales
#include <signal.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 con las flags que utilizaremos
#include "flags.h"
// incuimos la librería de tipos de datos
#include "type.h"
// incuimos la librería de herramientas para los echo
#include "tools.h"
// incluimos las librerías que nos permitirán hacer uso de SSL
#include "ssl.h"

#define DIM 1024 // definimos el tamaño de los array de textos

#define     READ    STDIN_FILENO
#define     WRITE    STDOUT_FILENO

Incluimos los archivos de cabecera o librerías necesarias para la ejecución del chat y realizamos las definiciones de tamaños, y ya puestos, definimos los descriptores de ficheros de entrada y salida con el nombre de las constantes genéricas en lugar de utilizar los números 0 para lectura y 1 para escritura.

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
 */
 int serverTalk = 0;
 //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
 */
 char dir[DIM] = "localhost"; /** definimos la cadena que contendrá a la
 * dirección del servidor, la cual, será por
 * defecto localhost
 */
 //struct hostent*  server; // estructura utilizada para la gestión de direcciones
 sms auxMsj;
 char name[DIM] = {0};
 int nbytes = 0; // contador de bytes leidos y escritos
 char aux[DIM] = {0};
 // inicializamos las variables de SSL
 BIO* bio = NULL;
 SSL_CTX* ctx = NULL;
 SSL* ssl = NULL;
 char cert[DIM] = "/usr/share/doc/libssl-dev/demos/sign/cert.pem";

Realizamos la declaración de las variables de las que vamos a hacer uso durante la ejecución del cliente, de manera que, al menos las que son genéricas las tenemos al inicio y si lo necesitamos, podemos encontrar fácilmente su definición.

//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("INFO: Puerto identificadon");
 i++;
 port = atoi(argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-d") == 0){ // dirección de destino
 if(argc <= i + 1){
 perror("Se esperaba una dirección después de -d");
 exit(-1);
 }else{
 PDEBUG("INFO: Destino identificado");
 i++;
 strcpy(dir, argv[i]);
 }
 continue;
 }else if(strcmp(argv[i], "-cert") == 0){ // dirección de destino
 if(argc <= i + 1){
 perror("Se esperaba una ruta de certificado después de -cert");
 exit(-1);
 }else{
 PDEBUG("INFO: Destino identificado");
 i++;
 strcpy(cert, argv[i]);
 }
 continue;
 }
 }

Analizamos los parámetros de entrada, para un mayor detalle de lo que hace cada uno de los parámetros de entrada, sería conveniente hacer una lectura del archivo README.


/***********************************SSL************************************/
 PDEBUG("INFO: Iniciamos la librería SSLn");
 SSL_load_error_strings(); // strings de error
 SSL_library_init(); // iniciams la libreria en sí
 ERR_load_BIO_strings(); // strings de error de BIO
 OpenSSL_add_all_algorithms(); // inicializamos los algoritmos de la librería

 PDEBUG("INFO: Conectando...n");
 PDEBUG("INFO: Inicializando los punterosn");
 ctx = SSL_CTX_new(SSLv23_client_method());
 ssl = NULL;
 PDEBUG("INFO: Cargamos el certificadon");
 if(SSL_CTX_load_verify_locations(ctx, cert, NULL) == 0){
 char aux[] = "ERROR: No se pudo comprobar el certificadon";
 write(WRITE, aux, strlen(aux));
 exit(-1);
 }
 PDEBUG("INFO: Inicializando BIOn");
 bio = BIO_new_ssl_connect(ctx);
 BIO_get_ssl(bio, &ssl);
 if(ssl == 0){
 char aux[] = "ERROR: Error al crear el objeto ssln";
 write(WRITE, aux, strlen(aux));
 exit(-1);
 }
 PDEBUG("INFO: Estableciendo el modo de trabajo, no queremos reintentosn");
 SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
 PDEBUG("INFO: Intentando realizar la conexiónn");
 PDEBUG("INFO: Conectando a -> ");
 sprintf(aux, "%s:%i", dir, port);
 PDEBUG(aux);PDEBUG("n");
 BIO_set_conn_hostname(bio, aux);
 PDEBUG("INFO: Verificando la conexiónn");
 if (BIO_do_connect(bio) < 1){
 char aux[] = "ERROR: al conectar el BIOn";
 write(WRITE, aux, strlen(aux));
 exit(-1);
 }

 PDEBUG("INFO: Conectadon");
 PDEBUG("INFO: Esperando mensaje de bienvenidan");

Comenzamos con la parte divertida de la película, llegados a este punto, y una vez que ya hemos leído los parámetros necesarios para la ejecución del chat, vamos a proceder a conectarnos al servidor mediante SSL, para lo cual iniciamos la librería openssl y más concretamente las funciones de SSL, creamos una nueva conexión apoyándonos en los certificados que tenemos e intentamos establecer la conexión entre el cliente y el servidor del chat proporcionando la dirección del servidor y el puerto en el cual está escuchando.


memset(&auxMsj, 0, sizeof(sms));
 if(client_read(bio, &auxMsj, sizeof(sms)) <= 0){
 PDEBUG("INFO: El socket está cerradon");
 perror("Error al leer contenido del socket porque está cerradon");
 perror("El cliente se pararán");
 exit(-1);
 }


 // atendemos la autentificación del cliente
 do{
 bzero(aux, DIM);
 sprintf(aux, "%s ~$> %sn", auxMsj.name, auxMsj.text);
 write(WRITE, aux, DIM);
 bzero(&auxMsj.text, SMS_LEN);

 if(auxMsj.flag == MSJ){ // si es un mensaje, solo imprimimos por la salida estandar

 memset(&auxMsj, 0, sizeof(sms));
 if(client_read(bio, &auxMsj, sizeof(sms)) <= 0){
 PDEBUG("INFO: El socket está cerradon");
 perror("Error al leer contenido del socket porque está cerradon");
 perror("El cliente se pararán");
 exit(-1);
 }
 continue;

 }

 if(auxMsj.flag == REQ_PASS){ // si es password desactivamos el echo
 echo_off();
 }
 nbytes = read(READ, &auxMsj.text, SMS_LEN);
 if(auxMsj.flag == REQ_PASS){// si es password activamos el echo
 echo_on();
 }
 auxMsj.text[nbytes - 1] = '\0'; // eliminamos el retorno de carro

 // nos salimos?
 if(strcmp(auxMsj.text, "-x") == 0){
 PDEBUG("EXIT: Cerrando el cliente, avisando al servidor...n");
 auxMsj.flag = CLI_EXIT;
 client_write(bio, &auxMsj, sizeof(sms));
 PDEBUG("EXIT: Cerrando el socketn");
 shutdown(sock, 2);
 PDEBUG("EXIT: Cerrando el clienten");
 exit(0);
 }else{ // es un mensaje
 strcpy(name, auxMsj.text); // hacemos una copia del nombre introducido para no perderlo
 if(auxMsj.flag == REQ_TEXT){
 auxMsj.flag = REQ_AUTH;
 }else if(auxMsj.flag == REQ_PASS){ // entonces REQ_PASS
 auxMsj.flag = CHECK_PASS;
 }else if(auxMsj.flag == REQ_ROOM){ // entonces REQ_ROOM
 auxMsj.flag = CHECK_ROOM;
 }
 client_write(bio, &auxMsj, sizeof(sms));
 memset(&auxMsj, 0, sizeof(sms));
 if(client_read(bio, &auxMsj, sizeof(sms)) <= 0){
 PDEBUG("INFO: El socket está cerradon");
 perror("Error al leer contenido del socket porque está cerradon");
 perror("El cliente se pararán");
 exit(-1);
 }
 }
 }while(auxMsj.flag != OK);

Tras recibir el mensaje de bienvenida del servidor, nos tocará autenticarnos, por lo que le enviamos al servidor nuestro usuario, la contraseña (sólo la pide si nos estamos intentando conectar con un usuario dado de alta en el chat y que por tanto es capaz de ejecutar órdenes en el servidor como apagarlo por ejemplo) y por último le indicamos en qué sala deseamos entrar para iniciar la conversación.

PDEBUG("INFO: Usuario conectadon");
 printf("Usuario conectado...n");

 fd_set desc, descCopy; // definimos un descriptor que contendrá nuestros descriptores
 //inicializamos la lista de conexiones
 FD_ZERO(&desc);
 // Inicio del bit descriptor sock con el valor de sock
 int fd;
 if(BIO_get_fd(bio, &fd) < 0){
 write(WRITE, "ERROR: crear le descriptor %sn", DIM);
 // terminamos la ejecución del programa
 exit(-1);
 }
 FD_SET (fd, &desc);
 // Inicio del bit descriptor connList con el valor del descriptor de entrada estándar
 FD_SET (READ, &desc);

Con el fin de poder atender en un sólo hilo de ejecución tanto el socket que se encuentra escuchando posibles mensajes con el servidor, como detectar nuevas entradas de texto por la entrada estándar, creamos un multiplexor, al igual que hicimos en el servidor con un par de descriptores de fichero.

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

 // recorriendo los sockets para ver los que están activos
 PDEBUG("INFO: recorriendo los sockets para ver los que están activosn");
 if(FD_ISSET(fd, &descCopy)){
 PDEBUG("INFO: Nuevo mensaje recibidon");
 if(client_read(bio, &auxMsj, sizeof(sms)) <= 0){
 PDEBUG("INFO: El socket está cerradon");
 perror("Error al leer contenido del socket porque está cerradon");
 perror("El cliente se pararán");
 exit(-1);
 }

 switch(auxMsj.flag){
 case OK: // mensaje de aceptacion, mismo comportamiento que msj
 case MSJ:  // mensaje recibido
 if(serverTalk != 1 || strcmp(auxMsj.name, SERVER) == 0){
 PDEBUG("INFO: Recibido mensajen");
 sprintf(aux, "%s ~$> %sn", auxMsj.name, auxMsj.text);
 write(WRITE, aux, strlen(aux));
 sync();
 }
 break;
 case SERV_EXIT: // el servidor se va a cerrar
 PDEBUG("EXIT: El servidor se está cerrando, se dejará de leern");
 shutdown(sock, SHUT_RDWR);
 sprintf(aux, "%s ~$> Servidor desconectadon", SERVER);
 write(WRITE, aux, strlen(aux));
 sprintf(aux, "El proceso cliente se cerrarán");
 write(WRITE, aux, strlen(aux));
 exit(0);
 break;
 default:
 sprintf(aux, "Recibido un mensaje mal formadon");
 write(WRITE, aux, strlen(aux));
 sync();
 break;
 }
 }else if(FD_ISSET(READ, &descCopy)){
 PDEBUG("INFO: Nuevo mensaje escriton");

 bzero(&auxMsj.text, SMS_LEN); // inicializamos el array
 nbytes = read(READ, &auxMsj.text, SMS_LEN); // leemos de la entrada estándar
 auxMsj.text[nbytes - 1] = 0; // eliminamos el retorno de carro

 // nos salimos?
 if(strcmp(auxMsj.text, "-x") == 0 && serverTalk != 1){

 PDEBUG("EXIT: Cerrando el cliente, avisando al servidor...n");
 auxMsj.flag = CLI_EXIT;

 }else if(strcmp(auxMsj.text, "--serv") == 0){ // queremos hablar con el servidor

 PDEBUG("SERV_ADMIN: Iniciando la comunicación directa con el servidorn");
 sprintf(aux, "%s ~$> Iniciada conversación con el servidorn", SERVER);
 write(WRITE, aux, strlen(aux));
 serverTalk = 1;
 continue;

 }else if(sscanf(auxMsj.text, "--mp %s", aux) == 1){ // queremos hablar con el servidor

 PDEBUG("MP: Mensaje privado detectadon");
 strcpy(auxMsj.to, aux);
 sprintf(aux, "%s ~$> Inserte el mensaje privadon", SERVER);
 write(WRITE, aux, strlen(aux));
 auxMsj.flag = MP;
 nbytes = read(READ, &auxMsj.text, SMS_LEN); // leemos de la entrada estándar
 auxMsj.text[nbytes - 1] = 0; // eliminamos el retorno de carro

 }else{ // es un mensaje

 if(serverTalk == 1){

 PDEBUG("SERV_ADMIN: Enviando mensaje al servidorn");
 auxMsj.flag = SERV_ADMIN;

 if(strcmp(auxMsj.text, "exit") == 0){

 serverTalk = 0;
 sprintf(aux, "%s ~$> Envio de mensajes de configuración terminada:n", SERVER);
 write(WRITE, aux, strlen(aux));
 continue;

 }

 }else{

 auxMsj.flag = MSJ;

 }
 }

 strcpy(auxMsj.name, name); // hacemos una copia del nombre introducido para no perderlo
 PDEBUG("INFO: Enviando mensaje...n");
 client_write(bio, &auxMsj, sizeof(sms));
 PDEBUG("INFO: Mensaje Enviadon");

 // nos salimos?
 if(auxMsj.flag == CLI_EXIT){

 PDEBUG("EXIT: Cerrando el socketn");
 shutdown(sock, SHUT_RDWR);
 PDEBUG("EXIT: Cerrando el clienten");
 exit(0);

 }
 }
 }

 return 0;
}

Esta parte, a pesar de ser la más larga de todas con diferencia, no ha de preocuparnos demasiado, y es que lo único que hace es leer mensajes indefinidamente bien del socket o de la entrada estándar y actúa en consecuencia al descriptor de entrada de datos y en caso de ser un mensaje al Flag que tenga el mensaje, por lo que a mi modo de ver, no vale la pena detenerse demasiado en su funcionamiento.

interfaz.c

Como último fichero que creo que merece la pena comentar el interfaz.c, y es que este es un archivo gracias al cual podremos ejecutar el proceso cliente sin tocar una sola línea de código, y es que, aunque el binario cliente es autónomo por sí mismo, vale la pena decir que es un poco feo porque se ejecuta en consola, por lo que apoyándonos en un sencillo programa en java cuyo código puedes encontrar en el primer artículo de la serie, seremos capaces de añadirle una interfaz gráfica al chat gracias a la redirección de los flujos de entrada-salida.

Así que si más rollo vamos a analizar el código de este interesante programa:


int pipe_chat[2];       // definimos el pipe que va del chat a la interfaz
 int pipe_interfaz[2];   // definimos el pipe que va de la interfaz al chat

 if(
 pipe(pipe_chat) == -1 ||
 pipe(pipe_interfaz) == -1
 ){
 perror("ERROR: ocurrión un error al crear los pipesn");
 exit(-1);
 }

 if((pid = fork()) < (pid_t)0){
 perror("ERROR: ocurrió un error al crear el proceso hijon");
 exit(-1);
 }

Por ahora lo único que hemos hecho es crear un par de pipes y crear un segundo proceso.

else if(pid == (pid_t)0){  // proceso hijo, es el que albergará la interfaz
 close(pipe_chat[1]);    // cerramos el descriptor de escritura del chat
 close(pipe_interfaz[0]); // cerramos el descriptor de lectura de la interfaz

 // cerramos los descriptores de entrada salida estandar
 close(STDIN_FILENO);
 close(STDOUT_FILENO);

 // redireccionamos la entrada y la salida
 dup2(pipe_chat[0], STDIN_FILENO);
 dup2(pipe_interfaz[1], STDOUT_FILENO);

 // cerramos los descriptores ya que no vamos a utilizarlos
 close(pipe_chat[0]);
 close(pipe_interfaz[1]);

 execl("/usr/bin/java", "java", "-jar", "chat.jar", (char*) 0);
 }

En el caso de que el proceso en cuestión sea el proceso hijo, le cerramos los descriptores de salida estándar, y se los reasignamos a los descriptores de nuestras pipes, de manera que ahora los descriptores de entrada-salida de la interfaz escribirán, no en consola, sino en las pipes asignadas. Por último hacemos que este hilo de ejecución, se encargue de correr la interfaz gráfica.

else{ // proceso padre
 close(pipe_interfaz [1]);    // cerramos el descriptor de escritura de la interfaz
 close(pipe_chat[0]); // cerramos el descriptor de lectura del chat

 // cerramos los descriptores de entrada salida estandar
 close(STDIN_FILENO);
 close(STDOUT_FILENO);

 // redireccionamos la entrada y la salida
 dup2(pipe_interfaz[0], STDIN_FILENO);
 dup2(pipe_chat[1], STDOUT_FILENO);

 // cerramos los descriptores ya que no vamos a utilizarlos
 close(pipe_chat[0]);
 close(pipe_interfaz[1]);

 execv("./cli5", argv);
 //execl("./cli5", "cli5", (char*) 0);

 }

En el proceso padre hacemos exactamente lo mismo que en el proceso hijo, la única diferencia es que ahora la entrada-salida estándar es asignada a los otros extremos de las pipes y que ahora ejecutamos el binario cliente.

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.