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ì 19 luglio 2021

Volatile: Endgame
come, quando e perché usare il type qualifier volatile in C

Scott Lang/Ant-Man: No, se rispettiamo le regole del viaggio nel tempo: non parlare con noi che troveremo in quel passato, non scommettere su eventi sportivi...
Tony Stark/Iron Man: Ti fermo prima che tu vada avanti, Scott. Mi stai seriamente dicendo che il tuo piano per salvare l'universo è basato su Ritorno al Futuro?

Il bel Avengers: Endgame termina in maniera magistrale il primo ciclo degli Avengers Marvel, ed è un bel film che chiude varie trame e sotto-trame iniziate tempo addietro, ed ha molteplici chiavi di lettura che gli permettono di piacere sia ai Cinefili (come me) sia a chi pensa che un Cinema è "un posto come un altro per ripararsi dalla pioggia" [S.Kubrick, 1987].

E cosa centra tutto questo con la famigerata keyword volatile? Beh, forse la presenza di trame e sotto-trame di una lunga storia che ha creato non pochi equivoci sull'argomento. Con questo articolo vorrei creare un Endgame sull'uso e disuso di volatile, ma ho obiettivi limitati e realistici: perlomeno mi accontenterei di dare qualche utile informazione a chi neppure sa cosa sia e, allo stesso tempo, di non confondere le idee a chi la usa già in maniera efficace. Speriamo bene...

...ti spiego: un volatile non necessariamente ha le ali...

E direi di cominciare con le buone notizie:

  • La prima buona notizia è che volatile si usa veramente col contagocce, perché realmente serve solo nella programmazione di bassissimo livello, quella a stretto contato con l'Hardware: quindi se non siete dei programmatori hard-embedded probabilmente non la userete mai.
  • La seconda buona notizia è che l'uso improprio di volatile è probabile che non faccia molti danni: nel senso che se la usate (per sbaglio) quando in realtà non vi serve è molto probabile che il codice funzioni ugualmente bene: in quel caso potrete essere fieri di avere un codice ben funzionante nonostante l'uso di volatile (sono soddisfazioni...).

Ma c'è anche una cattiva notizia molto somigliante alla seconda descritta sopra:

  • L'uso improprio di volatile potrebbe anche fare molti danni: nel senso che se la usate per scopi che non sono quelli previsti dallo standard, è molto probabile che il codice abbia dei malfunzionamenti veramente strani e difficili da capire, roba da sindrome di "mal di testa del programmatore".

E veniamo al dunque: nel titolo di questo articolo preannunciavo la descrizione del come, quando e perché usare volatile in C, per cui ora ci tocca cominciare con:

IL COME:

Ricordiamo che volatile è un type qualifier, e quindi si usa (solo) per aggiungere una proprietà a un tipo qualsiasi già esistente, quindi il come è semplicissimo e, ad esempio, possiamo scrivere:

int dummy; // un int normalissimo che si chiama dummy
volatile int vol_dummy; // un int di tipo volatile che si chiama vol_dummy

E approfitto l'occasione per ricordare che i type qualifier sono solo quattro, e che gli altri sono const, restrict e _Atomic : magari ne parleremo in un prossimo articolo (magari uno per ogni qualificatore). E ora siamo pronti a passare al prossimo punto:

IL QUANDO:

In questo caso è sufficiente ricordare i motivi "storici" alla base dell'uso di volatile, quattro motivi raggruppati sotto la definizione: "in tutte situazioni in cui il valore della variabile può cambiare senza azione da parte del codice visibile". E vediamoli, questi quattro quando (spoiler: il quarto è sbagliatissimo):

  1. Quando ci si interfaccia con un Hardware che cambia il valore stesso della variabile.
  2. Quando c'è un gestore di segnali che potrebbe cambiare il valore della variabile.
  3. Quando una variabile cambia tra un setjmp e il longjmp collegato.
  4. Quando c'è un altro thread in esecuzione che usa anche lui la stessa variabile (ERRORE! Anzi, ORRORE!)

Ecco, il punto 4 è veramente sbagliato, ma non ve lo spiegherò direttamente, lo vedremo nel prossimo punto, che è:

IL PERCHÉ:

Ma perché scrivendo del codice bisogna informare qualche entità suprema che la variabile è volatile? Ma che gliene frega all'entità suprema (che è, in questo caso, il compilatore) che qualcuno esternamente potrebbe modificare il valore della variabile? Sembrerebbe più un problema di run-time che di compile-time, eppure... beh, la risposta è semplice: perché funzioni il tutto  il compilatore deve sviluppare del codice-macchina che sia a conoscenza che quella variabile è speciale, che quella variabile non cambia per le operazioni dirette scritte nel codice ma cambia per alcune ingerenze esterne.

E tutto questo è strettamente collegato alle ottimizzazioni: dovete sapere (beh, immagino che lo sappiate già) che i compilatori hanno la pessima (o ottima, dipende dai punti di vista) abitudine di ottimizzare il codice (...vedi al proposito questo ottimo articolo...). Quindi, il compilatore ha bisogno di sapere se la variabile è volatile, per evitare di eseguire ottimizzazioni dannose su un codice apparentemente "strano" come può apparire un codice che ha veramente bisogno di variabili volatili (l'ottimizzatore del compilatore non può capire tutti i dettagli se non glieli esplicitiamo).

E adesso vi propongo un esempio semplicissimo, che è uno dei tanti possibili (è veramente, ma veramente, semplificato, ma è solo per rendere l'idea):

int main()
{
int dummy = 0; // per semplificare, ma dovrebbe essere esterna

while (dummy == 0) {
// eseguo qualcosa...
;
}

return 0;
}

ecco, questo semplice codice verrà trasformato dal compilatore (anche con ottimizzazioni minime, ad esempio usando -O1  nel caso del GCC ) in:

int main()
{
int dummy = 0; // per semplificare, ma dovrebbe essere esterna

while (1) {
// eseguo qualcosa...
;
}

return 0;
}

Questo perché il test della variabile nel while è inutile, già che la condizione è sempre vera.

Ma cosa succede se la mia variabile dummy è una abilitazione proveniente da una variabile mappata direttamente nell'Hardware (un input digitale, per esempio)? Succede che il test che sembrava inutile in realtà deve essere sempre fatto, e quindi mio codice non funziona più! E che si fa allora? Si fa questo:

int main()
{
volatile int vol_dummy = 0; // per semplificare, ma dovrebbe essere esterna

while (vol_dummy == 0) {
// eseguo qualcosa...
;
}

return 0;
}

In questo caso il compilatore è informato che non deve assolutamente ottimizzare il loop, perché è basato su una variabile volatile, e se provate a ottenere l'assembler dalla compilazione ve ne renderete facilmente conto (certo, spiegare dettagli della programmazione C usando l'assembler è un po' fuorviante, ma in questo caso ci sta bene). E questo spiega anche la seconda buona notizia descritta sopra: se usiamo volatile dove non ce n'è bisogno, alla fin fine solo stiamo disabilitando le ottimizzazioni per una parte limitata del codice, e questo non provoca malfunzionamenti, ma, al massimo, una riduzione delle prestazioni.

E la allora, perché il punto 4 del quando era sbagliato? Questo ci porta alla cattiva notizia descritta in testa all'articolo: se usiamo volatile dove non bisogna usarla, potremmo avere dei problemi, e questo nasce da un vecchio malinteso: il motivo per cui la variabile si intende come "modificabile esternamente"  è valido solo per i primi tre casi elencati sopra, ma non include le modifiche effettuate da un altro thread. Una operazione thread-safe su una variabile deve essere atomica (usando i costrutti forniti dal linguaggio) oppure deve essere protetta dai soliti metodi di sincronizzazione disponibili: mutex, spinlock, ecc. Le operazioni su una volatile non sono atomiche, e quindi non sono adatte per eseguire sincronizzazione (e questo è specificato anche nei vari standard (C, POSIX e altri).

E per oggi penso che possa bastare, e spero di non avere aggiunto confusione a un argomento già di per sé abbastanza confuso...

Ciao, e al prossimo post!

venerdì 18 giugno 2021

Better Call Go
come scrivere Server e Client TCP in Go e C - pt.2

signora: Ed io che pensavo che tutti gli avvocati fossero idioti!
Jimmy McGill (Saul Goodman): Solo la metà sono idioti, l'altra sono truffatori.

Dove eravamo rimasti? Ah, si, nell'ultimo articolo vi avevo proposto un confronto tra un Server TCP scritto in C (il nostro amato C) e uno scritto in Go (la nostra ultima fiamma). L'ispirazione proveniva dalla "vulcanicità" di Jimmy"Saul"McGill, l'avvocato "multi-piattaforma" delle (oramai) mitiche serie Breaking Bad e Better Call Saul: Saul ci insegna che non bisogna fossilizzarci sulle nostre abitudini, che dobbiamo esplorare nuovo orizzonti e avere sempre nuove ispirazioni, che bisogna essere dei "vulcani di idee". E quindi: il C è un grande e insostituibile linguaggio, ma se in alcuni campi il Go ci permette di scrivere più rapidamente e altrettanto bene la stessa applicazione perché non usarlo? Io poi lo trovo divertente e leggero, mi sento veramente a mio agio e senza preoccupazioni e restrizioni quando lo uso.

(...esattamente il contrario, ma proprio il contrario, di quando sono costretto a mettere le mani su codice che è stato scritto da qualche fanatico della programmazione generica in C++... ebbene si, in quel caso riesco a lanciare molte maledizioni con grande ritmo e fluidità. Ma questa è un'altra storia...).

...e allora ti spiego: questo è un Server e quello è un Client...

Veniamo al dunque: i due Server TCP li abbiamo già visti. Ci mancano i Client TCP, e cominciamo anche stavolta con l'esempio in C, che anche in questo caso è, 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

int main(int argc, char *argv[])
{
// test argomenti
if (argc != 3) {
// errore di chiamata
printf("%s: numero argomenti errato\n", argv[0]);
printf("uso: %s host port [e.g.: %s 127.0.0.1 9999]\n", argv[0], argv[0]);
return EXIT_FAILURE;
}

// creo il socket in modo internet/TCP
int cli_sock;
if ((cli_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 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(argv[1]); // set indirizzo del server
server.sin_port = htons(atoi(argv[2])); // set port del server

// mi connetto al server remoto
if (connect(cli_sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
// errore connect()
printf("%s: errore connect (%s)\n", argv[0], strerror(errno));
close(cli_sock);
return EXIT_FAILURE;
}

// loop di comunicazione col server remoto
for (;;) {
// compongo un messaggio per il server remoto
char my_msg[MYBUFSIZE];
printf("Scrivi un messaggio per il Server remoto: ");
scanf("%s", my_msg);

// invio il messaggio al server remoto
if (send(cli_sock, my_msg, strlen(my_msg), 0) == -1) {
// errore send()
printf("%s: errore send (%s)\n", argv[0], strerror(errno));
close(cli_sock);
return EXIT_FAILURE;
}

// ricevo una risposta dal server remoto
memset(my_msg, 0, MYBUFSIZE);
if (recv(cli_sock, my_msg, MYBUFSIZE, 0) == -1) {
// errore recv()
printf("%s: errore recv (%s)\n", argv[0], strerror(errno));
close(cli_sock);
return EXIT_FAILURE;
}

// mostro la risposta
printf("%s: il server risponde: %s\n", argv[0], my_msg);
}

// esco con Ok
return EXIT_SUCCESS;
}

E, come sempre, il codice è esageratamente commentato e decisamente 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 una descrizione del flusso che è quello classico ed elementare di un Client TCP:

  1. creo il socket in modo internet/TCP
  2. preparo la struttura sockaddr_in per il server remoto
  3. mi connetto al server remoto
  4. loop di comunicazione col server remoto
    - compongo un messaggio per il server remoto
    - invio il messaggio al server remoto
    - ricevo e mostro la risposta del server remoto

ovviamente esistono varianti di questa struttura, ma questa è quella classica. Come avrete notato nel loop di comunicazione c’è l'attesa del messaggio di risposta, e questo ci aiuta, durante l'esecuzione, a osservare il corretto funzionamento della coppia Client/Server che metteremo in prova.

Anche in questo caso (come per il Server) il codice è compatto e ben leggibile, ed è, quindi, facilissimo da manutenere. Ma... e la versione in Go? Vediamola!

package main

import (
"bufio"
"fmt"
"net"
"os"
)

func main() {
// test argomenti
if len(os.Args) != 3 {
// errore di chiamata
fmt.Printf("%s: numero argomenti errato\n", os.Args[0])
fmt.Printf("uso: %s host port [e.g.: %s 127.0.0.1 9999]\n", os.Args[0], os.Args[0])
return
}

// mi connetto al server remoto
addr := os.Args[1] + ":" + os.Args[2] // set indirizzo (i.e.: "host:port")
conn, err := net.Dial("tcp", addr) // set dial con network di tipo TCP
if err != nil {
// errore di connessione
fmt.Println(err)
return
}

// loop di comunicazione col server remoto
connrdr := bufio.NewReader(conn) // reader sulla connessione
stdinrdr := bufio.NewReader(os.Stdin) // reader sullo standard input
for {
// compongo un messaggio per il server remoto
fmt.Print("Scrivi un messaggio per il Server remoto: ")
client_msg, _ := stdinrdr.ReadString('\n')

// invio il messaggio al server remoto
_, err = conn.Write([]byte(client_msg)) // scrivo sulla connessione
if err != nil {
// errore di invio
fmt.Println(err)
return
}

// ricevo una risposta dal server remoto
server_msg, err := connrdr.ReadString('\n') // leggo con il conn reader
if err != nil {
// errore di lettura
fmt.Println(err)
return
}

// mostro la risposta
fmt.Printf("%s: il server risponde: %s", os.Args[0], server_msg)
}
}

Io questa versione la trovo fantastica! I passi sono esattamente gli stessi dell'esempio in C (e ci mancherebbe che non lo siano), ma è tutto più semplice e lineare, una vera sciccheria!

E, per chi non conoscesse ancora il Go, faccio notare che buona parte della semplificazione è dovuta alla semplicità intrinseca del linguaggio (solo 25 keywords, mentre C99 ne ha 37 e C++11 ne ha 84 e, prevedo, C++2099 ne avrà 9999...), ma è anche dovuta al meccanismo dei "Package", che sono una specie di via di mezzo tra gli "#include" del C, la STL del C++ e le librerie di C e C++: una volta identificato quale Package esegue la funzionalità che ci serve, lo si importa (l'istruzione "import" all'inizio del sorgente) e lo si usa. Tutto qua! Un buon esempio è l'uso del Package fmt  (formatted I/O) nel classicissimo programma "hello world" in Go:

package main

import "fmt" // importa il Package fmt (formatted I/O)

func main() {
fmt.Println("hello, world")
}

la successiva operazione di "build" dell'eseguibile fa tutto in automatico (scordatevi delle operazioni di link da mettere nei makefile di C e C++): basta importare, usare e "buildare". Go è un gran linguaggio, è veramente ad alto livello ma mantiene allo stesso tempo dettagli da "basso livello" (scusate il gioco di parole) che ti permettono un range di utilizzazione enorme.

E, prima dei saluti di rito, vi lascio con una nuova citazione del mitico Rob Pike (l'altra la trovate qui) sulle idee alla base del Go:

"Notice that Robert [Griesmer] said C was the starting point, not C++. I'm not
certain but I believe he meant C proper, especially because Ken [Thompson] was 
there. But it's also true that, in the end, we didn't really start from C. We 
built from scratch, borrowing only minor things like operators and brace 
brackets and a few common keywords. (And of course we also borrowed ideas from 
other languages we knew.) In any case, I see now that we reacted to C++ by 
going back down to basics, breaking it all down and starting over. We weren't 
trying to design a better C++, or even a better C. It was to be a better 
language overall for the kind of software we cared about."

                                da "Less is exponentially more", Rob Pike, 2012

Vi ricordo che il codice di questo post (e di alcuni dei precedenti) lo trovate sul mio repository GitHub.

Ciao, e al prossimo post!

venerdì 14 maggio 2021

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

  1. creo il socket in modo internet/TCP
  2. preparo la struttura sockaddr_in per questo Server
  3. associa l'indirizzo del server al socket
  4. start ascolto con una coda di max BACKLOG connessioni
  5. accetta connessioni da un Client entrante
  6. 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:

  1. start ascolto sul port richiesto
  2. accetta connessioni da un Client entrante
  3. 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!