Introduzione

La socket (in inglese “presa“) è una particolare astrazione software che permette ai processi messi in comunicazione di inviare e ricevere dati. Le socket sono nate intorno agli anni ’80, il primo kernel a implementarle fu BSD 4.2 nel 1983.

Client/Server

La struttura base di funzionamento delle socket è di tipo Client/Server. Supponiamo di avere due processi p1 e p2. Il processo p2 ha bisogno del processo p1 per eseguire un determinato compito. Il processo p1 offrirà al processo p2 tale servizio, esso perciò sarà il “Servente” ovvero il Server. Il processo p2 che richiede il servizio sarà dunque il “Cliente”, ovvero il Client.

Tipologie di socket

Esistono quattro tipologie di socket:

  1. Socket che utilizzano i protocolli ARPA di internet (come TCP e UDP).
  2. Gli Unix Domain Socket. Queste socket vengono usate in ambienti POSIX per la comunicazione in locale dei processi.
  3. Socket che utilizzano i protocolli di Xerox Network System.
  4. L’ultima tipologia è quella che utilizza i protocolli della Internationa Standard Association (fa riferimento al modello ISO/OSI).

In questo articolo andremo a vedere solo la prima tipologia.

Come programmare con le socket

Lato server:

– Si genera la socket e la si associa al processo (binding, in italiano “legare”).
– Si mette in ascolto il server per delle nuove connessioni da parte dei client, specificandone il numero.
– A questo punto si accettano le connessioni e avviene la comunicazione.

Lato client:

– Si genera anche qui la socket (in questo caso si definisce l’indirizzo ip del server) e la si associa al processo.
– Si effettua una connessione al server, se viene accettata può incominciare la comunicazione.

Al termine della sessione di comunicazione vengono chiuse tutte le socket aperte ed i vari stream (nel caso di invii di file).

Le funzioni da utilizzare

Funzione Socket

La prima funzione che si va ad analizzare è:

int socket(int domain, int type, int protocol);

La funzione restituisce un intero che ne identifica la socket. Questo intero viene chiamato socket descriptor. La funzione richiede tre argomenti:

Argomento: domain

Questo parametro serve a identificare quale tipo di famiglia di protocolli si vuole utilizzare (vedi, “Tipologie di socket”).

  • AF_INET: Protocolli ARPA di internet.
  • AF_UNIX: Protocolli interni di sistemi POSIX.
  • AF_NS: Protocolli di Xerox Network System.
  • AF_ISO: Protocolli della International Standard Association.

Il prefisso AF sta per address family. Esistono anche un altro gruppo con prefisso PF. L’uso dei due prefissi è indifferente.

Argomento: type

Questo parametro serve a specificare quale tipo di connessione deve essere stabilita. Può assumere i seguenti valori:

  • SOCK_STREAM: Connessione sequenzia (TCP)
  • SOCK_DGRAM: Connessione trmite datagramma (UDP)
  • SOCK_RAW: Connessione tramite protocollo IP

Esistono anche altri due tipi che non si vedranno.

Argomento: protocol

Serve a specificare quale protocollo usare. Le possibili opzioni sono:

  • IPPROTO_TCP: Protocollo TCP.
  • IPPROTO_UDP: Protocollo UDP.
  • IPPROTO_ICMP: Protocollo ICMP.
  • IPPROTO_RAW: Protocollo IP.
  • 0: Il sistema sceglie il protocollo adatto, in base alla comppia degli argomenti domain e type. Infatti, AF_INET e SOCK_STREAM identificano un protocollo TCP (IPPROTO_TCP), AF_INET e SOCK_DGRAM identificano un protocollo UDP (IPPROTO_UDP).

Funzione Bind

Questa funzione permette di associare una socket ad un processo. La sua sintassi è la seguente:

int bind(int sd, struct sockaddr *my_addr, int addr_lenght);

Come si può notare richiede 3 argomenti:

Argomento: sd

Ossia il socket descriptor ottenuto dalla funzione socket.

Argomento: *my_addr

È una struttura che contiene informazioni come l’indirizzo al quale si vuole connettere, la famiglia a cui appartiene (AF_xxx) e la porta. Tramite casting viene passata una struttura sockaddr_in:

struct sockaddr_in {
short int sin_family; /* famiglia AF_xxx */
unsigned short int sin_port; /* numero di porta */
struct in_addr sin_addr; /* l'indirizzo al quale ci si connette (dipende dal protocollo) */
char sin_zero[8] /* non usato */
};

Questa struttura attraverso il casting viene trasformata in quest’altra struttura:

struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* famiglia AF_xxx */
char sa_data[14]; /* l'indirizzo al quale ci si connette (dipende dal protocollo) */
};

Argomento: addr_lenght

È la dimensione della struttura passata come secondo argomento. La dimensione della struttura si ricava utilizzando la funzione sizeof.

Funzione Listen

 int listen(int sd, int queue);

Una volta creata la socket e associata al processo, quest’ultimo viene messo in ascolto per eventuali connessioni da parte dei client. Accetta solo due argomenti:

Argomento: sd

Il socket descriptor generato dalla funzione socket

Argomento: queue

Il numero di connessioni massime che ammette il processo.

Funzione Accept

Questa funzione consente di accettare le connessioni da parte dei client. Una volta messo in ascolto il processo viene eseguita questa funzione che genera un socket descriptor associato al client connesso.

int accept(int sd, struct sockaddr *client_addr, int client_addr_lenght);

Gli argomenti che vengono richiesti sono:

Argomento: sd

Il solito socket descriptor generato dalla funzione socket

Argomento: client_addr

Una struttura che conterrà alcune informazioni del client. Come quelle viste in precedenza nella funzione bind.

Argomento: client_addr_lenght

La dimensione della struttura che si passa come secondo argomento.

Funzione Connect

Consente ad un client di connettersi ad un server.

int connect(int sdc, sockaddr *server_addr, int server_addr_lenght);

Richiede tre argomenti:

Argomento: sdc

È il socket descriptor generato dalla funzione socket richiamata all’interno del client.

Argomento: server_addr

È la struttura che conterrà l’indirizzo a cui connettersi e la porta del server.

Argomento: server_addr_lenght

È la lunghezza della struttura passato come secondo argomento.

Le funzioni Recv e Send

Vengono usate sia dal server che dal client per inviare informazioni:

int recv(int sd, void *msg, int msg_lenght, int flag);
int send(int sd, void *msg, int msg_lenght, int flag);

Richiedono tre argomenti:

Argomento: sd

Il socket descriptor;

Argomento: msg

Per quanto riguarda la funzione recv sono i dati da ricevere, quindi è la variabile nella quale verrano memorizzati.
Per quanto riguarda la funzione send sono i dati da inviare.

Argomento: flag

Generalmente è posto a 0. Le opzioni possibili non saranno trattate in questo articolo.

Esempio

Di seguito sono mostrati i codici sorgente di un server e di un client. Basta rinominarli rispettivamente in server.c e client.c. Da terminale basta dare i seguenti comandi:

$gcc server.c -o server
$gcc client.c -o client

Ecco i sorgenti:

Il file server.c

/*
 * File: server.c
 * Autore: Iezzi Alessandro
 * Server d'esempio
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
/*#include */ /* Se si vuole visualizzare l'indirizzo ip del client, decommentare questa riga */

#define MAX 8192 /* in bytes, 8KB */

int main()
{
	char buff[MAX]; /* dati di invio e ricezione */
	struct sockaddr_in server_addr; /* indirizzo del server */
	struct sockaddr_in client_addr; /* indirizzo del client */
	int sd_server, sd_client; /* i socket descriptor usati per identificare server e client */

/*
 * Creazione socket descriptor per il server.
 * AF_INET + SOCK_STREAM --> TCP, utilizzo del protocollo TCP (IPPROTO_TCP)
 */
	if((sd_server = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		printf("Errore nella creazione del server\n");

/*
 * Inseriamo nella struttura alcune informazioni
 */
	server_addr.sin_family = AF_INET; /* la famiglia dei protocolli */
	server_addr.sin_port = htons(1745); /* la porta in ascolto */
	server_addr.sin_addr.s_addr = INADDR_ANY; /* dato che è un server bisogna associargli l'indirizzo della macchina su cui sta girando */

/*
 * Assegnazione del processo alla socket tramite la funzione BIND
 */
	if(bind(sd_server, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
		printf("Errore di binding\n");

/*
 * Si mette in ascolto con un massimo di 20 connessioni
 */
	listen (sd_server, 20);

/*
 * Essendo un server monothreading, accetterà una sola connessione per volta
 */
	int address_size = sizeof(client_addr); /* dimensione della struttura client_addr */
/* Con opportune modifiche si potrebbe vedere anche l'ip del client */
	if((sd_client = accept(sd_server, (struct sockaddr *)&client_addr, &address_size)) < 0)
		printf("Errore nella chiamata accept\n");
/* si ricevono i dati dal client */
	recv(sd_client, buff, sizeof(buff), 0);
	printf("Dati ricevuti: %s\n", buff);
/* Decommentare queste due righe per visualizzare l'indirizzo ip del client */
	/*char *ip_address = inet_ntoa(client_addr.sin_addr);
	printf("IP del client: %s\n", ip_address);*/
/* si spedisce un messaggio */
	strcpy(buff, "Tutto OK!");
	send(sd_client, buff, strlen(buff), 0);
/* chiusura del socket descriptor */
	close(sd_client);
	close(sd_server);

	return EXIT_SUCCESS;
}

Il file client.c

/*
 * File: client.c
 * Autore: Iezzi Alessandro
 * Client d'esempio
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX 8192 /* in bytes, 8KB */

int main() {
	int sd; /* Il socket descriptor del client */
	struct sockaddr_in server_addr; /* l'indirizzo del server */
	char buff[MAX]; /* dati di invio e ricezione */

/* Utilizzando la struttura hostent si definisce l'indirizzo del server */
	struct hostent *hp;
	hp = gethostbyname("127.0.0.1");

	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(1745);
/* successivamente viene memorizzato nella struttura server_addr */
	server_addr.sin_addr.s_addr = ((struct in_addr*)(hp->h_addr)) -> s_addr;

/* Viene creato il socket descriptor */
	if((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		printf("Errore nella creazione della socket\n");

/* Viene connesso al server */
	if(connect(sd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
		printf("Errore di connessione al server\n");

/* Si inviano alcuni dati */
	send(sd, "Dati inviati dal client", strlen("Dati inviati dal client"), 0);

/* Si riceve la risposta */
	recv(sd, buff, sizeof(buff), 0);
	printf("Risposta del server: %s\n", buff);
	close(sd);
	return EXIT_SUCCESS;
}

Questo è tutto, alla prossima.

22 commenti
  1. IDN
    IDN dice:

    articolo interessante,non è che avete in programma di dedicarne uno per descrivere il funzionamento degli autotool,mi stanno facendo impazzire ::)
    buon lavoro

    Rispondi
  2. marco
    marco dice:

    Bella guida: chiara e concisa.

    Ho solo una domanda… In un altro tutorial, nel codice del client, quando bisogna assegnare un vaolre al terzo campo della struttura “struct sockaddr_in” (indirizzo del server) viene usata la funzione bcopy() mentre voi fate un “semplice” assegnamento con casting.
    Potreste chiarirmi le idee per quanto riguarda questo passaggio?

    Grazie mille e ancora complimenti.

    Rispondi
  3. Antonio Barba
    Antonio Barba dice:

    @marco: Grazie per i complimenti, che rigiro all’autore dell’articolo (Alessandro).

    Lo standard C moderno prevede la copia di una struct su un’altra tramite semplice assegnazione (viene inserito un memcopy automaticamente dal compilatore). Probabilmente il codice che hai visto in giro è fatto per compilare correttamente anche su versioni molto vecchie del C.

    Quindi don’t worry!

    Rispondi
  4. Pietro
    Pietro dice:

    Ciao, come cambia il codice in caso di invio di un file (per esempio un pdf) di dimensione molto più grande del buffer?

    Rispondi
  5. Alessandro Iezzi
    Alessandro Iezzi dice:

    @Pietro:
    Tenendo conto che non ho mai provato ad inviare file tramite delle socket, posso solo dire che se vuoi inviare dati più grandi devi obbligatoriamente modificare il buffer.
    Una soluzione sarebbe quella di andare a creare un buffer dinamico che venga dimensionato in base al file da inviare. Per fare questo ci sono due modi:
    1° Usando il C89 con la funzione “malloc”;
    2° Usando il C99 con le VLA.
    Penso anche che tramite l’uso di sizeof ti potresti ricavare la dimensione in byte del file e di conseguenza adattare il buffer. La tipologia del socket dovrebbe essere invariata, ovvero usare SOCK_STREAM.
    Magari se ho tempo qualche piccolo esperimento lo faccio questi giorni e se ne esce qualcosa di buono non esito a scrivere un articolo.

    @marco:
    Scusa se rispondo solo ora, ma l’università ha preso il sopravvento sul mio tempo libero.
    Mi dispiace doverti rispondere per commento, se non ti è chiaro qualcosa non esitare ad inviarmi una email.

    La struttura hostent ha le seguenti variabili:

    struct hostent
    {
    char *h_name; // Official name of host.
    char **h_aliases; // Alias list.
    int h_addrtype; // Host address type.
    int h_length; // Length of address.
    char **h_addr_list; // List of addresses from name server.
    #define h_addr h_addr_list[0] // Address, for backward compatibility.
    };

    Come puoi ben notare, h_addr è il primo indirizzo del vettore di “stringhe” (intese come array di caratteri), ogni array è formato da 4 elementi, quindi 4 byte.
    L’indirizzo perciò viene memorizzato in questo modo:

    127.0.0.1 —> h_addr[0] = 127, h_addr[1] = 0, h_addr[2] = 0, h_addr[3] = 1.

    Il casting, non fa altro che prendere questo piccolo vettore e trasformarlo in un intero formato da 4 byte. Esso è contenuto nella struttura in_addr:

    struct in_addr {
    uint32_t s_addr; // that’s a 32-bit int (4 bytes)
    };

    Viene quasi spontaneo ora eseguire:

    server_addr.sin_addr.s_addr = ((struct in_addr*)(hp->h_addr)) -> s_addr;

    Dovrebbe funzionare anche così:

    server_addr.sin_addr = (struct in_addr*)(hp->h_addr);

    Spero sia esaustiva come risposta e grazie per il commento.

    Rispondi
  6. Antonio Barba
    Antonio Barba dice:

    @Alessandro e @Pietro:
    per file piccoli (al di sotto del MB) si può optare per ingrandire il buffer, ad ogni modo la sizeof() non restituisce la dimensione del file (operazione fattibile soltanto a runtime), perchè è un operatore del linguaggio che viene risolto a compile time (non è una funzione).
    Per sapere la dimensione di un file in bytes bisogna usare la funzione stat(), che restituisce tra le altre cose anche questa informazione.

    Per file di dimensione arbitrariamente grande, la soluzione consiste nel leggere il file a “chunk” di grandezza fissata (ad esempio 1K alla volta), attraverso la funzione fread(), e inviare via socket questi chunk.
    Sia la trasmissione che la ricezione dovranno avvenire dentro un ciclo while, usando una variabile che conteggia i byte trasmessi/ricevuti, e ad ogni ciclo si dovrà effettuare una nuova fread/fwrite per leggere/scrivere quel chunk sul file.
    E’ un’operazione che comunque comporta tutta una serie di problemi dovuti alla scarsa affidabilità del mezzo di trasmissione, pertanto è meglio affidarsi a protocolli già sperimentati come FTP, che consentono anche il reinvio di chunk perduti, e il riordino degli stessi in caso di arrivo “in ordine sparso”, evento che si verifica frequentemente su grandi reti come internet.

    Rispondi
  7. marco
    marco dice:

    @Alessandro lezzi:
    grazie per la delucidazione in merito al casting 🙂
    Per quanto riguarda la domanda di Pietro, invece di utilizzare un unico buffer di dimensione pari al file da spedire non si potrebbe usarne uno di dimensione definita a priori e ciclare l’invio dei dati “un pezzo per volta”? Così facendo si evita di sovraccaricare la memoria e si permette anche a computer con poca RAM di inviare file molto grandi.

    Rispondi
  8. Antonio Barba
    Antonio Barba dice:

    @marco:
    Bene! 🙂
    Il problema comunque non è soltanto quello di computer con poca Ram, anche perchè grazie all’address space virtuale puoi avere sempre 2GB di Ram “virtuale” anche con un PC a 32bit con 128MB di Ram fisica.
    Il problema è che i moderni filesystem supportano file con dimensioni a 64bit, è quindi possibile (ed ormai anche comune) avere un file maggiore di 2GB (basti pensare all’immagine ISO di un qualsiasi DVD), e quindi sarebbe fisicamente impossibile caricarlo in Ram su un sistema a 32 bit.
    Ricordo che, sebbene 32 bit rappresentino lo spazio di 4 GB, metà di questo address space è riservato al kernel nella zona alta della memory map, quindi i programmi utente ne vedono solo 2 GB. Questo vale sia su Windows che Linux per x86. Il problema non si pone sulle versioni AMD64 (o meglio, il problema viene semplicemente rimandato più in là nel tempo).

    Rispondi
  9. Alessandro Iezzi
    Alessandro Iezzi dice:

    @Antonio e @marco:
    Si in effetti avrebbe più senso ciclare l’invio.

    @marco:
    Sono felice che il commento ti sia stato utile, avevo paura di non essermi spiegato bene. Come accade spesso, molte cose purtroppo vengono tralasciate, ad esempio come quella del casting, non per dimenticanza ma proprio perché sarebbero un surplus.

    Rispondi
  10. Saverio
    Saverio dice:

    C’e’ un modo per rifiutare tout court le connessioni se ce n’e’ già una attiva?
    Pur mettendo 1 come valore di queue della listen, comunque le connessioni sono accettate (temo che 5 sia il valore minimo nel mio ambiente). Il problema e’ che cio’ che il client manda, viene cachato, e mi arriva tutto assieme quando il client corrente si disconnette!!

    Io ho bisogno di limitare ad una sola connessione (il server accetta comandi per un Servo), magari dando “Busy, try later” alle successive e chiudendo il soket.

    Rispondi

Trackbacks & Pingbacks

  1. […] Come programmare le socket in ambienti GNU/Linux (Parte 2) Ti interesser&agrave anche…Come programmare le socket in ambienti GNU/Linux (Parte 1) Nel primo articolo abbiamo analizzato il funzionamento delle socket e di un server con rispettivo client. In questo vedremo un modo di programmare un tipico server concorrente utilizzando la funzione fork(). […]

Lascia un Commento

Vuoi partecipare alla discussione?
Fornisci il tuo contributo!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *