Bonsoir à tous.
Ce tutoriel portera sur l'utilisation des sockets en langage C, et notamment sur la création d'un module client et d'un module serveur. Cependant, le langage de démonstration importe peu, car fondamentalement le fonctionnement méthodique des sockets et leur architecture sont relativement les mêmes pour tous les langages prenant en compte leur gestion.
Ce tutoriel est mené sous Windows pour des raisons de commodité (et surtout parce que je suis sur ma session Win7), nous utiliserons donc la librairie WinSock. Toutefois, le développement est identique sous Linux, seuls l'appel aux librairies (sys/socket.h au lieu de winsock2.h), et l'initialisation de l'API diffèrent. L'IDE utilisé est Code::Blocks.
I. Introduction
Les sockets sont des objets, effectifs en couche 5 du modèle OSI, permettant d'établir des tunnels de liaison unidirectionnelle ou bidirectionnelle (selon la finalité recherchée) entre deux machines distantes, typiquement selon une topologie serveur-client.
Leur protocole de définition standard leur confère une très grande flexibilité et stabilité, et leur fonctionnement intuitif en font des outils de connexion de premier ordre.
II. Préparation liminaire
Après avoir ouvert un nouveau projet « Console application » avec Code::Blocks, nous allons commencer par linker les librairies nécessaires à nos développements :
Project → Build options... → Linker settings → Add → (aller dans mingw/lib/) et sélectionner libws2_32.a
Donc nous venons de linker la librairie WinSock2 (version 2) qui contient tous les appels fonctionnels futurs.
Ensuite, nous allons inclure les headers nécessaires, notamment WinSock2 :
Voilà, le socle est posé, nous pouvons commencer le développement.
III.Création de la socket globale
A) Activation de l'API WinSock2
Comme nous l'avons vu, nous allons utiliser l'API native WinSock2 fournie par Windows. Avant toute déclaration de sockets cette API doit être activée, ceci permet d'initialiser la pile TCP dédiée à l'application. Pour ce faire, Windows nous met à disposition la fonction standard WSAStartup(), qui renvoi la valeur 0 quand l'initialisation s'est effectuée avec succès.
Elle se compose comme suit :
wVersionRequested → spécifie la version la plus haute (last updated) utilisée pour le protocole de déclaration des sockets, c'est à dire à partir de quel fichier-version le socket s'initialisera.
lpWSAData → pointeur vers la structure WSADATA qui recevra l'implémentation du socket.
Pour l'utiliser, nous devons tout d'abord déclarer une variable de type WSADATA.
Voilà, l'API est maintenant correctement activée, nous pouvons déclarer les objets sockets qui nous serviront dans l'établissement de la connexion.
B ) Déclaration de l'objet socket
La seconde étape du développement consiste donc à créer une socket unique qui catalysera l'ensemble des appels fonctionnels de la procédure d'écoute, d'envoi et de réception. La socket est créée par la fonction socket().
Elle se compose de la façon suivante :
af → famille de l'adresse (AF_INET = IPV4, AF_INET6 = IPV6, AF_NETBIOS = adresse netbios, AF_BTH = bluetooh...)
type → type de communication utilisé (SOCK_STREAM = TCP, SOCK_DGRAM = UDP, SOCK_RDM = PGM...)
protocol → protocole d'échange standard utilisé (IPPROTO_ICMP = ICMP, IPPROTO_TCP = TCP, IPPROTO_UDP = UDP...)
Pour l'utiliser, nous devons d'abord déclarer un objet SOCKET qui lui sera lié.
L'objet socket global est maintenant correctement créé et initialisé. Nous pouvons nous en servir pour les appels fonctionnels de communication.
Arrivé à cette étape, le développement est uniquement fonction du module d'interfaçage envisagé : client ou serveur. Nous commencerons par étudier les fonctions propres au client, puis les fonctions propres au serveur.
IV. Développement côté client
A) Établissement de la connexion
Il appartient au client d'ouvrir le tunnel de communication vers le serveur, pour des raisons techniques évidentes : le serveur possède une adresse IP fixe, et celle du client peut quant à elle varier.
La première fonction à mettre en place est donc celle qui établit la connexion, nous utiliserons pour cela la fonction connect().
Elle se compose comme suit :
s → le socket par l'intermédiaire duquel on ouvre la connexion
*name → pointeur vers la structure d'adressage contenant adresse IP et port
namelen → taille de la structure d'adressage
Avant de l'utiliser, nous devons déclarer une nouvelle structure d'adressage via l'objet SOCKADDR_IN.
A ce stade, la connexion est correctement établie.
B ) Envoi de données
La connexion établie, le client peut enfin envoyer des données au serveur. Ceci est possible via l'appel à la fonction send().
Elle se compose de cette manière :
s → socket utilisé pour envoyer les données
*buf → pointeur vers le buffer alloué au stockage des données à envoyer
len → taille des données contenues dans le buffer
flags → spécification du type de routage, généralement mis à 0
La partie client est maintenant terminée, toutefois il est nécessaire de libérer la pile TCP en fermant la connexion. Ceci s'effectue via la fonction shutdown().
Elle se compose comme suit :
s → socket utilisée par la session TCP
how → type de communication à fermer (0 = réception, 1 = envoi, 2 = envoi et réception)
Après avoir fermé la session TCP, nous devons libérer l'objet socket utilisé pendant le développement. Ceci s'effectue via la fonction closesocket().
Elle se compose comme suit :
s → socket à libérer
Pour finir, l'API WinSock doit être désactivée afin de libérer le canal. Ceci s'effectue via la fonction WSACleanup() ; cette fonction doit être utilisée autant de fois que l'API a été activée dans le développement.
Comme vous l'aurez compris, ces trois bouts de code sont à ajouter en fin de partie client.
Le module client est maintenant totalement terminé.
V. Développement côté serveur
A) Synchronisation
Afin de pouvoir recevoir les connexions de la part des clients, la socket serveur doit être synchronisée sur un port et une adresse IP d'écoute. Pour ce faire, nous utilisons la fonction bind().
Elle se compose comme suit :
s → socket à synchroniser
*name → pointeur vers la structure d'adressage
namelen → taille de la structure d'adressage
La socket étant maintenant synchronisée, le serveur doit écouter le port de réception en attente de connexions entrantes.
B ) Mise en écoute
Afin d'identifier et d'accepter les connexions entrantes des clients, le serveur doit systématiquement être placé en attente de connexions, c'est la mise en écoute. Ceci s'effectue via la fonction listen().
Elle se compose de cette façon :
s → socket utilisée pour l'écoute
backlog → taille de la file d'attente
Le serveur peut maintenant recevoir la demande de connexion de la part d'un client. Une fois reçue, il doit accepter cette connexion.
C) Validation de l'établissement de la connexion
Quand un client envoi une demande de connexion au serveur, ce dernier doit valider l'ouverture de la session TCP via la fonction accept().
Elle se compose de cette manière :
s → socket par laquelle s'effectue l'établissement de la connexion
*addr → pointeur vers la structure d'adressage
*addrlen → pointeur vers la taille de la structure d'adressage
Cette fonction est particulière car elle nécessite la déclaration d'un nouvel objet socket, le premier étant monopolisé par la fonction listen().
La connexion TCP est maintenant établie entre le client et le serveur. Ce dernier doit maintenant récupérer dans un tableau les données envoyées par le client.
D. Récupération des données envoyées par le client
Une fois la connexion établie, le serveur doit être capable de récupérer les données envoyées par le client, de les stocker puis, pourquoi pas, les afficher. Ceci s'effectue via la fonction recv().
Elle se compose comme suit :
s → socket utilisée pour la réception
*buf → pointeur vers le buffer stockant les données reçues
len → taille du buffer de réception des données
flags → gestion du comportement de routage, généralement mis à 0
Le module serveur est maintenant totalement terminé.
-------------------------------------------------------
N'hésitez pas à me demander d'éclaircir d'autres points concernant les sockets, j'éditerai mon post au fur et à mesure.
Ce tutoriel portera sur l'utilisation des sockets en langage C, et notamment sur la création d'un module client et d'un module serveur. Cependant, le langage de démonstration importe peu, car fondamentalement le fonctionnement méthodique des sockets et leur architecture sont relativement les mêmes pour tous les langages prenant en compte leur gestion.
Ce tutoriel est mené sous Windows pour des raisons de commodité (et surtout parce que je suis sur ma session Win7), nous utiliserons donc la librairie WinSock. Toutefois, le développement est identique sous Linux, seuls l'appel aux librairies (sys/socket.h au lieu de winsock2.h), et l'initialisation de l'API diffèrent. L'IDE utilisé est Code::Blocks.
I. Introduction
Les sockets sont des objets, effectifs en couche 5 du modèle OSI, permettant d'établir des tunnels de liaison unidirectionnelle ou bidirectionnelle (selon la finalité recherchée) entre deux machines distantes, typiquement selon une topologie serveur-client.
Leur protocole de définition standard leur confère une très grande flexibilité et stabilité, et leur fonctionnement intuitif en font des outils de connexion de premier ordre.
II. Préparation liminaire
Après avoir ouvert un nouveau projet « Console application » avec Code::Blocks, nous allons commencer par linker les librairies nécessaires à nos développements :
Project → Build options... → Linker settings → Add → (aller dans mingw/lib/) et sélectionner libws2_32.a
Donc nous venons de linker la librairie WinSock2 (version 2) qui contient tous les appels fonctionnels futurs.
Ensuite, nous allons inclure les headers nécessaires, notamment WinSock2 :
Code:
#include <stdio.h> #include <winsock2.h> int main() { return 0; }
III.Création de la socket globale
A) Activation de l'API WinSock2
Comme nous l'avons vu, nous allons utiliser l'API native WinSock2 fournie par Windows. Avant toute déclaration de sockets cette API doit être activée, ceci permet d'initialiser la pile TCP dédiée à l'application. Pour ce faire, Windows nous met à disposition la fonction standard WSAStartup(), qui renvoi la valeur 0 quand l'initialisation s'est effectuée avec succès.
Elle se compose comme suit :
Code:
int WSAStartup( _In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData );
lpWSAData → pointeur vers la structure WSADATA qui recevra l'implémentation du socket.
Pour l'utiliser, nous devons tout d'abord déclarer une variable de type WSADATA.
Code:
#include <stdio.h> #include <winsock2.h> int main() { WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); return 0; }
B ) Déclaration de l'objet socket
La seconde étape du développement consiste donc à créer une socket unique qui catalysera l'ensemble des appels fonctionnels de la procédure d'écoute, d'envoi et de réception. La socket est créée par la fonction socket().
Elle se compose de la façon suivante :
Code:
SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol );
type → type de communication utilisé (SOCK_STREAM = TCP, SOCK_DGRAM = UDP, SOCK_RDM = PGM...)
protocol → protocole d'échange standard utilisé (IPPROTO_ICMP = ICMP, IPPROTO_TCP = TCP, IPPROTO_UDP = UDP...)
Pour l'utiliser, nous devons d'abord déclarer un objet SOCKET qui lui sera lié.
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ return 0; }
L'objet socket global est maintenant correctement créé et initialisé. Nous pouvons nous en servir pour les appels fonctionnels de communication.
Arrivé à cette étape, le développement est uniquement fonction du module d'interfaçage envisagé : client ou serveur. Nous commencerons par étudier les fonctions propres au client, puis les fonctions propres au serveur.
IV. Développement côté client
A) Établissement de la connexion
Il appartient au client d'ouvrir le tunnel de communication vers le serveur, pour des raisons techniques évidentes : le serveur possède une adresse IP fixe, et celle du client peut quant à elle varier.
La première fonction à mettre en place est donc celle qui établit la connexion, nous utiliserons pour cela la fonction connect().
Elle se compose comme suit :
Code:
int connect( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen );
*name → pointeur vers la structure d'adressage contenant adresse IP et port
namelen → taille de la structure d'adressage
Avant de l'utiliser, nous devons déclarer une nouvelle structure d'adressage via l'objet SOCKADDR_IN.
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /************** ETABLISSEMENT DE LA CONNEXION AU SERVEUR *********/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage char adresse_ip_serveur[] = "xxx.xxx.xxx.xx.xxx"; // définition de l'adresse IP du serveur int port = 80; // définition du port de destination iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = inet_addr(adresse_ip_serveur); // passage de l'adresse IP du serveur en adresse de destination iSocket_Addr.sin_port = htons(port); // passage du port du serveur en port de destination // établissement de la connexion if(connect(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la connexion printf("Connexion failed → %d",WSAGetLastError()); return 1; } else printf("Connexion established"); /********** FIN D'ETABLISSEMENT DE LA CONNEXION AU SERVEUR **********/ return 0; }
B ) Envoi de données
La connexion établie, le client peut enfin envoyer des données au serveur. Ceci est possible via l'appel à la fonction send().
Elle se compose de cette manière :
Code:
int send( _In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags );
*buf → pointeur vers le buffer alloué au stockage des données à envoyer
len → taille des données contenues dans le buffer
flags → spécification du type de routage, généralement mis à 0
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /************** ETABLISSEMENT DE LA CONNEXION AU SERVEUR *********/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage char adresse_ip_serveur[] = "xxx.xxx.xxx.xx.xxx"; // définition de l'adresse IP du serveur int port = 80; // définition du port de destination iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = inet_addr(adresse_ip_serveur); // passage de l'adresse IP du serveur en adresse de destination iSocket_Addr.sin_port = htons(port); // passage du port du serveur en port de destination // établissement de la connexion if(connect(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la connexion printf("Connexion failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Connexion established"); /********** FIN D'ETABLISSEMENT DE LA CONNEXION AU SERVEUR **********/ /********* ENVOI DES DONNEES AU SERVEUR ********/ #define BUFFER_LEN 65000 // taille du buffer contenant les données à envoyer char datasToSendBuffer[BUFFER_LEN]; // initialisation du buffer contenant les données à envoyer /************************************************************************** Les données à envoyer peuvent être de tout type, numérique (valeurs numériques, ex : clé de validation) ou sous format texte. Pour envoyer une string, l'on peut soit la déclarer directement dans le buffer comme ceci : char datasToSendBuffer[] = "Hello le serveur !"; soit copier dynamiquement la chaine dans le buffer via la fonction strcopy(). Cette dernière option est à privilégier car la mémoire est gérer plus précisément. ***************************************************************************/ strcpy(datasToSendBuffer, "Hello le serveur !"); // copie dynamique dans le buffer // envoi des données if((connect(iSocket, datasToSendBuffer, strlen(datasToSendBuffer), 0))!=0) { // échec de l'envoi printf("Datas sending failed → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Datas sent successfully"); /********* FIN DE L'ENVOI DES DONNEES AU SERVEUR ********/ return 0; }
La partie client est maintenant terminée, toutefois il est nécessaire de libérer la pile TCP en fermant la connexion. Ceci s'effectue via la fonction shutdown().
Elle se compose comme suit :
Code:
int shutdown( _In_ SOCKET s, _In_ int how );
how → type de communication à fermer (0 = réception, 1 = envoi, 2 = envoi et réception)
Code:
if((shutdown(iSocket,2))!=0) { // échec de la fermeture printf("TCP session closing → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("TCP session closed successfully");
Après avoir fermé la session TCP, nous devons libérer l'objet socket utilisé pendant le développement. Ceci s'effectue via la fonction closesocket().
Elle se compose comme suit :
Code:
int closesocket( _In_ SOCKET s );
Code:
if((closesocket(iSocket))!=0) { // échec de la libération printf("Socket closing → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket closed successfully");
Pour finir, l'API WinSock doit être désactivée afin de libérer le canal. Ceci s'effectue via la fonction WSACleanup() ; cette fonction doit être utilisée autant de fois que l'API a été activée dans le développement.
Code:
if((WSACleanup())!=0) { // échec de la désactivation printf("WSA deactivation → %d", WSAGetLastError()); return 1; } else printf("WSA deactived successfully");
Comme vous l'aurez compris, ces trois bouts de code sont à ajouter en fin de partie client.
Le module client est maintenant totalement terminé.
V. Développement côté serveur
A) Synchronisation
Afin de pouvoir recevoir les connexions de la part des clients, la socket serveur doit être synchronisée sur un port et une adresse IP d'écoute. Pour ce faire, nous utilisons la fonction bind().
Elle se compose comme suit :
Code:
int bind( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen );
*name → pointeur vers la structure d'adressage
namelen → taille de la structure d'adressage
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /********** SYNCHRONISATION DU SOCKET **************/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage int port = 80; // définition du port d'écoute iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = INADDR_ANY; // comme l'on ne connait pas l'IP du client qui se connectera, on écoute toutes les IPs entrantes iSocket_Addr.sin_port = htons(port); // passage du port d'écoute // établissement de la synchronisation if(bind(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la synchronisation printf("Synchronization failed → %d",WSAGetLastError()); WSACleanup(); // libration de l'API return 1; } else printf("Synchronization established"); /***** FIN DE SYNCHRONISATION DU SOCKET ************/ return 0; }
La socket étant maintenant synchronisée, le serveur doit écouter le port de réception en attente de connexions entrantes.
B ) Mise en écoute
Afin d'identifier et d'accepter les connexions entrantes des clients, le serveur doit systématiquement être placé en attente de connexions, c'est la mise en écoute. Ceci s'effectue via la fonction listen().
Elle se compose de cette façon :
Code:
int listen( _In_ SOCKET s, _In_ int backlog );
backlog → taille de la file d'attente
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /********** SYNCHRONISATION DU SOCKET **************/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage int port = 80; // définition du port d'écoute iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = INADDR_ANY; // comme l'on ne connait pas l'IP du client qui se connectera, on écoute toutes les IPs entrantes iSocket_Addr.sin_port = htons(port); // passage du port d'écoute // établissement de la synchronisation if(bind(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la synchronisation printf("Synchronization failed → %d",WSAGetLastError()); WSACleanup(); // libration de l'API return 1; } else printf("Synchronization established"); /***** FIN DE SYNCHRONISATION DU SOCKET ************/ /****** ECOUTE DE CONNEXIONS ENTRANTES *******/ int switch = 1; // switch de contrôle de la boucle while(switch!=0) { // en attente de connexion (connexion effectuée → switch = 0) switch = listen(iSocket,5); // mise en écoute pour 5 connexions clients } printf("Awaiting connexion..."); /********* FIN D'ECOUTE DE CONNEXIONS ENTRANTES *****/ return 0; }
Le serveur peut maintenant recevoir la demande de connexion de la part d'un client. Une fois reçue, il doit accepter cette connexion.
C) Validation de l'établissement de la connexion
Quand un client envoi une demande de connexion au serveur, ce dernier doit valider l'ouverture de la session TCP via la fonction accept().
Elle se compose de cette manière :
Code:
SOCKET accept( _In_ SOCKET s, _Out_ struct sockaddr *addr, _Inout_ int *addrlen );
*addr → pointeur vers la structure d'adressage
*addrlen → pointeur vers la taille de la structure d'adressage
Cette fonction est particulière car elle nécessite la déclaration d'un nouvel objet socket, le premier étant monopolisé par la fonction listen().
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /********** SYNCHRONISATION DU SOCKET **************/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage int port = 80; // définition du port d'écoute iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = INADDR_ANY; // comme l'on ne connait pas l'IP du client qui se connectera, on écoute toutes les IPs entrantes iSocket_Addr.sin_port = htons(port); // passage du port d'écoute // établissement de la synchronisation if(bind(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la synchronisation printf("Synchronization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Synchronization established"); /***** FIN DE SYNCHRONISATION DU SOCKET ************/ /****** ECOUTE DE CONNEXIONS ENTRANTES *******/ int switch = 1; // switch de contrôle de la boucle while(switch!=0) { // en attente de connexion (connexion effectuée → switch = 0) switch = listen(iSocket,5); // mise en écoute pour 5 connexions clients } printf("Awaiting connexion..."); /********* FIN D'ECOUTE DE CONNEXIONS ENTRANTES *****/ /********* VALIDATION D'OUVERTURE DE SESSION TCP *******/ SOCKET iSocket2; // déclaration de la deuxième socket // validation de l'ouverture TCP via la nouvelle socket iSocket2 = accept(iSocket,(struct sockaddr*)&iSocket_Addr,&sizeof(iSocket_Addr)); if(iSocket2 == INVALID_SOCKET) { // échec de la validation printf("TCP session establishement failed→ %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("TCP session established"); /***** FIN DE VALIDATION D'OUVERTURE DE SESSION TCP *****/ return 0; }
La connexion TCP est maintenant établie entre le client et le serveur. Ce dernier doit maintenant récupérer dans un tableau les données envoyées par le client.
D. Récupération des données envoyées par le client
Une fois la connexion établie, le serveur doit être capable de récupérer les données envoyées par le client, de les stocker puis, pourquoi pas, les afficher. Ceci s'effectue via la fonction recv().
Elle se compose comme suit :
Code:
int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags );
*buf → pointeur vers le buffer stockant les données reçues
len → taille du buffer de réception des données
flags → gestion du comportement de routage, généralement mis à 0
Code:
#include <stdio.h> #include <winsock2.h> int main() { /******************* ACTIVATION DE L'API **************/ WSADATA wsadata; // déclaration de la variable de type WSADATA /*********************************************************************** Le paramètre « MAKEWORD » contient la version de WinSock utilisée, ici la version 2. Le deuxième paramètre est le pointeur renvoyant vers la structure WSADATA. ***********************************************************************/ WSAStartup(MAKEWORD(2,2),&wsadata); // activation // on test l'initialisation if((WSAStartup(MAKEWORD(2,2),&wsadata))!=0) { // l'initialisation a échoué printf("Initialization failed → %d",WSAGetLastError()); return 1; } else printf("WSA initialized successfully"); /*************** FIN D'ACTIVATION DE L'API ***************/ /*********** INITIALISATION DU SOCKET **************/ SOCKET iSocket; // déclaration de l'objet socket /********************************************************************** La premier paramètre « AF_INET » signifie que l'on choisit la famille bidirectionnelle Le deuxième paramètre « SOCK_STREAM » signifie que l'on choisit le type TCP Le troisième paramètre « IPPROTO_TCP » signifie le choix du protocole TCP **********************************************************************/ iSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // initialisation // on test l'initialisation if(iSocket == INVALID_SOCKET) { // échec de l'initialisation printf("Socket initialization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket initialized successfully"); /*************** FIN DE L'INITIALISATION DU SOCKET ************/ /********** SYNCHRONISATION DU SOCKET **************/ SOCKADDR_IN iSocketAddr; // déclaration de la structure d'adressage int port = 80; // définition du port d'écoute iSocketAddr.sin_family = AF_INET; // structure de famille AF_INET (pour TCP) iSocket_Addr.sin_addr.s_addr = INADDR_ANY; // comme l'on ne connait pas l'IP du client qui se connectera, on écoute toutes les IPs entrantes iSocket_Addr.sin_port = htons(port); // passage du port d'écoute // établissement de la synchronisation if(bind(iSocket,(struct sockaddr*)&iSocketAddr,sizeof(iSocketAddr))!=0) { // échec de la synchronisation printf("Synchronization failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Synchronization established"); /***** FIN DE SYNCHRONISATION DU SOCKET ************/ /****** ECOUTE DE CONNEXIONS ENTRANTES *******/ int switch = 1; // switch de contrôle de la boucle while(switch!=0) { // en attente de connexion (connexion effectuée → switch = 0) switch = listen(iSocket,5); // mise en écoute pour 5 connexions clients } printf("Awaiting connexion..."); /********* FIN D'ECOUTE DE CONNEXIONS ENTRANTES *****/ /********* VALIDATION D'OUVERTURE DE SESSION TCP *******/ SOCKET iSocket2; // déclaration de la deuxième socket // validation de l'ouverture TCP via la nouvelle socket iSocket2 = accept(iSocket,(struct sockaddr*)&iSocket_Addr,&sizeof(iSocket_Addr)); if(iSocket2 == INVALID_SOCKET) { // échec de la validation printf("TCP session establishement failed→ %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("TCP session established"); /***** FIN DE VALIDATION D'OUVERTURE DE SESSION TCP *****/ /***** RECEPTION DES DONNEES CLIENT *****/ #define BUFFER_LEN 65000 // taille du buffer contenant les données reçues char datasToStockBuffer[BUFFER_LEN]; // initialisation du buffer contenant les données reçues // réception des données if((recv(iSocket2, datasToStockBuffer, BUFFER_LEN, 0)) == INVALID_SOCKET) { // échec printf("Datas reception failed → %d",WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else { datasToStockBuffer[recv(iSocket2, datasToStockBuffer, BUFFER_LEN, 0)] = 0; // fermeture de flux de sortie du tableau printf("Datas received : %s", datasToStockBuffer); } /************ FIN DE RECEPTION DES DONNEES CLIENT *****/ /***** FERMETURE SESSION TCP, LIBERATION SOCKETS ET API ******/ // fermeture session TCP if((shutdown(iSocket2,2))!=0) { // échec de la fermeture printf("TCP session closing → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("TCP session closed successfully"); // libération des sockets if((closesocket(iSocket))!=0) { // échec de la libération printf("Socket closing → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket closed successfully"); if((closesocket(iSocket2))!=0) { // échec de la libération printf("Socket closing → %d", WSAGetLastError()); WSACleanup(); // libération de l'API return 1; } else printf("Socket closed successfully"); // désactivation de l'API if((WSACleanup())!=0) { // échec de la désactivation printf("WSA deactivation → %d", WSAGetLastError()); return 1; } else printf("WSA deactived successfully"); return 0; }
Le module serveur est maintenant totalement terminé.
-------------------------------------------------------
N'hésitez pas à me demander d'éclaircir d'autres points concernant les sockets, j'éditerai mon post au fur et à mesure.
Commentaire