Come usare i socket in C su Linux

Tramite: O2O
Difficoltà: media
18

Introduzione

Un argomento fondamentale, quando si parla di informatica, riguarda la comunicazione intesa nel suo significato più ampio.
La comunicazione può avere luogo all'interno del computer, ad esempio con più processi che parlano tra di loro, o all'esterno
con più dispositivi che si scambiano informazioni.

I computer di tutti i giorni possiedono un'importante astrazione software chiamata "socket". Questo strumento permette comunicazioni
via rete e tra processi (IPC) utilizzando API standard e ben definite.
Utilizzando il linguaggio C e un sistema operativo Linux, andremo a costruire un semplice sistema client-server che servirà per comprendere meglio questo importante meccanismo, e capire come usare i socket su questi sistemi. I concetti imparati, inoltre, saranno analoghi anche su altri sistemi.

La comunicazione tra i nostri due programmi avverrà utilizzando il protocollo TCP. Sono fortemente consigliate una media conoscenza del linguaggio C
e una conoscenza base della comunicazione di rete.

28

Occorrente

  • Computer con sistema operativo Linux
  • Compilatore GCC o equivalente
  • Librerie di sviluppo C
38

I livelli ISO/OSI e il protocollo TCP

Ogni scambio di informazioni segue uno specifico protocollo, che determina le caratteristiche dei pacchetti e la logica di comunicazione.
La struttura logica della rete segue un modello a strati standard, chiamato modello ISO/OSI. Ogni strato rappresenta un passo necessario
eseguire per portare a termine un certo tipo di comunicazione.

Il protocollo di un certo livello "incapsula", quindi immagazzina al suo interno, il pacchetto dati del livello superiore. In questo modo
è possibile far lavorare insieme protocolli di livelli differenti in modo del tutto compatibile.
Si prenda l'esempio di un server web: il pacchetto inviato parte dai livelli ISO/OSI più alti del server, scende fino a livello 1 (livello fisico)
venendo incapsulato lungo tutti i livelli e spedito.
Quando viene ricevuto dal client, si fa l'operazione inversa e viene presentata l'informazione all'utente finale.

Il protocollo usato nei nostri esempi è chiamato TCP. Opera a livello 3 del modello ISO/OSI e permette di avere una trasmissione affidabile: gli utenti
sanno sempre se hanno ricevuto o meno il pacchetto. Questo protocollo lavora insieme a un altro protocollo di livello 2, il protocollo IP, per cui è facile
vedere sigle come TCP/IP che racchiudono entrambi.
La trasmissione viene eseguita tra due computer utilizzando un indirizzo IP sorgente, un indirizzo IP destinazione, una porta di comunicazione sorgente e una porta destinazione.

Client e server devono seguire due procedimenti diversi per potersi collegare. Uno dei procedimenti più semplici, usato negli esempi, è il seguente.
Il server:

1) Prepara le strutture dati e definisce il tipo di socket che si vuole usare. Questo determinerà il tipo di comunicazione che si vuole intraprendere.
2) Chiede al sistema operativo di riservare una porta alle sue connessioni e si mette in ascolto.
3) Quando riceve un pacchetto dati da un determinato IP, può iniziare la comunicazione.

Il client:

1) Prepara le strutture dati, definisce il tipo di socket e sceglie porta e indirizzo IP di destinazione.
2) Tenta la connessione al server.
3) Se la connessione ha successo, i dati possono essere inviati e ricevuti.

Ogni connessione sarà rappresentata da un socket, che avrà un riferimento univoco chiamato "descrittore".
Nelle prossime pagine vedremo nel dettaglio codice e funzionamento di quanto scritto: andremo infatti ad implementare un semplice sistema client-server, nel quale il client "saluta" il server che risponde.

48

Primo esempio: un semplice client

Vediamo subito il codice del client.

// Header necessari per funzioni e strutture
#include
#include
#include
#include
#include

int main (int argc, char *argv[]) {

int socket_descrittore; // Variabile che contiene il descrittore per il socket che andremo a creare
struct sockaddr_in server_destinazione; // Struttura che contiene i parametri di connessione al server
char *messaggio = "Client invia: ciao mondo!";
char risposta[100];

socket_descrittore = socket (AF_INET, SOCK_STREAM, 0);
if (socket_descrittore == -1) {
printf ("Errore di creazione socket.\n");
return 1;
}

server_destinazione. Sin_addr. S_addr = inet_addr ("127.0.0.1");
server_destinazione. Sin_family = AF_INET;
server_destinazione. Sin_port = htons (5555);

if (connect (socket_descrittore, (struct sockaddr *)&server_destinazione, sizeof (server_destinazione)) printf ("Errore di connessione.\n");
return 1;
}

printf ("Connesso al server. Invio messaggio...\n");

if (send(socket_descrittore, messaggio, strlen (messaggio), 0) printf ("Errore invio messaggio.\n");
return 1;
}

if (recv(socket_descrittore, risposta, 100, 0) printf ("Errore ricezione.\n");
return 1;
}

printf ("%s\n", risposta);
close (socket_descrittore);
return 0;
}

Per quanto possa sembrare complicato a prima vista, tutto quello che fa è: connettersi, inviare il messaggio, attendere risposta. Iniziamo ad analizzarlo.

Superati le direttive "#include" e le definizioni di rito, si passa alle variabili. Quelle realmente interessanti sono due: un intero chiamato "socket_descrittore" che
identificherà univocamente il nostro socket di comunicazione, e una struttura dati di tipo sockaddr_in chiamata "server_destinazione" che conterrà i dettagli di connessione
al nostro server.

Subito dopo, vi è la fase di preparazione. Viene creato il socket con la funzione "socket ()", specificando che si tratta di una comunicazione sul protocollo IP versione 4 (AF_INET)
di tipo sequenziale, bidirezionale, affidabile, con connessione, dati in un flusso di byte (SOCK_STREAM).
I parametri di connessione vengono poi immessi nella struttura dati. L'indirizzo IP viene convertito in binario con ordinamento "network byte order" (big endian) tramite la funzione
"inet_addr ()". Si specificano anche il tipo di protocollo IP, che dev'essere AF_INET perché la struttura sockaddr_in tratta solo IPv4, e la porta 5555 convertita in dati binari con "htons ()".

La funzione "connect ()" tenta di connettersi, utilizzando i dati del server inseriti prima nella struttura dati.
Se la connessione ha successo, si tenta l'invio del messaggio con "send ()". Il testo è stato memorizzato in un buffer chiamato "messaggio", di cui si calcola la lunghezza con "strlen ()".
Terminato l'invio del messaggio, l'esecuzione prosegue con "recv ()" per ricevere la risposta.
Le funzioni connect (), recv (), send (), in caso di errori ritornano tutte un valore inferiore a 0 e questo ci consente di gestire eventuali errori.
Attenzione: connect (), send () e recv () sono chiamate cosiddette "bloccanti". Finché non terminano l'esecuzione, non sarà possibile proseguire nel programma. È possibile ovviare a questo
utilizzando i socket non bloccanti, ma esulano dallo scopo di questa guida.
Al termine del programma, è importante chiudere il descrittore del socket utilizzando "close ()". Si eviterà così uno spreco inutile di risorse.

Nella pagina seguente, vedremo come implementare un piccolo server che possa gestire questa comunicazione.

Continua la lettura
58

Primo esempio: un semplice server

Il codice sottostante permette di gestire un solo client per volta, ma è utile al fine di capire la logica di funzionamento. Diverse parti saranno simili a quanto già
visto per il client, mentre verranno aggiunge le chiamate di funzione e le definizioni necessarie.

// Header necessari per funzioni e strutture
#include
#include
#include
#include
#include

int main (int argc, char *argv[]) {

int socket_descrittore; // Variabile che contiene il descrittore per il socket che andremo a creare
int socket_client, len; // Socket del client e dimensione della struttura del socket
struct sockaddr_in mio_server; // Struttura che contiene i dettagli del server
struct sockaddr_in client;
char *risposta = "Server risponde: ciao mondo!";
char buffer_ricezione[100];
len = sizeof (struct sockaddr_in);

socket_descrittore = socket (AF_INET, SOCK_STREAM, 0);
if (socket_descrittore == -1) {
printf ("Errore di creazione socket.\n");
return 1;
}

// Salvo configurazioni opportune, le porte più basse di 1024 possono essere impegnate solo dall'utente root
mio_server. Sin_addr. S_addr = INADDR_ANY; // Si rimane in ascolto su ogni indirizzo
mio_server. Sin_family = AF_INET;
mio_server. Sin_port = htons (5555);

if (bind (socket_descrittore, (struct sockaddr *)&mio_server, sizeof (mio_server)) printf ("Errore durante il bind.\n");
return 1;
}

if (listen (socket_descrittore, 1) printf ("Errore durante la listen.\n");
return 1;
}

printf ("In ascolto.\n");

socket_client = accept (socket_descrittore, (struct sockaddr *)&client, (socklen_t*)&len);

if (socket_client printf ("Errore durante accept.\n");
return 1;
}

printf ("Connessione in arrivo.\n");

if (recv (socket_client, buffer_ricezione, 100, 0) printf ("Errore ricezione.\n");
return 1;
}

if (send (socket_client, risposta, strlen (risposta), 0) printf ("Errore invio messaggio.\n");
return 1;
}

printf ("%s\n", buffer_ricezione);
close (socket_descrittore);
return 0;
}

Il codice rimane quasi invariato, salvo alcune aggiunte. Le prime differenze si hanno nell'inizializzazione delle variabili: è necessario allocare una struttura per ogni client connesso, così da poterne memorizzare i dettagli di connessione.
Inoltre, viene preparato un messaggio di risposta, e una variabile "buffer_ricezione" che conterrà i dati inviati dal client.
Dopo aver creato il socket, si inizializza la struttura dati del server, ponendolo in ascolto su tutti gli indirizzi e impostando la porta a 5555 per una connessione IPv4.

Nelle righe successive, abbiamo le prime differenze vere e proprie. Viene chiamata la funzione "bind ()", che richiede al sistema operativo di riservare la porta indicata prima al programma. Nessun altro programma potrà usarla, come noi non potremmo usare una porta se fosse già in uso.
Le due chiamate successive, "listen ()" e "accept ()", servono rispettivamente a rilevare eventuali connessioni sul socket indicato e accettarle. La funzione listen () riceve eventuali richieste di connessione sul socket indicato da "socket_descrittore", mentre accept () si occupa di accettarle. Negli argomenti di listen () è anche possibile impostare una coda chiamata "backlog", qui settata a 1, per mettere un limite al numero di connessioni in attesa.

Arrivato alla chiamata accept (), il programma si blocca in attesa di connessioni. La chiamata è infatti bloccante, e riprenderà l'esecuzione solo quando vi sarà una connessione da accettare.
Una volta arrivata la richiesta a listen (), accept () accetta la connessione e crea un nuovo socket di comunicazione col client appena connesso. Il descrittore del nuovo socket è memorizzato in "socket_client", la struttura aggiuntiva che era stata creata all'inizio per il client.

Da qui in poi, il procedimento è lo stesso del client: il server si mette in ascolto con recv () sul socket del client, in attesa dei dati. Infatti, nel nostro esempio il client prima invia con send () e poi riceve con recv () la risposta, mentre il server opera al contrario, prima ricevendo con recv () sul socket del client e poi rispondendo con send ().
Questa differenza nella sequenza di ricezione e invio è importante: se client e server lanciassero la recv () nello stesso momento, si verrebbe a creare una situazione di stallo.

68

Conclusioni

Abbiamo visto un semplice esempio nel quale un client e un server si scambiavano un saluto reciproco. Il codice presentato è puramente didattico, e serve a fornire delle basi di partenza sulle quali lavorare per approfondire il vasto argomento della programmazione di rete.
Il software può essere compilato normalmente utilizzando gcc, ad esempio:

"gcc client. C -o client && gcc server. C -o server"

Inoltre, è progettato per funzionare su un sistema operativo Linux. Questa differenza è importante, perché ogni sistema operativo può avere le proprie specificità.
Nel caso si desideri portare il software su un'altra macchina, si rimanda alla documentazione ufficiale relativa a quel sistema.

78

Guarda il video

88

Consigli

Non dimenticare mai:
  • Conoscenza intermedia del linguaggio C
  • Conoscenza base sulle reti di calcolatori
  • Conoscenza base su Linux
Alcuni link che potrebbero esserti utili:

Potrebbe interessarti anche

Segnala contenuti non appropriati

Tipo di contenuto
Devi scegliere almeno una delle opzioni
Descrivi il problema
Devi inserire una descrizione del problema
Si è verificato un errore nel sistema. Riprova più tardi.
Segnala il video che ritieni inappropriato
Devi selezionare il video che desideri segnalare
Verifica la tua identità
Devi verificare la tua identità
chiudi
Grazie per averci aiutato a migliorare la qualità dei nostri contenuti

Guide simili

Programmazione

Come programmare in java su Linux

Ai giorni nostri la programmazione è sempre più avanzata e ci sono numerosi modi e piattaforme per effettuare la programmazione. Tra i software più utilizzati troviamo Java, inoltre si può programmare su varie piattaforme tra cui anche Linux. Nei...
Programmazione

Come eseguire il tar di una directory

Il comando tar su Linux viene spesso utilizzato per creare file di archivio. Tar. Gz o .tgz, chiamati anche "tarball". Questo comando ha un gran numero di opzioni, ma è sufficiente ricordare alcune lettere per creare rapidamente archivi con tar. Il comando...
Programmazione

Come installare e configurare Eclipse su Ubuntu

Per Linux esistono centinaia di compilatori adatti alla programmazione Java. Eclipse è sicuramente uno dei migliori e soprattutto uno dei più famosi editor per linguaggio Java e non solo, perché infatti gestisce perfettamente anche C++, C, Javascript...
Programmazione

Come installare e configurare Komodo Edit

Per riuscire a creare dei siti internet sono necessarie delle nozioni che consentano di capire i passi da eseguire. Per realizzare il sito poi è essenziale utilizzare programmi che consentono di avere tutto ciò che serve. Komodo Edit è un'eccellente...
Programmazione

Java: 10 cose da sapere

Java è un linguaggio di programmazione orientato agli oggetti. Questo significa che attraverso questo linguaggio è possibile creare giochi ed applicazioni con cui è possibile interagire a schermo. L'origine di questo linguaggio risale al lontano 1995,...
Programmazione

Come creare un file eseguibile Java

Quando si parla di Java si intende un linguaggio di programmazione che è stato pensato e realizzato allo scopo di non dipendere dalla piattaforma di esecuzione. Questo, dunque, può essere sfruttato da sistemi come Windows, Linux, Unix, ecc. Per questi...
Programmazione

Programmazione: come impostare il giusto compilatore sul proprio computer

In informativa un compilatore è ciò che converte (e quindi in un certo senso traduce) delle informazioni scritte in un determinato modo (nel linguaggio di programmazione) in un altro linguaggio. La compilazione, che è possibile anche al contrario e...
Programmazione

Come digitare il simbolo Copyright

Il Copyright viene applicato su immagini, su testi e su files che hanno dei diritti d'autore particolari.Per questo motivo, potremo utilizzarlo, sui lavori da noi realizzati, per poter così bloccare l'uso di questi ultimi da parte di terzi senza regolamentazione....
I presenti contributi sono stati redatti dagli autori ivi menzionati a solo scopo informativo tramite l’utilizzo della piattaforma www.o2o.it e possono essere modificati dagli stessi in qualsiasi momento. Il sito web, www.o2o.it e Arnoldo Mondadori Editore S.p.A. (già Banzai Media S.r.l. fusa per incorporazione in Arnoldo Mondadori Editore S.p.A.), non garantiscono la veridicità, correttezza e completezza di tali contributi e, pertanto, non si assumono alcuna responsabilità in merito all’utilizzo delle informazioni ivi riportate. Per maggiori informazioni leggi il “Disclaimer »”.