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.

venerdì 4 settembre 2015

Il Server oscuro - Il ritorno
come scrivere un Socket Server in C

Un po' come Batman, sono tornato! Certo, lungi da me mettermi sullo stesso piano del mitico Cavaliere Oscuro, ma questo è il mio blog e qualche licenza posso permettermela, no?.
Non è un Blog di Cinema questo, neh?
Come si intuisce dal titolo riprendiamo la nostra C-adventure parlando di Socket Server (o TCP Server, fa lo stesso). Spero che tutti sappiate cos'è, se no vi consiglio una utile lettura con tanto di esempio (ma il mio esempio è meglio!), così non perdo tempo e posso passare direttamente al codice. Eccolo!

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

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

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)) < 0) {
        // 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)) < 0) {
        // 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) < 0) {
        // 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]);
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
        // errore accept()
        printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // 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", argv[0], client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "mi hai scritto: %s", client_msg);
        write(client_sock, server_msg, strlen(server_msg));

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

    // loop terminato: test motivo
    if (read_size < 0) {
        // errore recv()
        printf("%s: recv failed\n", argv[0]);
        return EXIT_FAILURE;
    }
    else if (read_size == 0) {
        // Ok: il client si è disconnesso
        printf("%s: client disconnected\n", argv[0]);
    }

    // esco con Ok
    return EXIT_SUCCESS;
}

Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale.

la struttura è quella classica e basica di un Socket Server:
  1. socket() - crea un socket
  2. prepara la struttura sockaddr_in per questo server
  3. bind() - bind informazioni del server al socket
  4. listen() - start ascolto con una coda di max BACKLOG connessioni
  5. accept() - accetta connessioni da un client entrante
  6. recv() - loop di ricezione messaggi dal client
ovviamente esistono varianti di questa struttura, ma questa è quella classica. In quest'esempio, che ho scritto e testato appositamente per il blog (beh, in realtà ho adattato/modificato ad uso blog un po' di codice che ho scritto per lavoro, non è certo il primo Socket Server che scrivo!), nel loop di lettura c'è il re-invio al Client del messaggio ricevuto: sorpresa! Nel prossimo post vedremo il Client, così chiudiamo il cerchio e potremo testare sul serio una conversazione Client/Server.

Notare, poi, che il main() inizia con un bel test sugli argomenti con eventuale esempio di uso in caso di errore: questo dà la opportuna aria professionale al codice e non dovrebbe mai mancare in una applicazione di questo tipo.

Dal punto di vista strettamente stilistico ho scritto questo codice non rispettando il mio stile preferito (ci sono multipli punti di uscita) ma per un programma quasi-sequenziale come questo non è uno stile disprezzabile, e poi... mi è venuto così! Del resto a suo tempo avevo detto che ci vuole un po' di elasticità, mai essere troppo rigidi.

Un ultimo accenno sulla #define BACKLOG: trovare il valore adatto è un argomento quasi filosofico, per cui vi rimando a una ottima descrizione che ho trovato in linea. Ci rivediamo per il Client e, come sempre, non trattenete il respiro nell'attesa...

Ciao e al prossimo post!

domenica 22 giugno 2014

Skylog
come scrivere un Syslog in C - pt.3

Rieccoci qua. Spero che non abbiate trattenuto il respiro in attesa di questo post, come vi avevo raccomandato qui. Nel caso l'abbiate fatto dubito che sarete in grado di continuare la lettura... dove eravamo rimasti? (uh, è passato tanto tempo)... Ah si, questo è il seguito di Quantum of Solace (o era Quantum of Syslog?), quindi, per chi non l'avesse visto, il titolo è Skyfall (o Skylog ?).
Ma dove siamo? Ma non era un Blog di programmazione questo?
Nel primo e nel secondo episodio avevamo visto l'header mylog.h, l'utilizzatore main.c e il file di implementazione mylog.c. Per completare quest'ultimo ci mancava solo scrivere la funzione locale getDateUsec(), che è proprio quello che stiamo per fare.

In realtà una versione semplificata del nostro Syslog potrebbe contenere una funzione di questo tipo:
/* getDate()
 * Genera una stringa con data e ora.
 */
static char *getDate(char *dest, size_t size)
{
    // get time
    time_t t = time(NULL);
    struct tm *tmp = localtime(&t);

    // format stringa destinazione dest (deve essere allocata dal chiamante)
    strftime(dest, size, "%Y-%m-%d %H:%M:%S", tmp);

    // return stringa destinazione dest
    return dest;
}
Una funzione così è, nella maggior parte dei casi, più che sufficiente per aggiungere il dato orario alle nostre stringhe di log. Il risultato finale nel file log.log (descritto qui) sarebbe così:
2014-06-22 00:16:48 - questo è un msg di tipo 0 (il livello impostato è 2)
2014-06-22 00:16:48 - questo è un msg di tipo 1 (il livello impostato è 2)
2014-06-22 00:16:48 - questo è un msg di tipo 2 (il livello impostato è 2)
Ma noi siamo come il nostro amico Bond, abbiamo bisogno di mezzi più sofisticati, quindi useremo una funzione così:
/* getDateUsec()
 * Genera una stringa con data e ora (usa i microsecondi).
 */
static char *getDateUsec(char *dest, size_t size)
{
    // get time (con gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec)
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest(deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    snprintf(dest, size, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}
che è molto simile alla precedente, però ci fornisce anche i microsecondi (come giustamente descritto nei commenti interni alla funzione). L'uscita sul file di log sarà del tipo:
2014-06-22 00:17:16.921026 - questo è un msg di tipo 0 (il livello impostato è 2)
2014-06-22 00:17:16.921150 - questo è un msg di tipo 1 (il livello impostato è 2)
2014-06-22 00:17:16.921170 - questo è un msg di tipo 2 (il livello impostato è 2)
Ok, sento già dei mormorii: "che esagerato, addirittura i microsecondi!". Ribadisco: la versione semplice è più che sufficiente nella maggior parte dei casi, ma, sforzandoci un po', qualche esempio in cui ci può tornare utile una maggior precisione si trova.

Il primo esempio che mi viene in mente, senza scomodare il Software per sistemi hard-realtime, è il seguente: immaginatevi di aver scritto un socket-server e un client di test e di testarli nella stessa macchina (questo è uno scenario reale in cui si approfitta di avere una base dei tempi comune per Server e Client). Durante la fase di debug vi accorgete che in una fase operativa (tipicamente quella iniziale) qualche messaggio si perde per strada. Analizzate i due log disponibili (Server e Client) e non riuscite a capire quale è la prima coppia di messaggi che non sincronizza, perchè vari messaggi sembrano partire nello stesso secondo. Ecco, con la precisione del microsecondo riuscirete a capire meglio quello che sta succedendo (questo è un caso reale che mi è successo sul lavoro, non mi sto inventando niente!).

Uff, finalmente possiamo dare per terminata la trilogia del Syslog. Spero che un po' del Software descritto possa tornarvi utile in futuro. Ah, dimenticavo: "Mi chiamo Log, James Log".

Ciao e al prossimo post!