La Programmazione Client/Server in C | |||
Ven 21 Dic 2007 |
|
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
/* Inizializzazione della libreria Socket */
WORD wVersionRequested = MAKEWORD(2,2);
WSADATA wsaData;
wsastartup = WSAStartup(wVersionRequested, &wsaData);
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
/* Creazione della Socket che si porrà in ascolto di richieste del Client*/
listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket < 0)
printf("Server: errore nella creazione della socket.\n");
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
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.
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
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.
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR* lpVendorInfo;
} WSADATA,
*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
Con af si indica la famiglia di indirizzi da utilizzare. I possibili valori assumibili da questo parametro sono i seguenti
Con type si indica il tipo di socket da creare. I possibili valori assumibili da questo parametro sono i seguenti
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
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
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
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.
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
struct sockaddr_in{
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* numero di porta */
struct in_addr sin_addr;
char sin_zero[8] /* non usato */
}
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
/* Effettua la bind sull’indirizzo e porta ora specificati */
port = 4000;
Server_addr.sin_family = AF_INET;
Server_addr.sin_addr.s_addr = "127.0.0.1";
Server_addr.sin_port = htons(port);
if (bind(listenSocket,(LPSOCKADDR)&Server_addr,sizeof(struct sockaddr)) < 0)
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 listenFigura 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
/* La socket si pone in "ascolto" tramite la listen() */
ls_result = listen(listenSocket, SOMAXCONN);
if (ls_result < 0) printf("Server: errore durante la listen.\n");
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
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.
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.