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.

lunedì 11 luglio 2022

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).

...crepi l'avarizia, oggi ti offrirò un menu high-level...

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):

lbbroker

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!

Nessun commento:

Posta un commento