Jimmy McGill (Saul Goodman): Quanti avvocati servono per cambiare una lampadina? Tre. Uno sale sulla scala, uno lo fa cadere e uno fa causa all'azienda della scala.
Better Call Saul è un altro grande esempio di Cinema per la TV (è uno spin-off di Breaking Bad... ma questo lo sapete tutti, No? Non lo sapete? e vabbè, continuiamo così, facciamoci del male…), come la serie di cui è un prequel. Il nostro Saul, che qui si chiama ancora Jimmy McGill, è un personaggio ineffabile, un avvocato sui generis. È un vulcano di idee, citazioni, iniziative. È un personaggio "todoterreno", un po' come lo è il Go, che è un linguaggio super eclettico, con molteplici usi in ambito backend, network, microservizi... e con uno speciale occhio di riguardo per la programmazione concorrente. Nonostante la mia nota predilezione e passione per il C non ho nessun problema a decantare le doti del Go, anche perché è, comunque, della famiglia: sia per la forma (è C-like) che per la storia (due dei tre creatori sono Ken Thompson e Rob Pike... e ho detto tutto!). E poi per chi viene dal C è veramente facile impararlo, e non ti senti in colpa se, magari, ti trovi a usarlo per scrivere cose che prima facevi esclusivamente in C (a parte la programmazione di sistema). Insomma: il Go non sostituisce il C, ma lo affianca.
(...e chi si deve preoccupare, semmai, è il C++, visto che in Google hanno creato Go proprio per liberarsi un po' delle sue criticità... ma questa è un altra storia, che avevo già accennato. Magari la riprenderò nella seconda parte dell'articolo...)
...le dure decisioni della vita: C o Go?... |
Ho già parlato del Go in passato (qui e qui) mostrando alcuni usi interessanti. Oggi ho deciso di offrirvi un confronto tra le versioni C e Go di due classici: un Server TCP e un Client TCP (quest'ultimo nella seconda parte dell'articolo). Ho scritto decine di programmi come questo in C e C++ per lavoro e per studio, e ne ho scritto, in varie versioni (TCP, UDP, con OpenSSL), anche su queste pagine. Cominciamo, allora, con un semplice esempio in C, praticamente identico a quello che avevo descritto in un vecchio articolo: vai col codice!
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <unistd.h>#include <arpa/inet.h>#define MYBUFSIZE 1024#define BACKLOG 10 // per listen()int main(int argc, char *argv[]){// test argomentiif (argc != 2) {// errore di chiamataprintf("%s: numero argomenti errato\n", argv[0]);printf("uso: %s port [e.g.: %s 9999]\n", argv[0], argv[0]);return EXIT_FAILURE;}// creo il socket in modo internet/TCPint srv_sock;if ((srv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {// errore socket()printf("%s: non posso creare il socket (%s)\n", argv[0], strerror(errno));return EXIT_FAILURE;}// preparo la struttura sockaddr_in per questo serverstruct sockaddr_in server;memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET; // set famiglia di indirizziserver.sin_addr.s_addr = INADDR_ANY; // set indirizzo del serverserver.sin_port = htons(atoi(argv[1])); // set port del server// associo l'indirizzo del server al socketif (bind(srv_sock, (struct sockaddr *)&server, sizeof(server)) == -1) {// errore bind()printf("%s: errore bind (%s)", argv[0], strerror(errno));close(srv_sock);return EXIT_FAILURE;}// start ascolto con una coda di max BACKLOG connessioniif (listen(srv_sock, BACKLOG) == -1) {// errore listen()printf("%s: errore listen (%s)\n", argv[0], strerror(errno));close(srv_sock);return EXIT_FAILURE;}// accetto connessioni da un client entranteprintf("%s: attesa connessioni entranti...\n", argv[0]);socklen_t socksize = sizeof(struct sockaddr_in);struct sockaddr_in client; // struttura sockaddr_in per il client remotoint cli_sock;if ((cli_sock = accept(srv_sock, (struct sockaddr *)&client, &socksize)) == -1) {// errore accept()printf("%s: errore accept (%s)\n", argv[0], strerror(errno));close(srv_sock);return EXIT_FAILURE;}// chiudo il socket non più in usoclose(srv_sock);// loop di ricezione messaggi dal clientchar cli_msg[MYBUFSIZE];int recv_size;while ((recv_size = recv(cli_sock, cli_msg, MYBUFSIZE, 0)) > 0 ) {// send messaggio di ritorno al clientprintf("%s: ricevuto messaggio dal sock %d: %s\n", argv[0], cli_sock, cli_msg);char srv_msg[MYBUFSIZE];sprintf(srv_msg, "mi hai scritto: %s", cli_msg);if (send(cli_sock, srv_msg, strlen(srv_msg), 0) == -1) {// errore send()printf("%s: errore send (%s)\n", argv[0], strerror(errno));close(cli_sock);return EXIT_FAILURE;}// clear del buffermemset(cli_msg, 0, MYBUFSIZE);}// loop terminato: test motivoif (recv_size == -1) {// errore recv()printf("%s: errore recv (%s)\n", argv[0], strerror(errno));close(cli_sock);return EXIT_FAILURE;}else if (recv_size == 0) {// Ok: il client si è disconnessoprintf("%s: client disconnesso\n", argv[0]);}// esco con Okclose(cli_sock);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.
Il flusso è quello classico ed elementare di un Server TCP:
- creo il socket in modo internet/TCP
- preparo la struttura sockaddr_in per questo Server
- associa l'indirizzo del server al socket
- start ascolto con una coda di max BACKLOG connessioni
- accetta connessioni da un Client entrante
- loop di ricezione messaggi dal Client
- riceve un messaggio
- send messaggio di ritorno al client
ovviamente esistono varianti di questa struttura, ma questa è quella classica. Come avrete notato nel loop di lettura c’è il re-invio al Client del messaggio ricevuto, e questo ci aiuta, durante l'esecuzione, a osservare il corretto funzionamento della coppia Client/Server che metteremo in prova.
Evidentemente questo esempio, pur essendo abbastanza completo (è quasi un codice di produzione), è relativamente semplice, sono poche righe e fa il suo dovere. Ma si può rendere ancora più semplice e compatto? La risposta è si, chiedendo aiuto al nostro nuovo amico Go (o Golang, se preferite). Ecco un esempio di Server TCP in Go, guardate e stupite!
package mainimport ("bufio""fmt""net""os")func main() {// test argomentiif len(os.Args) != 2 {// errore di chiamatafmt.Printf("%s: numero argomenti errato\n", os.Args[0])fmt.Printf("uso: %s port [e.g.: %s 9999]\n", os.Args[0], os.Args[0])return}// start ascolto sul port richiestoport := ":" + os.Args[1] // set port (i.e.: ":port")lner, err := net.Listen("tcp", port) // set listener con network di tipo TCPif err != nil {// errore di ascoltofmt.Println(err)return}defer lner.Close() // prenoto la chiusura del listener// accetta connessioni da un client entrantefmt.Printf("%s: attesa connessioni entranti...\n", os.Args[0])conn, err := lner.Accept()if err != nil {// errore di acceptfmt.Println(err)return}// loop di ricezione messaggi dal clientconnrdr := bufio.NewReader(conn) // reader sulla connessionefor {// attende la ricezione di un messaggioclient_msg, err := connrdr.ReadString('\n') // leggo con il conn readerif err != nil {// errore di ricezionefmt.Println(err)return}// mostra il messaggio ricevuto e compone la rispostafmt.Printf("%s: ricevuto messaggio: %s", os.Args[0], string(client_msg))server_msg := fmt.Sprintf("mi hai scritto %s", string(client_msg))// send messaggio di ritorno al client_, err = conn.Write([]byte(server_msg)) // scrivo sulla connessioneif err != nil {// errore di sendfmt.Println(err)return}}}
È veramente semplicissimo e compattissimo! Ho, volutamente (come sempre) esagerato coi commenti per descrivere ogni singola attività, e ho usato (sempre volutamente) le stesse frasi nelle descrizioni dei passi del flusso che ho usato nella versione C, così è più facile fare una comparazione. Risulta evidente che, grazie ai package inclusi nel linguaggio e grazie alla natura stessa del linguaggio, questo Server TCP in Go ha una struttura con meno passi ed ogni passo è più semplice da scrivere (e da leggere!) dell'equivalente in C. Vediamo la struttura:
- start ascolto sul port richiesto
- accetta connessioni da un Client entrante
- loop di ricezione messaggi dal Client
- riceve un messaggio
- send messaggio di ritorno al client
Sono veramente quattro righe, e fa esattamente lo stesso lavoro del TCP Server in C con cui abbiamo introdotto l'argomento. Fantastico.
Nel prossimo articolo vedremo, come promesso, il Client TCP in Go e C. Una volta compilati potrete verificare che si comportano esattamente nella stessa maniera, provare per credere!
Ciao, e al prossimo post!
Nessun commento:
Posta un commento