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.

Visualizzazione post con etichetta Go. Mostra tutti i post
Visualizzazione post con etichetta Go. Mostra tutti i post

giovedì 11 luglio 2024

Furiosa Go
come spedire una struttura in Go - pt.2

Smeg: Chi sono?
Dementus: Qualcuno competente ed eccessivamente risentito.
Smeg: Cosa pensi che vogliano?
Dementus: Me, senza il mio equipaggio.

Ed eccoci di nuovo sul pezzo. E, visto che si tratta della seconda parte dell'articolo Fury Go, non posso non agganciarmi allo splendido prequel di Mad Max: Fury Road, e cioè a Furiosa: A Mad Max Saga diretto sempre dal Maestro George Miller. E, in effetti, anche questo articolo è un prequel: nell'altro avevo descritto come spedire una struttura in Go via IPC socket (UNIX domain socket), e il Software l'avevo scritto partendo da una mia versione "base" che inviava solo dei semplici testi. Ed ecco, in questa seconda parte vi mostrerò, come prequel, la versione base, dimostrandovi, come promesso, che le prestazioni buone ma non eccellenti del benchmark erano dovute (spoiler) solo alla codifica della struttura, perché con i soli testi il Go va come un treno!

...adesso vi faccio vedere cosa sa fare il Go...

Ok, e allora andiamo con il prequel, descrivendo la versione base che ho scritto prima di quella che usa le strutture. Siamo pronti? Si? E allora partiamo direttamente con il codice!

// reader.go - main processo figlio: è un reader (un server) su IPC socket
package main

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

// funzione main
func main() {

// start ascolto sul file di scambio "myipcs" (con UNIX domain socket)
fmt.Printf("processo %d partito (reader)\n", os.Getpid())
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
lner, err := net.ListenUnix("unix", &addr)
if err != nil {
// errore listen
fmt.Println(err)
return
}

// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio
defer lner.Close()
defer os.Remove("./myipcs")

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()
if err != nil {
// errore accept
fmt.Println(err)
return
}

// set time di partenza per calcolare il tempo impiegato
start := time.Now()

// loop di lettura messaggi dal writer
n_msg := 0
connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')
if err != nil {
// errore di lettura
fmt.Println(err)
return
}

// test numero messaggi per forzare l'uscita
n_msg++
if n_msg == 2000000 {
// il processo chiude la connessione ed esce per numero raggiunto
fmt.Printf("reader: ultimo messaggio ricevuto: %s", client_msg)
fmt.Printf("reader: processo %d terminato (messaggi=%d tempo totale:%s)\n",
os.Getpid(), n_msg,
time.Since(start).Truncate(time.Millisecond).String())
conn.Close()
return
}
}
}
// writer.go - main processo figlio: è un writer (un client) su IPC socket
package main

import (
"fmt"
"net"
"os"
"time"
)

// funzione main
func main() {

// mi assicuro che il writer parta dopo il reader
fmt.Printf("processo %d partito (writer)\n", os.Getpid())
time.Sleep(100 * time.Millisecond)

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)
if err != nil {
// errore dial
fmt.Println(err)
return
}

// loop di scrittura messaggi per il reader
var my_text string
index := 0
for {
// test index per forzare l'uscita
if index == 2000000 {
// il processo chiude la connessione ed esce per indice raggiunto
fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",
os.Getpid(), my_text, index)
conn.Close()
return
}

// compongo il messaggio e lo invio
index++
my_text = fmt.Sprintf("un-messaggio-di-test:%d\n", index)

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

Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato anche questa volta gli IPC socket. Poi ho anche scritto la versione con i Network Socket, ma non vi mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C vista qui (in questo caso era la versione "fast"), ed è anche quasi identico alla versione con le strutture dello scorso articolo. Come sempre il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, però mi interessa, a questo punto, aggiungere qualche dettaglio per far notare le (poche) differenze rispetto a quello dello scorso articolo.

Cominciamo, allora, con il writer: in entrambe versioni si crea, inizialmente, un oggetto "connessione" conn:

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)

Poi, nella versione con le strutture, si invia il messaggio attraverso il codec encoding/gob passandogli la connessione:

// set encoder e spedizione dall'encoder
encoder := gob.NewEncoder(conn)
err = encoder.Encode(message)

Invece, nella versione base si scrive, direttamente, con l'oggetto connessione creato all'inizio:

// invio il messaggio al server remoto
_, err = conn.Write([]byte(my_text))

E ora passiamo al reader: anche qui, in entrambe versioni, si crea un oggetto "connessione" conn:

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()

Poi, nella versione con le strutture, si riceve il messaggio attraverso il codec passandogli la connessione:

// set decoder e ricezione dal decoder
decoder := gob.NewDecoder(conn)
decoder.Decode(&message)

Invece, nella versione base, si legge con un oggetto "Reader" (della libreria bufio) creato sulla connessione vista sopra:

connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')

Come avrete notato le differenze sono poche ma significative. Notare anche che nella versione con le strutture il testo del messaggio è una stringa dentro la struttura Message, mentre nella versione base il messaggio è direttamente una stringa terminata con un "newline": questo è un particolare importante, perché in fase di ricezione con la funzione ReadString appena vista è necessario specificare qual'è il terminatore di stringa.

E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati dei benchmark delle "versioni base" che inviano solo testi in Go:

aldo@Linux $ cd ../go-ipcsocketbase/
aldo@Linux $ ./processes
sono il padre (15381): attendo la terminazione dei figli
sono il figlio 1 (15382): eseguo il nuovo processo
sono il figlio 2 (15383): eseguo il nuovo processo
processo 15382 partito (reader)
processo 15383 partito (writer)
writer: processo 15383 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15382 terminato (messaggi=2000000 tempo totale:3.058s)
sono il padre (15381): figlio 15382 terminato (0)
sono il padre (15381): figlio 15383 terminato (0)
./processes: processi terminati
aldo@Linux $ cd ../go-socketbase/
aldo@Linux $ ./processes
sono il padre (15408): attendo la terminazione dei figli
sono il figlio 1 (15409): eseguo il nuovo processo
sono il figlio 2 (15410): eseguo il nuovo processo
processo 15410 partito (writer)
processo 15409 partito (reader)
writer: processo 15410 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15409 terminato (messaggi=2000000 tempo totale:8.46s)
sono il padre (15408): figlio 15409 terminato (0)
sono il padre (15408): figlio 15410 terminato (0)
./processes: processi terminati

A questo punto i risultati parlano senza temi di smentite: i tempi realizzati sono decisamente migliori di quelli mostrati per le versioni che spedivano strutture complesse, quindi è evidente che la maggior parte del tempo di CPU se la mangiava la libreria specializzata encoding/gob, che funziona bene però, a quanto pare, non è un fulmine. E, grazie ai test appena mostrati sopra si può affermare (come anticipato nella prima parte dell'articolo) che il Go è un linguaggio notevolmente veloce, alla faccia di chi pensa il contrario... Notare che la versione IPC con i suoi 3.058s è addirittura veloce come la versione in C (che impiegava 3.309s ma con messaggi leggermente più lunghi a causa della presenza dell'indice)! Anche in questo caso (come già nello scorso articolo) la versione con i Network socket è un po' più lenta (8.46s) ma è, comunque, sufficientemente veloce.

Ok, credo che, per il momento si può chiudere la parentesi Go sulla comunicazione tra processi: credo che i risultati siano stati interessanti, specialmente per il fatto di avere confrontato codici analoghi per C e Go. Non so di cosa parlerò` prossimamente: in questo momento ho in mente solo le prossime (meritate) vacanze. Ci sentiremo più avanti, ben rilassati e pronti per nuove avventure in C (o in Go...)!

Ciao, e al prossimo post!

sabato 22 giugno 2024

Fury Go
come spedire una struttura in Go - pt.1

Toast: Che stai facendo?
Dag: Prego.
Toast: Chi preghi?
Dag: Chiunque ci ascolti.

Nell'ultimo articolo avevo giurato che l'argomento Fast IPC era, "almeno momentaneamente", chiuso. Poi, mentre riguardavo, per l'ennesima volta, lo stupendo Mad Max: Fury Road del Maestro George Miller, ho avuto un flash (si, ma non vi preoccupate, è durato solo un microsecondo, mentre guardo un film sono sempre molto concentrato). Il flash era questo: "E se ripetessi i test IPC socket usando il Go?" L'idea era intrigante e, alla fine, ho ceduto a me stesso. E così ho anche capito che riesco, con grande facilità, a non mantenere le promesse... avrò mica un gran futuro come politico? ah ah ah.

...e se provassi a farlo con il Go?...

E allora veniamo al dunque: il titolo qui sopra "come spedire una struttura in Go" è un po' fuorviante riguardo alla premessa iniziale: l'idea è ripetere alcuni benchmark usando il Go (Golang per gli amici) però per farlo bisogna scontrarsi un po' con una delle differenze che ha questo linguaggio rispetto al C (e al C++); come ricorderete (e se no potreste fare una rapida rilettura di quel vecchio post) i test erano basati sull'invio "a raffica" di molti messaggi (2000000!) composti così:

// struttura Data per i messaggi
typedef struct {
unsigned long index; // indice dei dati
char text[1024]; // testo dei dati
} Data;

La scelta di questa struttura non era casuale: avrei potuto, più semplicemente, inviare solo dei messaggi di testo, ma avevo deciso di inviare dati complessi ("Data" contiene solo due campi ma potrebbe contenerne moltissimi) per rimarcare che con il C è usuale trattare dati di qualsiasi tipo, e chi riceve un messaggio "complesso" lo può ricostruire semplicemente depositandolo in una variabile dello stesso tipo (ah, la potenza del C...). E poi, grazie alla struttura "Data" ho potuto anche mostrare come gestire un indice dei messaggi, il che non guasta mai.

E il Go? Come ben sapete (e ne ho parlato qui) il Go è un vero linguaggio ad alto livello, con tutti i pro e i contro che questo comporta. Tra i pro c'è, ovviamente, il fatto che è possibile scrivere applicazioni anche complesse con notevole semplicità e compattezza, sicuramente più del C (e C++). Però, quando si tratta di maneggiare dati a livello base (o meglio binario) il Go entra un po' in difficoltà, e questo è il caso che stiamo trattando: spedire (e ricevere) messaggi complessi (strutture) non è per nulla semplice e scontato come lo è per il nostro amato C. Ma è, comunque, possibile: e tra poco vedremo come e con quali prestazioni.

E ora, bando alle ciance, facciamo cantare il codice! Vediamo come sono i nostri reader.go e writer.go (equivalenti, più o meno, ai reader.c e writer.c visti qui). Per eseguire il benchmark è presente anche l'onnipresente processes.c, che vi risparmio perché è rimasto invariato. Vai col codice!

// reader.go - main processo figlio: è un reader (un server) su IPC socket
package main

import (
"encoding/gob"
"fmt"
"net"
"os"
"time"
)

// struttura Message per i messaggi
type Message struct {
Index int // indice dei dati
Text string // testo dei dati
}

// funzione main
func main() {

// start ascolto sul file di scambio "myipcs" (con UNIX domain socket)
fmt.Printf("processo %d partito (reader)\n", os.Getpid())
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
lner, err := net.ListenUnix("unix", &addr)
if err != nil {
// errore listen
fmt.Println(err)
return
}

// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio
defer lner.Close()
defer os.Remove("./myipcs")

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()
if err != nil {
// errore accept
fmt.Println(err)
return
}

// set time di partenza per calcolare il tempo impiegato
start := time.Now()

// loop di lettura messaggi dal writer
n_msg := 0
var message Message
for {
// set decoder e ricezione dal decoder
decoder := gob.NewDecoder(conn)
decoder.Decode(&message)

// test numero messaggi per forzare l'uscita
n_msg++
if n_msg == 2000000 {
// il processo chiude la connessione ed esce per numero raggiunto
fmt.Printf("reader: ultimo messaggio ricevuto: %s\n", message.Text)
fmt.Printf("reader: processo %d terminato (messaggi=%d tempo totale:%s)\n",
os.Getpid(), n_msg,
time.Since(start).Truncate(time.Millisecond).String())
conn.Close()
return
}
}
}
// writer.go - main processo figlio: è un writer (un client) su IPC socket
package main

import (
"encoding/gob"
"fmt"
"net"
"os"
"time"
)

// struttura Message per i messaggi
type Message struct {
Index int // indice dei dati
Text string // testo dei dati
}

// funzione main
func main() {

// mi assicuro che il writer parta dopo il reader
fmt.Printf("processo %d partito (writer)\n", os.Getpid())
time.Sleep(100 * time.Millisecond)

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)
if err != nil {
// errore dial
fmt.Println(err)
return
}

// loop di scrittura messaggi per il reader
var message Message
message.Index = 0
for {
// test index per forzare l'uscita
if message.Index == 2000000 {
// il processo chiude la connessione ed esce per indice raggiunto
fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",
os.Getpid(), message.Text, message.Index)
conn.Close()
return
}

// compongo il messaggio e lo invio
message.Index++
message.Text = fmt.Sprintf("un-messaggio-di-test:%d", message.Index)

// set encoder e spedizione dall'encoder
encoder := gob.NewEncoder(conn)
err = encoder.Encode(message)
if err != nil {
fmt.Println("errore di codifica: ", err)
return
}
}
}

Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato per il test gli IPC socket (UNIX domain socket). Poi ho ripetuto anche con i Network Socket, ma non mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C citata (che in questo caso era la versione "fast").

Però la complessità dell'operazione di spedire strutture complesse è mascherata dall'uso di un libreria specializzata, la encoding/gob, senza la quale il codice sarebbe molto più complesso (ebbene si, una libreria specializzata per una operazione semplice per il C ma complicata per il Go). E, come vedremo tra poco, le prestazioni non sono eccellenti come ci si aspetterebbe (spoiler: per colpa della encoding/gob). Comunque il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, per cui non mi dilungherò in spiegazioni superflue.

E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati del benchmark in Go e, per comparazione, vi riporto anche i risultati della versione C:

aldo@Linux $ cd go-fastipcsocket/
aldo@Linux $ ./processes
sono il padre (18903): attendo la terminazione dei figli
sono il figlio 1 (18904): eseguo il nuovo processo
sono il figlio 2 (18905): eseguo il nuovo processo
processo 18905 partito (writer)
processo 18904 partito (reader)
writer: processo 18905 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000)
sono il padre (18903): figlio 18905 terminato (0)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 18904 terminato (messaggi=2000000 tempo totale:13.081s)
sono il padre (18903): figlio 18904 terminato (0)
./processes: processi terminati
aldo@Linux $ cd fastipcsocket/
aldo@Linux $ ./processes
sono il padre (14990): attendo la terminazione dei figli
sono il figlio 1 (14991): eseguo il nuovo processo
sono il figlio 2 (14992): eseguo il nuovo processo
processo 14991 partito (reader)
processo 14992 partito (writer)
writer: processo 14992 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000)
sono il padre (14990): figlio 14992 terminato (0)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 14991 terminato (messaggi=2000000 tempo CPU: 3.309 - tempo totale:3.309s)
sono il padre (14990): figlio 14991 terminato (0)
./processes: processi terminati

Ebbene si, per trattare 2000000 (!) di messaggi la versione C ha bisogno di 10 secondi in meno! (13.081s vs 3.309s). Però, a questo punto, bisogna fare qualche considerazione:

  1. Come versione di riferimento in C ho usato quella "fast", visto che il meccanismo della versione Go è a size variabile ed è, quindi, somigliante. Comunque anche usando la versione C "normal" la differenza è alta: 8 secondi (13.081s vs 4.823s). (Ho scritto un sacco di benchmark... ma non ve li mostro tutti per non farvi addormentare, ah ah ah).
  2. Vi riporto, per curiosità, i risultati delle versioni con i Network Socket: 26.794s per il Go e 3.88s per il C. Questo era previsto, gli IPC socket essendo "locali" sono mediamente più veloci dei Network Socket, anche se il peggioramento della differenza Go vs C un po' sorprende.
  3. Comunque, non fatevi ingannare dalle prestazioni: in termini assoluti 13.081s (e 26.794s) per 2000000 di messaggi sono, comunque, pochi! Il Go è un linguaggio veloce!

E, riguardo al punto 3 appena mostrato qui sopra, vi cito lo spoiler accennato poco fa (...per colpa della "encoding/gob"...): nella seconda parte dell'articolo (in arrivo prossimamente su questi schermi) vi faro vedere di che cosa è capace il Go quando maneggia solo testi.

Ok, per oggi può bastare: per il momento vi saluto, e vi raccomando, come sempre, di non trattenere il respiro in attesa della seconda parte (potrebbe nuocere gravemente alla vostra salute, ah ah ah).

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!