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

venerdì 17 luglio 2026

MQTT
come usare MQTT in C - pt.3

Logan: Lo sai che sono tutte balle, vero? Sarà successo un quarto di questa roba e non così. E nel mondo reale le persone muoiono e nessun presuntuoso in calzamaglia del cazzo può evitarlo!

E siamo giunti, finalmente, alla terza e ultima puntata di "MQTT - come usare MQTT in C". E visto che la coerenza per me è molto importante, in questo articolo mostrerò come usare MQTT in Go (ah ah ah).

Beh, come ben sanno i miei lettori più assidui, io sono, fondamentalmente, un fanatico del C, però uso spesso (per lavoro) anche il Go e non disdegno di parlarne nei miei articoli; sarà, magari, perché è un bel linguaggio C-like, creato da due storici (e mitici) guru del mondo UNIX, Ken Thompson e Rob Pike, affiancati dal bravissimo Robert Griesemer (anche lui proveniente dal mondo UNIX). E non solo: il Go è la risposta di questi tre grandi informatici (e di Google) alle esagerate complicazioni introdotte nel C++11 (e non avevano ancora visto i successivi C++14/17/20/23!!!). In fondo all'articolo, per chi ha voglia di leggerlo, citerò uno scritto di Rob Pike sulla genesi del Go (che ho già citato qui e qui).

E la frase di Logan qui sopra? Beh, io direi che calza a pennello: nel mondo reale (della programmazione) bisogna fare scelte oculate (fregandosene delle mode) e il Go, per alcuni progetti, è una scelta oculata.

...meno balle: oggi usiamo il Go!...

Orbene, veniamo al dunque: ho scritto un programma mqttclient.go che è una replica delle due versioni in C pubblicate qui e qui. L'idea era che si potesse eseguire esattamente nello stesso modo e, ovviamente, produrre gli stessi risultati. Ok, vediamo un po se ci sono riuscito. Vai col codice!

// mqttclient.go - un semplice client mqtt in Go
package main

import (
"fmt"
"time"
"os"
mqtt "github.com/eclipse/paho.mqtt.golang"
)

// funzione main
func main() {

// test del numero di argomenti
argc := len(os.Args)
if (argc != 4) {
// errore: numero errato di argomenti
fmt.Printf("%s: numero errato di argomenti\n", os.Args[0])
fmt.Printf("uso: %s clientId topicPub topicSub (e.g.: %s client1 test/topic2 test/topic1)\n", os.Args[0], os.Args[0])
os.Exit(1)
}

// set delle opzioni di connessione
opts := mqtt.NewClientOptions()
opts.AddBroker(fmt.Sprintf("tcp://%s:%d", "localhost", 1883))
opts.SetClientID(os.Args[1])
opts.SetDefaultPublishHandler(onMessage)
opts.OnConnect = onConnect
opts.OnConnectionLost = onConnectionLost

// mi collego
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}

// eseguo subscribe e publish
subscribe(client)
publish(client)

// mi disconnetto
time.Sleep(2 * time.Second)
client.Disconnect(250)
}

// publish - funzione per pubblicare sui topic
func publish(client mqtt.Client) {

// pubblico messaggi sull'altro topic (topicPub in "uso: %s clientId topicPub topicSub")
for i := 0; i <= 2; i++ {
// attendo 2 secondi prima di pubblicare un messaggio
time.Sleep(2 * time.Second)

// pubblico
text := fmt.Sprintf("messaggio %d dal client %s", i, os.Args[1])
token := client.Publish(os.Args[2], 0, false, text)
token.Wait()
fmt.Printf("Messaggio %d publicato sul topic %s\n", i, os.Args[2]);
}
}

// subscribe - funzione per sottoscriversi ai topic
func subscribe(client mqtt.Client) {

// mi sottoscrivo a un topic (topicSub in "uso: %s clientId topicPub topicSub")
token := client.Subscribe(os.Args[3], 1, nil)
token.Wait()
fmt.Printf("%s: mi sottoscrivo al topic %s\n", os.Args[1], os.Args[3]);
}

// onMessage - una callback per i messaggi che arrivano sul topic sorvegliato
var onMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {

fmt.Printf("messaggio ricevuto sul topic %s: \"%s\"\n", msg.Topic(), msg.Payload())
}

// onConnect - una callback per gestire la connessione
var onConnect mqtt.OnConnectHandler = func(client mqtt.Client) {

//fmt.Println("Connected")
}

// onConnectionLost - una callback per gestire la disconnessione
var onConnectionLost mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {

fmt.Printf("Connection lost: %v", err)
}

Come avrete notato, il codice è abbastanza semplice e super-commentato, quindi con c'è molto da spiegare, anche se per i lettori completamente a digiuno del Go non tutto sarà chiarissimo... ma questo non è un corso rapido di Go (ah ah ah). Vi do un consiglio spassionato: se vi interessa ampliare i vostri orizzonti e avere, anche, nuovi e interessanti sbocchi nel mondo del lavoro, studiatevi il Go! Si può cominciare con l'ottimo corso Go by Example, che è gratis ed è proposto e curato direttamente dal team ufficiale del Go (io ho cominciato, molti anni fa, proprio con questo corso e poi ho proseguito come autodidatta).

Chiusa la parentesi posso dire che, a parte eventuali incomprensioni da linguaggio, se fate un confronto con gli analoghi Client scritti in C dei precedenti articoli, noterete che il codice è veramente molto simile ed è moderatamente più compatto, visto che il Go è, praticamente, un C ad alto livello.

Faccio nuovamente notare che anche questo esempio si collega a un broker  locale (ossia installato sulla macchina stessa di esecuzione), quindi il collegamento è fatto con l’indirizzo: “tcp://localhost:1883”. Ma ci sono anche dei broker pubblici disponibili in rete che funzionano esattamente come quello locale. Nel mio caso (Linux) il broker  locale che uso è Mosquitto (disponibile anche per Windows) e funziona alla grande.

Ed è giunta l'ora di compilare ed eseguire. Qui, devo aggiungere qualche dettaglio: nelle versioni relativamente più recenti del Go (dalla versione 1.11) la compilazione dei programmi passa attraverso i Go Modules che sono il sistema standard ufficiale per la gestione delle dipendenze nei progetti Go. Questo semplifica un po' la gestione rispetto al precedente sistema (che non è il caso di descrivere qui). In parole povere: prima di compilare bisogna preparare un file che si chiama go.mod, che normalmente sta nella stessa directory dove ci sono i sorgenti. Per crearlo ho scritto un piccolo script-shell (buildmod.sh) per facilitarmi (e facilitarvi) la vita: eccolo!

#!/bin/bash
rm go.mod
rm go.sum
rm mqttclient
go mod init mqttclient
go mod tidy
lanciando lo script vedrete questo:
aldo@Linux $ ./buildmod.sh
go: creating new go.mod: module mqttclient
go: to add module requirements and sums:
go mod tidy
go: finding module for package github.com/eclipse/paho.mqtt.golang
go: found github.com/eclipse/paho.mqtt.golang in github.com/eclipse/paho.mqtt.golang v1.5.1

una volta creato il go.mod (solo per la prima compilazione, non serve ricrearlo ogni volta) si può compilare ed eseguire. Compiliamo (e questo è veramente semplicissimo):

aldo@Linux $ go build

ed eseguiamo in due terminali diversi (possibilmente in quasi-contemporanea, per non perdersi messaggi: se siete troppo lenti, quando il secondo client parte il primo potrebbe già aver pubblicato tutti i suoi messaggi, ah ah ah. Comunque le "strane" sleep che ho aggiunto sono proprio per rendere leggibili i risultati). Il risultato è questo:

TERMINALE 1:

aldo@Linux $ ./mqttclient Client2 test/topic1 test/topic2
Client2: mi sottoscrivo al topic test/topic2
messaggio ricevuto sul topic test/topic2: "messaggio 0 dal client Client1"
Messaggio 0 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 1 dal client Client1"
Messaggio 1 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 2 dal client Client1"
Messaggio 2 publicato sul topic test/topic1

TERMINALE 2:

aldo@Linux $ ./mqttclient Client1 test/topic2 test/topic1
Client1: mi sottoscrivo al topic test/topic1
Messaggio 0 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 0 dal client Client2"
Messaggio 1 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 1 dal client Client2"
Messaggio 2 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 2 dal client Client2"

Visto? Anche questa versione in Go funziona perfettamente (e identicamente) alle altre scritte in C!

Ok, per oggi può bastare. Nel prossimo articolo non so ancora di cosa parlerò, ma vi prometto che sarà, come sempre, molto interessante! Ah, come promesso all'inizio dell'articolo, dopo il solito saluto vi proporrò una citazione di Rob Pike sulla genesi del Go. Leggetevi la citazione (e magari anche l'intero articolo di Pike) è molto interessante!

Ciao, e al prossimo post!

"...Intorno a settembre 2007, stavo lavorando a una modifica minore ma fondamentale su un enorme programma C++ di Google, uno con cui tutti voi avete avuto a che fare, e le mie compilazioni impiegavano circa 45 minuti sul nostro gigantesco cluster di compilazione distribuito. Fu annunciato che ci sarebbe stata una presentazione tenuta da un paio di dipendenti di Google membri del comitato per gli standard C++. Avrebbero illustrato le novità in arrivo con il C++0x, come veniva chiamato all’epoca. (Oggi è noto come C++11).

Nel corso di quell’ora di intervento abbiamo sentito parlare di circa 35 nuove funzionalità che erano in fase di progettazione. In realtà ce n’erano molte di più, ma solo 35 sono state descritte durante l’intervento. Alcune delle funzionalità erano minori, ovviamente, ma quelle citate nella presentazione erano almeno abbastanza significative da meritare di essere menzionate. Alcune erano molto sottili e difficili da comprendere, come i riferimenti rvalue, mentre altre sono particolarmente tipiche del C++, come i modelli variadici, e altre ancora sono semplicemente folli, come i letterali definiti dall’utente.

A questo punto mi sono posto una domanda: il comitato del C++ credeva davvero che il problema del C++ fosse che non avesse abbastanza funzionalità? Sicuramente, in una variante della battuta di Ron Hardin, sarebbe un risultato maggiore semplificare il linguaggio piuttosto che arricchirlo. Ovviamente è ridicolo, ma tenete a mente l’idea...” [da “Less is exponentially more”, Rob Pike, 2012]

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!