Le chiamate di funzione
La libreria fornita da Microsoft fornisce allo sviluppatore un ricco insieme di API: il nucleo centrale è basato sulla libreria di Berkeley, mentre un ulteriore insieme estende le funzionalità esportate con alcune caratteristiche specifiche per Windows.
Quando il programmatore invoca le API di Winsock, il kernel svolge automaticamente e magicamente tutto il lavoro necessario per effettuare la comunicazione.
Gli indirizzi delle socket
La struttura che memorizza le informazioni relative all’indirizzo per molti tipi di socket è la seguente:
001struct sockaddr {
002 unsigned short sa_family; // address family, AF_xxx
003 char sa_data[14]; // 14 bytes of protocol address
004};
I valori assegnati a questa struttura variano a seconda del protocollo selezionato ed ad eccezione del campo sa_family, gli altri sono espressi secondo il Network Byte Order.
Per quanto concerne il campo sa_family il valore per i protocolli TCP e UDP è AF_INET. Mentre il campo sa_data contiene l’indirizzo di destinazione ed il numero di porta.
Per gestire meglio i valori di questi campi sono messe a disposizione per i protocolli supportati altre strutture della stessa dimensione di sock_addr che possono essere utilizzate dal programmatore in modo più agevole. Il protocollo TCP/IP, ad esempio, utilizza la struttura seguente:
001struct sockaddr_in {
002 short sin_family; // address family
003 u_short sin_port; // port number
004 struct in_addr sin_addr; // internet address
005 char sin_zero[8]; // same size as sockaddr
006};
E’ importante notare che il campo sin_zero è presente per allineare la struttura alla stessa lunghezza della struttura sockaddr e deve essere riempito con tutti zeri utilizzando la funzione memset(), mente il campo sin_family corrisponde al campo sa_family della struttura sockaddr; in questo modo sarà possibile passare la struttura sockaddr_in a tutte le funzioni che si aspettano un parametro di tipo sockaddr, dopo averne effettuato il cast.
Come si può notare dalla struttura precedente il campo che rappresenta l’indirizzo internet è a sua volta una struttura definita come segue:
001struct in_addr{
002 unsigned long s_addr;
003};
Il motivo per cui è stata utilizzata una struttura per questo campo è dovuto soprattutto a ragioni storiche ed il programmatore può fare riferimento all’indirizzo internet nel modo seguente:
001struct sockaddr_in ina;
002ina.sin_addr.s_addr = …;
Segue un’esempio di inzializzazione della struttura sockaddr_in:
001#define PORT_NUM 1100
002Struct sockaddr_in myAddr;
003myAddr.sin_family = AF_INET; // host byte order
004myAddr.sin_port = htons(PORT_NUM);
005myAddr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
006memset(&(myAddr.sin_zero), '\0', 8);
Creazione di una socket
Per allocare le risorse necessarie alla creazione di una socket ed ottenerne il descrittore, la libreria Winsock mette a disposizione dello sviluppatore la funzione socket().
001SOCKET socket(
002 int af,
003 int type,
004 int protocol,
005);
La validità della socket è limitata al fornitore del servizio che è in grado di soddisfare la coppia di parametri af e protocol specificata dall’utente.
Per quanto riguarda i parametri della funzione:
- af [in] Specifica la famiglia degli indirizzi, ad esempio AF_INET (che identifica gli indirizzi internet).
- type [in] Definisce il tipo della nuova socket (SOCK_STREAM o SOCK_DGRAM)
- protocol [in] Identifica il protocollo da utilizzare nella comunicazione e valido per la famiglia di indirizzi specificata (normalmente 0)
Ecco un esempio di creazione di una socket:
001SOCKET sockFd;
002sockFd = socket(AF_INET, SOCK_STREAM, 0);
003if(sockFd == INVALID_SOCKET){
004 printf(“ERROR: Invalid socket!\n”);
005}
Associare la socket ad un indirizzo
Una volta che sia stata creata la socket ed impostata la struttura sockaddr_in per definirne la famiglia, l’indirizzo e la porta, all’applicazione non rimane che associare la socket alla struttura (l’indirizzo locale). In generale l’applicazione effettua questa associazione tramite la funzione bind() e la tripletta famiglia, indirizzo e porta è detta nome della socket.
001int bind(
002 SOCKET s,
003 const struct sockaddr FAR* name,
004 int namelen
005);
Questa funzione normalmente è chiamata prima delle funzioni connect() o listen() ed è utilizzata sia con socket orientate alla connessione, che con socket senza connessione.
La funzione ha tre parametri:
- s [in] Il descrittore della socket
- name [in] L’indirizzo da assegnare alla socket
- namelen [in] La lunghezza del parametro name
Il valore ritornato è zero nel caso in cui non vi siano errori oppure SOCKET_ERROR.
Se per l’applicazione è indifferente quale indirizzo sia assegnato alla socket, per il parametro name può essere utilizzata la costante ADDR_ANY che consente al fornitore del servizio, che opera sotto l’interfaccia Winsock, di stabilire un appropriato indirizzo di rete. Questo semplifica la programmazione di applicazioni in presenza di host con più di un’interfaccia di rete.
Per il TCP/IP se è specificata la porta 0, allora il fornitore del servizio assegnerà una porta unica all’applicazione con il con il valore compreso tra 1024 e 5000.
Di seguito un esempio di codice sorgente che riassume quanto detto fino ad ora.
001#include <winsock2.h>
002#define PORT_NUM 1100
003WORD wVersionRequested;
004WSADATA wsaData;
005Struct sockaddr_in myAddr;
006SOCKET sockFd;
007
008/* Winsock initialisation */
009wVersionRequested = MAKEWORD(2, 2);
010if (0 != WSAStartup(wVersionRequested, &wsaData)) {
011 return;
012}
013
014if (LOBYTE(wsaData.wVersion ) != 2 ||
015HIBYTE( wsaData.wVersion ) != 2){
016 WSACleanup();
017 return;
018}
019
020/* Local address specification */
021myAddr.sin_family = AF_INET; // host byte order
022myAddr.sin_port = htons(PORT_NUM);
023myAddr.sin_addr.s_addr = htonl(ADDR_ANY);
024memset(&(myAddr.sin_zero), '\0', 8);
025
026/* Socket creation */
027sockFd = socket(AF_INET, SOCK_STREAM, 0);
028if(INVALID_SOCKET == sockFd){
029 printf(“ERROR: Invalid socket!\n”);
030}
031
032/* Binding */
033if(SOCKET_ERROR == bind(sockFd, (struct sockaddr*)&myAddr,
034sizeof(struct sockAddr))){
035 printf(“ERROR: unsuccessful binding!\n”);
036}
037.
038.
039.
Quando si tenta di associare una porta ad una socket è bene ricordare che tutte le porte sotto la 1024 sono riservate, mentre sono utilizzabili tutti i valori a partire da 1024 fino a 65535, se non già utilizzati da qualche altra applicazione.
Vi possono essere dei casi in cui non è necessario chiamare la funzione bind(), ad esempio quando si utilizza la funzione connect() per connettersi ad una socket su un computer remoto e non si è interessati all’utilizzo di un particolare indirizzo locale: in questo caso il sistema lo assegna automaticamente.
Attendere una connessione
Per accettare connessioni da applicazioni remote, la prima cosa da fare è creare una socket con la funzione socket(), poi assegnarle un indirizzo locale tramite la chiamata alla funzione bind(), ed infine rimanere in attesa di nuove connessione utilizzando la funzione listen().
001int listen(
002 SOCKET s,
003 int backlog
004);
La socket passata come parametro è normalmente una socket orientata alla connessione ed è associata all’indirizzo su cui mettersi in ascolto, la funzione configura la socket in uno stato passivo nel quale le connessioni entranti sono riconosciute ed accodate per essere processate.
Il parametro backlog definisce il numero massimo di connessioni che possono essere messe in coda prima di essere servite.
Segue un esempio di codice sorgente.
001.
002.
003.
004/* Local address specification */
005myAddr.sin_family = AF_INET; // host byte order
006myAddr.sin_port = htons(PORT_NUM);
007myAddr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
008memset(&(myAddr.sin_zero), '\0', 8);
009
010/* Socket creation */
011sockFd = socket(AF_INET, SOCK_STREAM, 0);
012if(INVALID_SOCKET == sockFd){
013 printf(“ERROR: Invalid socket!\n”);
014}
015
016/* Binding */
017if(SOCKET_ERROR == bind(sockFd, (struct sockaddr*)&myAddr,
018 sizeof(struct sockAddr))){
019 printf(“ERROR: unsuccessful binding!\n”);
020}
021
022/* Listening */
023if(SOCKET_ERROR == listen(sockFd, 10)){
024 printf(“ERROR: unsuccessful binding!\n”);
025}
026.
027.
028.
Accettare una connessione
Una volta in ascolto, l’applicazione accetta le connessioni entranti utilizzando la funzione accept(). Questa funzione estrae dalla coda la prima connessione in attesa sulla socket passata come parametro.
001SOCKET accept(
002 SOCKET s,
003 struct sockaddr FAR* addr,
004 int FAR* addrlen
005);
Questa funzione è utilizzata con socket orientate alla connessione.
- s [in] Descrittore della socket che è stata posta nello stato di ascolto.
- addrs [out] Un puntatore opzionale al buffer che riceve l’indirizzo della connessione entrante.
- addrlen [out] Un puntatore opzionale all’intero che contiene la dimensione del parametro addr.
Questa funzione può bloccare il chiamante finché una connessione non è presente nella coda e la socket è marcata come bloccante.
Se non si verificano errori la funzione restituisce il descrittore della nuova socket, questo valore rappresenta l’handle della socket sulla quale sarà fatta l’attuale connessione. Nel caso in cui la funzione non abbia esito positivo sarà ritornata la costante INVALID_SOCKET.
Connessione ad una socket remota
Quando un’applicazione (tipicamente un client) vuole connettersi ad una specifica socket di destinazione ha a sua disposizione la funzione connect().
001int connect(
002 SOCKET s,
003 const struct sockaddr FAR* name,
004 int namelen
005);
Questa funzione stabilisce la connessione con l’indirizzo remoto specificato ed assegna automaticamente un indirizzo locale alla socket specificata come parametro.
- s [in] Il descrittore della socket locale
- name [in] L’indirizzo di destinazione
- namelen [in] La dimensione del parametro name
Per socket orientate alla connessione, la funzione inizializza una connessione attiva con l’host remoto; quando la funzione termina con successo, la socket è pronta per trasmettere e/o ricevere dati.
Per una socket non orientata alla connessione la funzione stabilisce semplicemente un indirizzo di destinazione di default che sarà utilizzate da chiamate successive alle funzioni send()/WSASend() e recv()/WSARecv().
Se connect() termina senza errori restituisce zero, altrimenti SOCKET_ERROR.
Trasmissione dei dati
La trasmissione dei dati può essere fatta utilizzando due chiamate di funzioni principali send() e sendto().
La funzione send() è utilizzata per spedire i dati utilizzando una socket connessa.
001int send(
002 SOCKET s,
003 const char FAR* buf,
004 int len,
005 int flags
006);
La terminazione della funzione senza errori non significa che i dati siano stati trasmessi senza errori.
- s [in] Il descrittore della socket connessa.
- buf [in] Un buffer contenente i dati da trasmettere.
- len [in] La lunghezza dei dati presenti in buf.
- flags [in] Un indicatore che specifica il modo in cui deve essere fatta la chiamata.
La funzione sendto() è utilizzata per scrivere dati su una socket.
001int sendto(
002 SOCKET s,
003 const char FAR* buf,
004 int len,
005 int flags,
006 const struct sockaddr FAR* to,
007 int tolen
008);
Se i dati sono troppo lunghi per passare atomicamente al protocollo sottostante, nessun dato sarà trasmesso e sarà ritornato un errore.
- s [in] Un descrittore che identifica una socket (possibilmente connessa).
- buf [in] Un buffer contenente i dati da trasmettere.
- len [in] La lunghezza dei dati nel buffer.
- flags [in] Un indicatore che specifica il modo in cui è fatta la chiamata.
- to [in] Un puntatore opzionale all’indirizzo della socket di destinazione.
- tolen [in] La dimensione dell’indirizzo nel parametro to.
La funzione sendto() è normalmente usata su una socket senza connessione per spedire un datagramma ad una specifica socket identificata dal parametro to. Perfino se la socket senza connessione è stata precedentemente connessa ad un indirizzo specifico, il parametro to sovrascrive l’indirizzo di destinazione solamente per questo particolare datagramma. Inoltre tramite questa funzione è possibile spedire messaggi broadcast specificando come indirizzo IP INADDR_BROADCAST definito nel file WINSOCK2.H.
Quando si usa la funzione send()/sendto() con socket orientate alla connessione è necessario fare attenzione che la dimensione del messaggio da trasmettere non superi la lunghezza massima prevista dal protocollo utilizzato, che può essere ottenuta utilizzando la funzione getsockopt(). Se i dati sono troppo lunghi da passare atomicamente al protocollo sottostante, nessun dato è trasmesso.
I parametro flags può essere utilizzato per influenzare il comportamento della funzione, normalmente il valore è zero.
Se non si verifica alcun errore, la funzione send()/sendto() restituisce il numero totale dei byte trasmessi, che possono essere anche meno del numero indicato dal parametro len; altrimenti restituisce il valore SOCKET_ERROR.
Se non è disponibile alcuno spazio all’interno del buffer del sistema di trasporto per contenere i dati da trasmettere, le due funzioni non ritornerà a meno che la socket non sia configurata come non bloccante.
Ricezione dei dati
Come per la trasmissione dei dati, anche per la ricezione esistono due funzioni principali: recv() e recvfrom().
La funzione recv() è usata per leggere dati sia su una socket orientata alla connessione che su una socket non orientata alla connessione.
001int recv(
002 SOCKET s,
003 char FAR* buf,
004 int len,
005 int flags
006);
Se la funzione è utilizzata su una socket orientata alla connessione, prima di iniziare la ricezione dei dati è necessario connettere la socket.
- s [in] Il descrittore della socket connessa.
- buf [out] Un buffer per i dati in arrivo.
- len [in] La lunghezza di buf.
- flags [in] Un indicatore che specifica il modo in cui deve essere fatta la chiamata.
La funzione recvfrom() è utilizzata per ricevere i dati sia da una socket connessa che non connessa e cattura l’indirizzo da cui sono stati spediti i dati.
001int recvfrom(
002 SOCKET s,
003 char FAR* buf,
004 int len,
005 int falgs,
006 struct sockaddr FAR* from,
007 int FAR* fromlen
008);
La socket non deve essere connessa. L’indirizzo locale della socket deve essere conosciuto. Per applicazioni server questo di solito si ottiene tramite la chiamata di funzione bind().
- s [in] Un descrittore che identifica una socket.
- buf [out] Un buffer per i dati da ricevere.
- len [in] La lunghezza di buffer.
- flags [in] Un indicatore che specifica il modo in cui è fatta la chiamata.
- from [out] Un puntatore opzionale ad un buffer che conterrà l’indirizzo della sorgente dopo il ritorno della funzione.
- fromlen [in] un puntatore opzionale alla dimesione del buffer from.
Se la socket è orientata alla connessione e la parte remota ha chiuso la connessione la chiamata di funzione recv()/recvfrom()terminerà immediatamente restituendo zero come numero di byte ricevuti. Se, invece, la connessione è stata resettata allora la funzione restituisce un errore (WSAECONNRESET).
Se non si verifica alcun errore, la funzione recv()/recvfrom() restituisce il numero totale dei byte ricevuti, che possono essere anche meno del numero indicato dal parametro len; altrimenti restituisce il valore SOCKET_ERROR.
Se non arriva alcun dato alla socket le due funzioni rimarranno in attesa di ricevere i dati richiesti, a meno che la socket non sia configurata come bloccante.
Chiusura della socket
La funzione closesocket() è utilizzata per chiudere e rilasciare le risorse associate ad una socket. Qualunque successivo utilizzo della socket restituirà un errore.
001int closesocket(
002 SOCKET s
003);
Un’applicazione dovrebbe chiamare la funzione closesocket() per ogni chiamata alla funzione socket() conclusa con successo.
Per assicurare che tutti i dati siano spediti e ricevuti su di una socket connessa prima che sia chiusa, un’applicazione dovrebbe utilizzare la funzione shutdown() per chiudere la connessione prima di chiamare la closesocket().