ZeroMQ Festival
come si usa ZeroMQ in C – pt.2
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!
