Sue: Hai sentito che il film di Philippe ha vinto il primo premio al Festival di Colonia, oggi? Mort: Molto eccitante. Ma Eichmann non era di lì?
E va bene, questa volta ci siamo, è il turno della seconda parte di Rifkin’s Festival (oops: ZeroMQ Festival), come promesso nella prima parte dell’articolo. Qui sopra vi ho proposto un commento secco e lapidario di Mort (Wallace Shawn), il protagonista alter-ego di Woody Allen in questo bel film, un commento che ben introduce questo articolo: dopo aver parlato dell’interfaccia a ZeroMQ di “basso livello“, e cioè la libreria libzmq, è il momento di parlare dell’interfaccia ad “alto livello“, la CZMQ (libczmq). Grazie a CZMQ è possibile scrivere codice di rete sintetico e compatto (anzi, secco e lapidario) anche per problemi complessi, problemi che richiederebbero la scrittura di codice lungo e complicato anche aiutandosi con ZeroMQ low-level (e lunghissimo e complicatissimo usando, ad esempio, i classici BSD Socket).

Allora: CZMQ è una interfaccia C ma è strutturata come se fosse una libreria di classi (nel manuale il termine “class” viene usato frequentemente). Comunque niente paura per chi non è avvezzo al C++: è una interfaccia C al 100%. E questa è la descrizione che danno gli autori stessi della libreria nel manuale:
CZMQ ha questi obiettivi: - "wrappare" l'API core di ØMQ in una semantica che sia naturale e porti ad applicazioni più brevi e leggibili. - Nascondere le differenze tra le versioni di ØMQ. - Fornire uno spazio per lo sviluppo di semantiche API più sofisticate.
E quali sono, in pratica, i vantaggi dell’uso di CZMQ? Facciamo un caso reale: se, per esempio, volessimo risolvere un problema complicato come scrivere un Load Balancing Broker che segua lo schema di funzionamento seguente (copio una descrizione di questo oggetto da 0MQ – The Guide):
grazie a CZMQ potremmo farlo in maniera abbastanza sintetica e leggibile. Mentre se provassimo a farlo con altri strumenti ci renderemmo subito conto della grande differenza a livello di lunghezza e complicazione. Per quanto riguarda la realizzazione pratica di questo LBB (visto che non amo fare il copia-incolla di cose non mie) vi passo direttamente due link dove viene descritto il codice corrispondente, un doppio codice scritto usando le due librerie (low e high-level): se gli date un occhiata noterete che con CZMQ è decisamente più semplificato e leggibile. Ah, e vi consiglio un utile esercizio: provate a riscrivere (o, perlomeno, a immaginare) lo stesso codice scritto con i BSD Socket… tremo solo a pensarci. Ecco i link: versione con lzmq e versione con CZMQ.
E adesso? Ma non posso chiudere un articolo senza proporre nessun codice mio! Quindi ho pensato che per ben descrivere la differenza di approccio tra low-level e high-level è meglio partire dalle basi, e allora vi propongo un esempio semplicissimo di codice Client/Server scritto in tre versioni, BSD Socket, ZeroMQ low-level e CZMQ.
Ho scritto tutti gli esempi a mo’ di codice di test (e non di produzione), quindi sono (ahimè) completamente assenti le importantissime istruzioni di trattamento degli errori (praticamente sono gli scheletri funzionali da completare per entrare in produzione): questo con l’obiettivo di non sviare l’attenzione dal flusso base del codice, quello che ci mostra come si lavora con una libreria rispetto a un’altra. Tutti gli esempi sono composti da due file, client.c e server.c, e sono perfettamente compilabili e funzionanti. Ok, cominciamo con i BSD Socket, vai col codice!
// client.c - un semplice client con BSD Socket #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> // main() - funzione main int main(void) { // creo il socket printf ("Connessione al server...\n"); int cli_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // preparo la struttura sockaddr_in per il server remoto struct sockaddr_in server; memset(&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; // set famiglia di indirizzi server.sin_addr.s_addr = inet_addr("127.0.0.1");// set indirizzo del server server.sin_port = htons(5555); // set port del server // connetto il socket al server remoto connect(cli_sock, (struct sockaddr *)&server, sizeof(server)); // loop di invio int cnt = 0; while (cnt++ < 10) { // invio la richiesta send(cli_sock, "Ping", 5, 0); // ricevo la risposta char string[10]; recv(cli_sock, string, sizeof(string), 0); printf("risposta ricevuta: %s\n", string); } // disconnetto close(cli_sock); return 0; }
// server.c - un semplice server con BSD Socket #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> // main() - funzione main int main(void) { // creo il socket TCP printf ("Avvio server...\n"); int srv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // preparo la struttura sockaddr_in per questo server struct sockaddr_in server; memset(&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; // set famiglia di indirizzi server.sin_addr.s_addr = INADDR_ANY; // set indirizzo del server server.sin_port = htons(5555); // set port del server // associo l'indirizzo del server al socket e start ascolto bind(srv_sock, (struct sockaddr *)&server, sizeof(server)); listen(srv_sock, 10); // accetto connessioni da un client entrante socklen_t socksize = sizeof(struct sockaddr_in); struct sockaddr_in client; // struttura sockaddr_in per il client remoto int cli_sock = accept(srv_sock, (struct sockaddr *)&client, &socksize); close(srv_sock); // loop di ricezione char string[10]; while (recv(cli_sock, string, sizeof(string), 0) != -1) { printf("richiesta ricevuta: %s\n", string); // invio la risposta send(cli_sock, "Pong", 5, 0); } // disconnetto close(cli_sock); return 0; }
E passiamo ora al codice in versione libzmq (ZeroMQ low-level):
// client.c - un semplice client ZeroMQ con libzmq #include <stdio.h> #include <zmq.h> // main() - funzione main int main(void) { // creo il context e il requester printf ("Connessione al server...\n"); void *context = zmq_ctx_new(); void *requester = zmq_socket(context, ZMQ_REQ); // connetto il requester al responder zmq_connect(requester, "tcp://localhost:5555"); // loop di invio int cnt = 0; while (cnt++ < 10) { // invio la richiesta zmq_send(requester, "Ping", 5, 0); // ricevo la risposta char string[10]; zmq_recv(requester, string, sizeof(string), 0); printf("risposta ricevuta: %s\n", string); } // disconnetto zmq_close (requester); zmq_ctx_destroy (context); return 0; }
// server.c - un semplice server ZeroMQ con libzmq #include <stdio.h> #include <zmq.h> // main() - funzione main int main(void) { // creo il context e il responder printf ("Avvio server...\n"); void *context = zmq_ctx_new(); void *responder = zmq_socket(context, ZMQ_REP); // associo l'indirizzo del responder al contesto zmq_bind(responder, "tcp://*:5555"); // loop di ricezione char string[10]; while (zmq_recv(responder, string, 10, 0) != -1) { printf("richiesta ricevuta: %s\n", string); // invio la risposta zmq_send(responder, "Pong", 5, 0); } // disconnetto zmq_close(responder); zmq_ctx_destroy(context); return 0; }
E già questo codice dimostra che ZeroMQ è una libreria che permette di scrivere codice di rete molto semplificato. Ma con CZMQ possiamo snellirlo ancora di più (ma senza aspettarci miracoli: la libzmq già di per sé un passo avanti rispetto al vero low-level, che è rappresentato dai BSD Socket). Vai di nuovo col codice!
// client.c - un semplice client ZeroMQ con CZMQ #include <stdio.h> #include <czmq.h> // main() - funzione main int main(void) { // create requester printf ("Connessione al server...\n"); zsock_t *requester = zsock_new_req("tcp://localhost:5555"); // loop di invio int cnt = 0; while (cnt++ < 10) { // invio la richiesta zstr_send(requester, "Ping"); // ricevo la risposta char *string; string = zstr_recv(requester); printf("risposta ricevuta: %s\n", string); zstr_free(&string); } // disconnetto zsock_destroy(&requester); return 0; }
// server.c - un semplice server ZeroMQ con CZMQ #include <stdio.h> #include <czmq.h> // main() - funzione main int main(void) { // creo il responder printf ("Avvio server...\n"); zsock_t *responder = zsock_new_rep("tcp://*:5555"); // loop di ricezione char *string; while ((string = zstr_recv(responder)) != NULL) { printf("richiesta ricevuta: %s\n", string); zstr_free(&string); // invio la risposta zstr_send(responder, "Pong"); } // disconnetto zsock_destroy(&responder); return 0; }
Che ne dite? La differenza tra le tre versioni, pur non essendo eclatante, è evidente, e si nota una filosofia di programmazione abbastanza diversa. E, a parte le considerazioni filosofiche, alla fin fine si può dire che nel percorso da BSD Socket a libzmq a CZMQ, le linee di codice vanno in diminuzione, e questo è il fattore molto importante che stavamo ricercando.
Ok, per oggi può bastare: ZeroMQ passa, a pieni voti, l’esame di “ottima libreria per Software di rete“. Lo passa bene già con la libreria low-level e “più meglio” (ah ah ah) con CZMQ, la libreria high-level. Ma, nonostante questo, vi invito a non dimenticare che, sotto sotto (anche sotto ZeroMQ!), ci sono i mitici e insostituibili BSD Socket che possono essere ancora usati direttamente per scrivere ottimo e soddisfacente codice di rete, magari solo un po’ più complicato (da scrivere e da leggere). De gustibus…
Ciao, e al prossimo post!
https://italiancoders.it/zeromq-festival-come-si-usa-zeromq-in-c-pt-2/