Better Call Go – come scrivere Server e Client TCP in Go e C – pt.1

B

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 Badma 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 Pikee 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…)

Better Cazll Go
…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!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti