Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 7 ottobre 2017

Thread Ringers
come usare i thread in C - pt.3

Con questo post chiudiamo (in bellezza, spero) il mini-ciclo sui thread (Dead Ringers per gli amici).
...proprio l'immagine che uno si aspetta in un blog di programmazione...
Dopo gli esempi base delle prime due parti del ciclo (che avete appena riletto, vero? qui e qui), è il caso di fare un esempio reale di una delle tante applicazioni che possono avere i thread. E tra le tante ne ho scelto una che mi sembra interessante, ovvero un Socket Server multithread, dove ogni connessione con un Client remoto viene gestita con un thread separato. Una raccomandazione: prima di andare avanti dovreste rileggere un mio vecchio post, e cioè: Il Server oscuro - Il ritorno, che è una ideale introduzione all'argomento in corso, visto che descrive (e bene, spero) funzionalità e codice di un Socket Server single-thread. Tra l'altro (come noterete tra poco) il nuovo codice che vi mostrerò è parente strettissimo di quello mostrato nel vecchio post.

E ora bando alle ciance, vai col codice!

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

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

// prototipi locali
void *connHandler(void *conn_sock);

int main(int argc, char *argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore args
        printf("%s: numero argomenti errato\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 struttura sockaddr_in per questo 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 informazioni del server al socket
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) {
        // errore bind()
        printf("%s: bind failed (%s)", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // start ascolto con una coda di max BACKLOG connessioni
    if (listen(my_socket, BACKLOG) == -1) {
        // errore listen()
        printf("%s: listen failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // accetta connessioni da un client entrante
    printf("%s: attesa connessioni entranti...\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: connessione accettata\n", argv[0]);
        if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) {
            // errore pthread_create()
            printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }

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

// thread function per gestione connessioni
void *connHandler(void *conn_sock)
{
    // estrae il client socket dall'argomento
    int client_sock = *(int*)conn_sock;

    // loop di ricezione messaggi dal client
    int read_size;
    char client_msg[MYBUFSIZE];
    while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) {
        // send messaggio di ritorno al client
        printf("%s: ricevuto messaggio dal sock %d: %s\n", __func__, client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "mi hai scritto: %s", client_msg);
        send(client_sock, server_msg, strlen(server_msg), 0);

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

    // loop terminato: test motivo
    if (read_size == -1) {
        // errore recv()
        printf("%s: recv failed\n", __func__);
    }
    else {
        // read_size == 0: il client si è disconnesso
        printf("%s: client disconnected\n", __func__);
    }

    return NULL;
}

Ok, non stiamo a raccontare di nuovo come funziona un Socket Server (già fatto nel vecchio post, rileggere attentamente, please), ma concentriamoci sulle differenze tra il codice single-thread e quello multithread: sicuramente avrete notato che sono praticamente identici fino alla fase di listen(), e anche dopo le differenze sono minime: la fase di accept() adesso è in un loop, e per ogni connessione accettata (di un Client remoto) viene creato un nuovo thread. E cosa esegue il thread? Esegue la funzione connHandler() che contiene, guarda caso, il loop di recv() che nel vecchio codice era eseguito subito dopo la fase di accept(). Anche il successivo test del motivo di uscita (prematura) dal loop è contenuto in connHandler(), e mostra il corretto segnale di errore (recv() error o client disconnected, in base al codice ritornato dalla recv()).

Cosa aggiungere? Semplice e super-funzionale: un Socket Server multithread con quattro righe di codice! Ovviamente la sintassi di creazione del thread e l'esecuzione della start_routine dello stesso sono identiche a quelle descritte qui. Per testare il nostro Socket Server è necessario compilare anche un Socket Client (ovviamente quello descritto nel mio vecchio post Il Client oscuro - Il ritorno), ed eseguire, ad esempio, una istanza del Socket Server e due istanze del Socket Client (in tre terminali diversi della stessa macchina, oppure su tre macchine diverse). Eseguendo sulla mia macchina (Linux, ovviamente) su tre terminali il risultato è il seguente:

Nel terminale 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999
./sockserver-mt: attesa connessioni entranti...
./sockserver-mt: connessione accettata
./sockserver-mt: connessione accettata
connHandler: ricevuto messaggio dal sock 4: pippo
connHandler: ricevuto messaggio dal sock 5: pluto
connHandler: client disconnected
connHandler: client disconnected

Nel terminale 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Scrivi un messaggio per il Server remoto: pippo
./sockclient: Server reply: mi hai scritto: pippo
Scrivi un messaggio per il Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

Nel terminale 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Scrivi un messaggio per il Server remoto: pluto
./sockclient: Server reply: mi hai scritto: pluto
Scrivi un messaggio per il Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

notare che quando uno dei Client esce (con un CTRL-C, ad esempio) il Server se ne accorge e visualizza, come previsto, client disconnected... perfetto.

Ok, con i thread abbiamo finito. Adesso cercherò di pensare a qualche nuovo interessante argomento per il prossimo post. Come sempre vi invito a non trattenere il respiro nell'attesa...

Ciao e al prossimo post!

2 commenti: