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

 

Torna alla main page! Torna alla pagina principale