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!
