Winsock - BASIC TUTORIAL
Questo è il mio primo tutorial
sull'uso delle socket, quindi partirò da un livello abbastanza basso, do
comunque per scontata la conoscenza del linguaggio C/C++, cercherò anche di
spiegare il più possibile le socket, le porte, gli indirizzi IP etc, ma se non
capite potete comunque contattarmi e chiedere ulteriori spiegazioni e/o
chiarimenti, sono sempre a disposizione per aiutare gli amici della rete!. Dato
che niente è meglio di un sorgente per imparare la programmazione, includerò al
seguente tutorial un file già pronto per essere compilato con la maggior parte
dei compilatori in circolazione (C++Builder, Visual C, Watcom), ed intercalerò
le istruzioni a dei robusti commenti.
Il programma che andrò a
realizzare è un miniserver che aspetta sulla porta TELNET che qualcuno si
connetta, gli risponde con un messaggio di test e gli rimanda indietro
qualsiasi cosa esso invii con la tastiera.
Alla pressione del tasto ESC
il server chiude la connessione. Mi rendo conto che non è un gran che, comunque
è sufficiente per vedere il funzionamento delle socket.
In verde ci sono i commenti, il resto scritto in Courier New nero è il codice del programma.
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <winsock.h>
#include
<conio.h>
includo tutti i file header che mi serviranno nel
programma, fin qui penso la cosa sia abbastanza chiara a tutti, definisco anche
il codice del tasto “ESC”, che controllerò nello stream di dati in ingresso per
capire quando è il momento di chiudere la connessione
#define KEY_ESC 0x1b
int main (void){
SOCKET
sock21,asock;
sockaddr_in
sock_in21,asock_in;
WORD
wVersionRequested;
WSADATA
wsaData;
int err,
backlog=1, addrlen;
char send_buff[]="
*SERVER TELNET DI PROVA*\n\r
*RECEIVE ONLY*\n\r*PREMI ESC PER SGANCIARTI*\n\r";
char
recv_buff=' ';
Dichiaro alcune variabili per il programma, tra cui
sock21 di tipo SOCKET e sock_in21 di tipo sockaddr_in.Si può pensare alla
socket come all’handler di un file aperto con fopen, le seguenti funzioni
dovranno poi avere come argomento il file handler per operare su tale file,
stessa cosa vale per le Socket.Sia il client che il server hanno bisogno di una
socket valida per accedere alla rete.
wVersionRequested = MAKEWORD( 2, 0 );
//winsock ver2.
Stampo
sulla consolle il messaggio sottostante, avvisando che il miniserver è in
attesa sulla porta n°21 di una qualche connessione.
printf("MINISERVER TCP/IP listen su
porta TELNET\n\n");
err = WSAStartup( wVersionRequested,
&wsaData );
if ( err != 0 ) {
printf("Nessuna
winsock.dll trovata!\n");
return(1);
}
else
printf("Ver$ %d\nHVer$ %d\nMaxSock %d\nMaxUdpDg %d\nDescription:
%s\nStatus: %s\n\n",
wsaData.wVersion,
wsaData.wHighVersion,
wsaData.iMaxSockets,
wsaData.iMaxUdpDg,
wsaData.szDescription,
wsaData.szSystemStatus);
Il codice soprastante serve per inizializzare la
wsock32.dll, tramite la funzione WSAStartup() che è specifica di Windoze, le
vengono passate come argomenti la versione di libreria richiesta e l'indirizzo
ad una struttura WSAData, che verrà riempita dalla WSAStartup con informazioni
relative alla libreria stessa. In caso di errore la WSAStartup ritorna un
codice diverso da 0 che stà ad indicare l'errore in questione (fai riferimento
al file winsock.h per la lista dei #define). il prefisso WSA stà ad indicare
Windows Socket API.
sock21 = socket (PF_INET, SOCK_STREAM, 0);
if(sock21==INVALID_SOCKET){
printf("Errore socket() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("socket() OK!\n");
Se tutto è andato bene nell'inizializzazione del
wsock32.dll allora si procede all'apertura della socket con l'omonima funzione
"socket()", il prototipo della funzione è
SOCKET PASCAL FAR socket (int af, int type, int
protocol)
e deve essere interpretata così: af=address family
(conosciuta come "dominio del socket"), type=tipo del socket,
protocol=protocollo da usare. Come già detto aprendo una socket si riceve come
risultato un descriptor, analogo ad un file handler, se non si può aprire il
socket il sistema ritorna l'errore INVALID_SOCKET.
Il parametro af indica la suite di protocolli che
intendiamo usare sulla rete, ad esempio PF_INET per il TCP/IP (l'unico
supportato da winsock 1.1), AF_IPX per IPX/SPX e AF_APPLETALK per il protocollo
di comunicazione della Apple.
Il parametro type spesso indica implicitamente il
protocollo all'interno dell'address family, per esempio nel nostro caso
potevamo indicare un tipo di protocollo "connession oriented" (SOCK_STREAM=TCP/IP) oppure no (SOCK_DGRAM=UDP),
io ho optato per il classico TCP/IP. Esiste un terzo tipo di socket, il
SOCK_RAW che per il momento non tratterò, aspettate e vedrete che arriverà un
tutorial anche per il SOCK_RAW e l'ICMP (internet Control Message Protocol).Il
terzo parametro in questo caso è ignorato, ma per correttezza è bene impostarlo
sempre a zero.E' bene ricordare che non è possibile collegarsi ad una porta UDP
con un protocollo TCP e viceversa, quindi il server ed il client devono
"parlare" la stessa lingua su quella socket !!
sock_in21.sin_family = PF_INET; //address
family
sock_in21.sin_port
= htons (IPPORT_TELNET); //porta
(service) number
sock_in21.sin_addr.s_addr
= INADDR_ANY; // qualsiasi address (locale)
err = bind
(sock21, (struct sockaddr*)&sock_in21, sizeof(struct sockaddr_in));
if(err==SOCKET_ERROR){printf("Errore
bind() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("bind() OK!\n");
Adesso la
socket deve essere "inizializzata" con la funzione bind() il cui
prototipo è:
int PASCAL
FAR bind (SOCKET s, struct sockaddr FAR* addr, int namelen)
questa funzione inizializza la socket locale con i
valori contenuti nella struttura sockaddr_in, di cui poi andrà fatto un cast a
(struct sockaddr) che peraltro ha lo stesso size. La struttura in questione
contiene la famiglia di indirizzi a cui appartiene la socket (PF_INET nel
nostro caso) la porta su cui mettersi in ascolto "IPPORT_TELNET" (o
collegarsi, dipende se lìapplicazione è un server oppure un client) e
l'indirizzo IP locale della macchina da cui viene chiamata la funzione bind
(INADDR_ANY ogni indirizzo è accettato). La funzione come le altre del resto,
ritorna un codice di errore nel caso di fallimento, il più frequente è WSAEADDRINUSE (10048) nel caso
in cui più client cerchino di inizializzare la stessa porta, per fortuna per i
client non è "mandatory" nominare un socket, mentre lo è invece per
un server.
err=listen(sock21, backlog);
if(err==SOCKET_ERROR){printf("Errore
listen() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("listen() OK! Aspetto
la connessione..\n");
Ora che la
socket è propriamente "nominata" si procede al listen(), ci si mette
cioè in ascolto per eventuali connessioni , il prototipo della funzione è:
int
PASCAL FAR listen( SOCKET s, int backlog)
la funzione necessita come parametro la socket
appena creata, e un intero che indica la lunghezza della coda delle connessioni
pendenti (non è la stessa cosa delle connessioni accettate).La funzione ritorna
SOCKET_ERROR nel caso di errore. Gli errori possono essere poi analizzati più
in dettaglio con la funzione WSAGetLastError() che restituisce un codice
coerente al tipo di errore occorso nell'ultima funzione chiamata (gli errori
sono descritti come #define nel file include winsock.h).
//è del tipo BLOCKING.....
addrlen=sizeof(struct sockaddr); //<-
se non fai così la accept dà WSAFAULT
asock=accept(sock21, (struct
sockaddr*)&asock_in, (LPINT)&addrlen);
if(asock==INVALID_SOCKET){printf("Errore
accept() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else {
printf("accept() OK!\n");
printf("sin_port
%d\n",ntohs(asock_in.sin_port));
printf("sin_addr
%d.%d.%d.%d\n",asock_in.sin_addr.S_un.S_un_b.s_b1
,asock_in.sin_addr.S_un.S_un_b.s_b2
,asock_in.sin_addr.S_un.S_un_b.s_b3
,asock_in.sin_addr.S_un.S_un_b.s_b4);
printf("addrlen %d\n",addrlen);
}
Per prepararsi al detecting delle connessioni
entranti è necessario chiamare la funzione sopra usata, il cui prototipo è:
SOCKET
PASCAL FAR accept(SOCKET s, struct sockaddr FAR*addr, int FAR addrlen)
qui è necessario spiegare una cosa: data la natura
multitasking di Win32 questo scelta che ho fatto non è proprio delle migliori,
in effetti sarebbe più giusto usare la funzione select() oppure più
propriamente in una applicazione win la WSAAsyncSelect() che permettono
di spettare in modo asincrono le connessioni, cioè in modo NON-BLOCKING.
In questo caso invece (per pura e semplice
vagabondaggine e per la furia di veder funzionare qualcosa) ho scelto la
soluzione di tipo BLOCKING, l'applicazione cioè si ferma in attesa della
connessione e riparte quando la connessione è avvenuta.Prossimamente su questo
sito troverete anche un programmetto che spiega l'uso della wsock32.dll anche
in modo NON-BLOCKING. Di default ogni socket nasce di tipo BLOCKING.
I parametri da passare alla accept() sono la socket
che è stata messa in listening, l'indirizzo di una struttura sockaddr_in che la
accept riempirà con i dati ricevuti dal client che vuole connettersi e come
ultimo il sizeof(struct sockaddr).La accept ritorna un nuovo SOCKET che fà
riferimento alla connessione accettata, volendo ci si può rimettere in ascolto
per accettare nuove connessioni, indipendentemente da cosa si farà sul socket
accettato, oppure si può chiudere il socket che si occupa di
"ascoltare" e dedicarci alle trasmissioni sul socket restituito dalla
accept().
Per motivi dettati dalla mia eterna vagabondaggine,
non mi rimetto in ascolto per altre connessioni, e procedo inviando un
messaggio di benvenuto a chi si è connesso con il mio server.
//invio dati al client.....
err=send(asock, &send_buff[0],
strlen(send_buff), 0);
if(err==SOCKET_ERROR){printf("Errore
send() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("send() OK\n");
Una volta accettata la connessione invio dei dati al
client con la funzione send() il cui prototipo è:
int
PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags)
i parametri da passare alla funzione sono la socket
su cui inviare i dati,un puntatore ad un buffer che contiene i dati da inviare,
la lunghezza del buffer stesso e un flag che indica il tipo di messaggio da
inviare (esempio MSG_OOB, MSG_DONTROUTE), per il momento mettete zero (0) in
seguito spiegherò il significato di questo flag (quando lo avrò capito al 100%
anche io!!!).Come le altre funzioni anche questa ritorna un codice di errore se
ci sono stati problemi di trasmissione o altri tipi di errori che si possono
analizzare con la onnipresente WSAGetLastError()....si lo sò che ripetere tutte
le volte questa porzione di codice è uno spreco ,sarebbe meglio fare una
funzioncina che analizza l'errore da richiamare tutte le volte, ma non
scordatevi che 1) questo è un programma di esempio e specialmente 2) sono un pò
pigro.......
A questo punto non rimane che star a sentire cosa
deve dirci il client:
// ricevo dati dal client ed esco se premo
ESC
printf("recv() - aspetto dati dal
client..\n");
while(recv_buff!=KEY_ESC){
err=recv(asock, &recv_buff, 1,
0); /*un byte alla volta*/
if(err==SOCKET_ERROR){printf("Errore
recv() ritorna (%d)\n",
WSAGetLastError());
return(1);}
else {
if (recv_buff!=KEY_ESC) {
printf("%c",recv_buff); //echo locale
send(asock, &recv_buff, 1, 0); //echo remoto
// un pò presuntuoso senza error
checking....
}
}
}
In questo ciclo while mi metto in attesa di dati dal
client, e non faccio altro che reinviarli indietro nello stesso modo in cui mi
arrivano, analizzando però che non ci sia un KEY_ESC, in tal caso esco dal
ciclo e chiudo la connessione.
La funzione che utilizzo per ricevere dati dal
client è la recv() il cui prototipo è:
int
PASCAL FAR recv(SOCKET s, char FAR *buff, int len, int flags)
come potete vedere la recv() ha gli stessi parametri
della send(), per cui và da se che si usa nello stesso modo della send(), unica
differenza stà che la recv() come la accept() blocca il flusso del programma in
attesa di ricevere qualcosa dal client. Ovviamente nella modalità NON-BLOCKING
usata con WSAAsyncSelect() non si bloccherebbe proprio nulla, la nostra
applicazione riceverebbe invece un
messaggio standard di Win che notificherebbe
l'arrivo di dati sulla socket (FD_READ).In questo caso invece la recv()
ritorna quando nel buffer c'è qualcosa da leggere.
printf("\n");
shutdown(asock,
2);
err=closesocket(asock);
if(err==SOCKET_ERROR){printf("Errore
closesocket(asock) ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("closesocket(listening) OK\n");
shutdown(sock21,
2);
err=closesocket(sock21);
if(err==SOCKET_ERROR){printf("Errore
closesocket(sock21) ritorna (%d)\n",
WSAGetLastError());
return(1);}
else
printf("closesocket(acccepted) OK\n");
return(0);
}
Fine del programma,
il miniserver chiude la connessione sulle socket aperte facendo prima la
shutdown() e successivamente la closesocket(). La shutdown() effettua una
parziale chiusura a seconda del flag passato dopo la socket (ad esempio si
potrebbe chiudere solo la ricezione ed inviare un messaggio del tipo "stò
per disconnetterti") e quì è stata inserita solo per prova, la funzione
necessaria per chiudere la trasmissione sulla socket è closesocket().
IMPORTANTE: per connettersi con il
miniserver non è necessario collegarsi in rete e farsi "telnettare"
da qualcuno, potete semplicemente impartire il comando "telnet
127.0.0.1" dalla linea "ESEGUI" del menù di Windoz e provare il
(fantastico) programma appena creato.
Ovviamente questo programma è
pieno di cose che andrebbero migliorate, errori da controllare, situazioni da
gestire, ma comunque per provare a capirci qualcosa nelle socket penso possa
bastare, anzi, demando a voi l'onere ad esempio di controllare che il client
vuole disconnettersi oppure un metodo per accettare nuove connessioni invece di
una sola come faccio io qui.Il mio MINISERVER non è un server echo, di cui
peraltro esistono ben dettagliate specifiche nei relativi file RFC, spero solo
possa aiutare qualcuno di voi a fare un po' di luce sull'argomento.
Ciao a tutti!!
MrCode