BETTER CALL GO
come scrivere Server e Client TCP in Go e C – pt.1
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 il 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…)
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 argomenti if (argc != 2) { // errore di chiamata printf("%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/TCP int 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 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(atoi(argv[1])); // set port del server // associo l'indirizzo del server al socket if (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 connessioni if (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 entrante printf("%s: attesa connessioni entranti...\n", argv[0]); socklen_t socksize = sizeof(struct sockaddr_in); struct sockaddr_in client; // struttura sockaddr_in per il client remoto int 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 uso close(srv_sock); // loop di ricezione messaggi dal client char cli_msg[MYBUFSIZE]; int recv_size; while ((recv_size = recv(cli_sock, cli_msg, MYBUFSIZE, 0)) > 0 ) { // send messaggio di ritorno al client printf("%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 buffer memset(cli_msg, 0, MYBUFSIZE); } // loop terminato: test motivo if (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 è disconnesso printf("%s: client disconnesso\n", argv[0]); } // esco con Ok close(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 main import ( "bufio" "fmt" "net" "os" ) func main() { // test argomenti if len(os.Args) != 2 { // errore di chiamata fmt.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 richiesto port := ":" + os.Args[1] // set port (i.e.: ":port") lner, err := net.Listen("tcp", port) // set listener con network di tipo TCP if err != nil { // errore di ascolto fmt.Println(err) return } defer lner.Close() // prenoto la chiusura del listener // accetta connessioni da un client entrante fmt.Printf("%s: attesa connessioni entranti...\n", os.Args[0]) conn, err := lner.Accept() if err != nil { // errore di accept fmt.Println(err) return } // loop di ricezione messaggi dal client connrdr := bufio.NewReader(conn) // reader sulla connessione for { // attende la ricezione di un messaggio client_msg, err := connrdr.ReadString('\n') // leggo con il conn reader if err != nil { // errore di ricezione fmt.Println(err) return } // mostra il messaggio ricevuto e compone la risposta fmt.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 connessione if err != nil { // errore di send fmt.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!