La Programmazione Client/Server in C 

La Programmazione Client/Server in C
21 Dicembre 2007 Reti
   





Socket

Una socket è un canale di comunicazione tra due processi che possono risiedere anche su host diversi. I due processi comunicheranno fra loro leggendo e/o scrivendo i dati nella socket.
Vi sono tre tipi fondamentali di socket
  • Datagram Socket. Supporta la comunicazione bidirezionale e senza connessione (un host invia dei dati ad un altro host senza stabilire una connessione). Non è affidabile, non garantisce la ricezione dei pacchetti, ne' che i pacchetti saranno ricevuti nello stesso ordine con cui sono stati inviati. Nelle comunicazioni internet è utilizzata dal protocollo UDP.
  • Stream Socket. Supporta la comunicazione bidirezionale e con connessione (un host per inviare dei dati ad un altro host stabilisce una connessione con l'altro host). È affidabile, garantisce la ricezione dei pacchetti e che questi saranno ricevuti nello stesso ordine con cui sono stati inviati. Nelle comunicazioni internet è utilizzata dal protocollo TCP.
  • Raw Socket. Socket di basso livello tramite le quali è possibile accedere al livello network.
 

Interazione Client/Server tramite il protocollo TCP

Affinché vi possa essere una comunicazione tra client e server mediante il protocollo TCP è necessario innanzitutto stabilire una connessione tra il client ed il server. Una volta stabilita la connessione vi potrà essere lo scambio di dati, terminato il quale si chiude la connessione.

Creazione della Socket del Server

Il primo passo consiste nella creazione del canale di comunicazione tra il server ed un’eventuale client, tramite questo canale il client potrà comunicare con il server. Questo canale di comunicazione sarà costituito da una socket.

Figura 1.

In ambiente windows prima della creazione della socket del server è necessario inizializzare la libreria winsock tramite la funzione WSAStartup

  1. /* Inizializzazione della libreria Socket */
  2. WORD wVersionRequested = MAKEWORD(2,2);
  3. WSADATA wsaData;
  4. wsastartup = WSAStartup(wVersionRequested, &wsaData);
  5. if (wsastartup != NO_ERROR) printf("Errore WSAStartup()\n");

Una volta inizializzata, solo in ambiente windows, la libreria winsock, si creerà la socket. La socket sarà creata con la funzione socket come indicato di seguito

  1. /* Creazione della Socket che si porrà in ascolto di richieste del Client*/
  2. listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  3. if (listenSocket < 0)
  4. printf("Server: errore nella creazione della socket.\n");
  5. else printf("La Listening Socket è partita\n");

Si è costruita una socket che come indirizzamento utilizza IPv4, utilizza una connessione TCP (SOCK_STREAM), e la comunicazione avviene secondo il protocollo TCP (IPPROTO_TCP).
Questa parte di codice è comune al server e al client.

La funzione WSAStartup inizializza l'uso della winsock da parte del processo

  1. int WSAStartup(
  2. WORD wVersionRequested,
  3. LPWSADATA lpWSAData
  4. );

consente all'applicazione di specificare la versione delle windows sockets richiesta.
La funzione ha due parametri, wVersionRequested che indica la più recente versione delle specifiche windows sockets che l'applicazione può usare. Quando viene chiamata la funzione WSAStartup la winsock esamina la versione delle windows sockets richiesta dall'applicazione, passata con il parametro wVersionRequested. Se la versione richiesta dall'applicazione è maggiore o uguale della minore delle versioni supportata dalla winsock la chiamata ha successo e la winsock restituisce le informazioni dettagliate nella struttura WSADATA, struttura puntata dal parametro lpWSAData.

  1. typedef struct WSAData {
  2. WORD wVersion;
  3. WORD wHighVersion;
  4. char szDescription[WSADESCRIPTION_LEN+1];
  5. char szSystemStatus[WSASYS_STATUS_LEN+1];
  6. unsigned short iMaxSockets;
  7. unsigned short iMaxUdpDg;
  8. char FAR* lpVendorInfo;
  9. } WSADATA,
  10. *LPWSADATA;

Elemento Descrizione
wVersion Indica la versione delle specifiche windows sockets che la Ws2_32.dll si aspetta che il chiamante usi
wHighVersion Indica la più recente versione delle specifiche windows sockets che questa .dll può supportare. Di solito è la stessa indicata dal parametro wVersion.
szDescription Stringa (Null-terminated) in cui la Ws2_32.dll inserirà la descrizione dell'implementazione delle Windows sockets.
szSystemStatus Stringa (Null-terminated) in cui la Ws2_32.dll inserirà informazioni rilevanti sullo stato o sulla configurazione.
iMaxSockets Può essere ignorato per la versione 2 delle Windows Sockets e per le versioni successive
iMaxUdpDg Ignorato per la versione 2 delle Windows Sockets e successive
lpVendorInfo Ignorato per la versione 2 delle Windows Sockets e successive

La funzione socket è definita sia in ambiente windows che in ambiente linux ed il suo prototipo è il seguente

  1. SOCKET socket(int af, int type, int protocol)

Con af si indica la famiglia di indirizzi da utilizzare. I possibili valori assumibili da questo parametro sono i seguenti
Famiglia Descrizione
AF_INET IPv4
AF_INET6 IPv6
AF_LOCAL Unix Domain Protocol
AF_ROUTE Routing Sockets
AF_KEY Key Socket

Con type si indica il tipo di socket da creare. I possibili valori assumibili da questo parametro sono i seguenti
Type Descrizione
SOCKET_STREAM Stream Socket (TCP). Fornisce una connessione sicura, ordinata e bidirezionale. Consente la comunicazione tramite un flusso di byte di lunghezza arbitraria. Utilizzano un protocollo con connessione.
SOCK_DGRAM Datagram Socket (UDP). Non fornisce una connessione, la comunicazione avviene attraverso datagram, buffer di una dimensione massima prefissata (tipicamente piccola). Utilizzano un protocollo senza connessione.
SOCK_RAW Unix Domain protocols. Supporta le socket raw. Le socket raw consentono ad una applicazione di inviare e ricevere pacchetti con header customized

con protocol si indica il protocollo da utilizzare, i possibili valori sono IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP, IPPROTO_UDP, SOL_SOCKET.

La funzione socket restituisce un socket descriptor (>0) in caso di successo o il valore -1 in caso di errore. Ogni socket descriptor identifica univocamente un canale di comunicazione che il server mette a disposizione.
Una lista di possibili codici di errore è la seguente

Errore Descrizione
EHOSTDOWN The networking subsystem has not been started
EAFNOSUPPORT The specified address family is not supported on this version of the system
ESOCKTNOSUPPORT The specified socket type is not supported in this address family
EPROTONOSUPPORT The specified protocol is not supported
EMFILE The per-process descriptor table is full
ENOBUFS No buffer space is available. The socket cannot be created
ENFILE The system's table of open files is temporarily full, and no more socket calls can be accepted
EPROTOTYPE The type of socket and protocol do not match
ETIMEDOUT The connection timed out

Assegnazione di un Indirizzo

Una volta creato il canale di comunicazione il client deve essere messo in condizione di accedervi. Per potervi accedere deve esistere un indirizzo per accedervi e il client deve conoscere l’indirizzo di questo canale.
A tale scopo si associa al server un indirizzo e alla socket una porta che la identifica univocamente.
Per associare indirizzo e porta alla socket si usa la funzione bind che assegna la gestione di un indirizzo e di una porta locali ad un socket

Figura 2.

La funzione bind è definita sia in ambiente windows che in ambiente linux ed il suo prototipo è il seguente

  1. int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)

dove con sockfd si indica l’identificativo della socket (il socket descriptor), con myaddr si indica l’indirizzo (indirizzo:porta) gestito dalla socket, con addrlen si indica la lunghezza dell’indirizzo (in byte). La funzione restituisce 0 in caso di successo o -1 in caso di errore.
La struttura sockaddr varia a seconda del protocollo selezionato. Nel caso si usi IPv4 la struttura di tipo sockaddr ha la forma seguente

  1. struct sockaddr_in{
  2. short int sin_family; /* AF_INET */
  3. unsigned short int sin_port; /* numero di porta */
  4. struct in_addr sin_addr;
  5. char sin_zero[8] /* non usato */
  6. }

possono essere specificati un indirizzo IP (sin_addr), o una porta (TCP o UDP, sin_port), o entrambi, o nessuno dei due. Quando un valore non viene specificato esso viene scelto automaticamente dal sistema operativo.

Normalmente la funzione bind() viene usata solo quando si intende realizzare un server in quanto è necessario specificare su quale indirizzo IP e su quale porta il server rimarrà in attesa di connessioni da parte dei client.
Il numero di porta scelto può variare tra 0 e 65535 e non deve essere già in uso da un'altra socket.
L'assegnazione dell'indirizzo alla socket prevede l'esecuzione dei seguenti passi

  1. /* Effettua la bind sull’indirizzo e porta ora specificati */
  2. port = 4000;
  3.  
  4. Server_addr.sin_family = AF_INET;
  5. Server_addr.sin_addr.s_addr = "127.0.0.1";
  6. Server_addr.sin_port = htons(port);
  7.  
  8. if (bind(listenSocket,(LPSOCKADDR)&Server_addr,sizeof(struct sockaddr)) < 0)
  9. printf("Server: errore durante la bind.\n");

Si specifica l'indirizzo (per specificare un indirizzo generico, con IPv4 si usa il valore INADDR_ANY) e la porta della socket, quindi si effettua la bind.

Server in Ascolto

Una volta che il canale di comunicazione è stato creato e sono stati dati i riferimenti ad esso il server si deve mettere in ascolto in attesa di una richiesta di connessione da parte di un client. Per mettersi in ascolto il server usa la funzione listen

Figura 3.

Nell'esempio proposto il server si pone in ascolto sulla socket identificata da listenSocket e accetta fino ad un massimo di SOMAXCONN tentativi di connessione

  1. /* La socket si pone in "ascolto" tramite la listen() */
  2. ls_result = listen(listenSocket, SOMAXCONN);
  3. if (ls_result < 0) printf("Server: errore durante la listen.\n");
  4. else printf("La Socket è in Ascolto\n");

La funzione listen è definita sia in ambiente windows che in ambiente linux ed il suo prototipo è il seguente

  1. int listen(int sockfd, int backlog)

dove con sockfd si indica la socket che si pone in ascolto, e con backlog si indica il numero massimo di connessioni in attesa di completamento contemporaneamente gestibili dalla socket. Queste connessioni sono quelle connessioni pendenti, connessioni richieste dai client ma non ancora accettate dal server tramite la funzione accept.
Questa funzione deve essere usata solo all’interno di un server. La funzione restituisce 0 in caso di successo o -1 in caso di errore.
Pagina 1 di 3
Prec 1 2 3 Succ