En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 7 de octubre de 2017

Thread Ringers
cómo usar los thread en C - pt.3

Con este post cerramos (a lo grande, espero) la mini-serie sobre los thread (Dead Ringers para los amigos).
...exactamente la imagen que te esperas en un blog de programación...
Después de los ejemplos básicos de las dos primeras partes de la serie (¿que acabáis de leer otra vez, verdad? aquí y aquí), es una buena idea enseñar un ejemplo real de una de las muchas aplicaciones que pueden tener los thread. Y entre las muchas, he elegido una que me parece interesante, es decir, un Socket Server multithread, donde cada conexión con un Client remoto se maneja con un thread separado. Una recomendación: antes de seguir, deberíais leer uno de mis antiguos post, o sea: El Server oscuro: la leyenda renace, que es una introducción ideal al tema actual, ya que describe (y bien, espero) la funcionalidad y el código de un Socket Server single-thread. Por cierto, como se verá en breve, el nuevo código que voy a mostrar está estrechamente relacionado con el que se muestraba en el antiguo post.

Y ahora vamos al grano, ¡vamos con el código!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>

#define BACKLOG   10      // para listen()
#define MYBUFSIZE 1024

// prototipos locales
void *connHandler(void *conn_sock);

int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: numero argumentos equivocado\n", argv[0]);
        printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        // errore socket()
        printf("%s: could not create socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struct sockaddr_in para este server
    struct sockaddr_in server;          // (local) server socket info
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(atoi(argv[1]));

    // bind informaciones del server al socket
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) {
        // error bind()
        printf("%s: bind failed (%s)", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // start escucha con una cola de max BACKLOG conexiones
    if (listen(my_socket, BACKLOG) == -1) {
        // error listen()
        printf("%s: listen failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // acepta conexiones de un client entrante
    printf("%s: espera conexiones entrantes...\n", argv[0]);
    pthread_t thread_id;
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) {
        printf("%s: conexion aceptada\n", argv[0]);
        if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) {
            // error pthread_create()
            printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }

    // error accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}

// thread function para gestión conexiones
void *connHandler(void *conn_sock)
{
    // extrae el client socket del argumento
    int client_sock = *(int*)conn_sock;

    // loop di recepción mensajes del client
    int read_size;
    char client_msg[MYBUFSIZE];
    while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) {
        // send mensaje de retorno al client
        printf("%s: recibido mensaje del sock %d: %s\n", __func__, client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "me has escrito: %s", client_msg);
        send(client_sock, server_msg, strlen(server_msg), 0);

        // clear buffer
        memset(client_msg, 0, MYBUFSIZE);
    }

    // loop terminado: test motivo
    if (read_size == -1) {
        // error recv()
        printf("%s: recv failed\n", __func__);
    }
    else {
        // read_size == 0: el client se ha desconectado
        printf("%s: client disconnected\n", __func__);
    }

    return NULL;
}

Bueno, no vamos a contar otra vez cómo funciona un Socket Server (ya hecho en el antiguo post, releerlo, please), pero vamos a centrarnos en las diferencias entre el codigo single-thread y el multithread: seguramente habeis notado que son prácticamente idénticos hasta la fase de listen(), e, incluso después, las diferencias son mínimas: la fase de accept() está ahora en un loop y se crea un nuevo thread para cada conexión aceptada (de un Client remoto). ¿Y qué hace el thread? Ejecuta la función connHandler() que contiene, por cierto, el loop de recv() que en el código antiguo se ejecutava inmediatamente después de la fase de accept(). tambien el siguiente test del motivo de salida (antes de tiempo) del loop está contenido en connHandler(), y enseña la señal de error correcta (recv() error o client disconnected, dependiendo del código devuelto por la recv()).

¿Qué añadir? Simple y super-funcional: ¡un Socket Server multithread con cuatro líneas de código! Por supuesto, la sintaxis de creación de los thread  y la ejecución de la start_routine del mismo son idénticas a las descritas aquí. Para probar el nuestro Socket Server, también es necesario compilar un Socket Client (por supuesto el que se describe en mi antiguo post El Client oscuro: la leyenda renace) y ejecutar, por ejemplo, una instancia del Socket Server y dos instancias del Socket Client (en tres terminales diferentes de la misma máquina, o en tres máquinas diferentes). Ejecutando en mi máquina (Linux, por supuesto) en tres terminales, el resultado es el siguiente:
 
En la terminal 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999
./sockserver-mt: espera conexiones entrantes...
./sockserver-mt: conexión aceptada
./sockserver-mt: conexión aceptada
connHandler: recibido mensaje del sock 4: pippo
connHandler: recibido mensaje del sock 5: pluto
connHandler: client disconnected
connHandler: client disconnected

En la terminal 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pippo
./sockclient: Server reply: me has escrito: pippo
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

En la terminal 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pluto
./sockclient: Server reply: me has escrito: pluto
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

notar que cuando uno de los Client sale (con un CTRL-C, por ejemplo) el Server se da cuenta y enseña, como era de esperar, client disconnected... perfecto.

Ok, hemos acabado con los
thread. Ahora intentaré pensar en algún nuevo tema interesante para el próximo post. Como siempre os recomiendo de no aguantar la respiración en la espera...

¡Hasta el próximo post!