MTProd > Dev4all > Articles > Programmation > Réseau > Programmation Winsock avancée avec les systèmes NT > 2 Les Sockets Streams
Rechercher14 Personnes en-ligne
Programmation Winsock avancée avec les systèmes NT

2  Les Sockets Streams

Nous allons commencer par cette petite introduction sur les sockets streams pour que vous puissiez prendre conscience des différentes fonctions que peut fournir la librairie Winsock. Seulement après cette parenthèse nous nous attaquerons à la programmation des raw sockets grâce à Winsock 2 sous Windows 2000 ou encore Windows XP.



2.1  Qu'est ce qu'un socket ?

Un socket est une sorte de point de communication, il est définit par une adresse réseau (IP) et un numéro de port qui est associé à une application sur une machine. Celui-ci va permettre une communication entre deux processus locaux ou distants.




2.2  Programmation d'un serveur TCP

Dans cette sous partie nous allons étudier la création d'un petit serveur qui ne fera que réacheminer à son destinataire le message que celui ci aura précédemment envoyé (écho). Nous utiliserons Winsock 1.1 pour cette réalisation, car nous n'avons pas l'utilité de la version 2 en ce qui concerne cette application, ceci permettra une meilleur compatibilité avec les systèmes ne supportant pas Winsock 2. Etudions la structure de fonctionnement général d'un serveur :

                   +---------------------------+
| initialisation de Winsock |
+---------------------------+
|
|
+-----------------------------------------------------------------+
| Création et attachement du socket d'écoute ( socket(), bind() ) |
+-----------------------------------------------------------------+
|
|
+-----------------------------------+
| Ouverture du service ( listen() ) |
+-----------------------------------+
|
|
+------------------------------------------------+
+---->| Attente des demandes de connexion ( accept() ) |
| +------------------------------------------------+
| |
| |
| Client => | Demande de connexion |
| |
| |
| +----------------------------------------+
| | Traitement de la demande de connection |
| +----------------------------------------+
| |
| |
| | Système de multithreading si le server gère |
| | plusieurs clients en même temps |
| |
| |
+----------------------------+





2.3  Les fichiers nécessaires

Pour pouvoir utiliser Winsock 1.1, il vous faudra plusieurs fichiers que je citerais ci dessous :

  • winsock.h

  • wsock32.lib


Ces fichiers sont tous deux présents dans visual c++ 6 (vc98/include/ pour winsock.h ou encore vc98/lib pour wsock32.lib). Si encore une fois vous travaillez sous visual c++ 6 vous devrez inclure la librairie wsock32.lib dans votre projet, pour ceci : projects => settings => link et dans cette onglet vous verrez objets/library modules, vous devez ajouter dans cette case wsock32.lib. Si vous ne travaillez pas avec cette IDE, une autre solution est de charger les fonctions de la librairie dynamiquement, mais ce n'est pas le sujet de cet article. (Fonctions : LoadLibrary, GetProcAddress)




2.4  Initialisation de l'API Winsock 1.1

Afin de pouvoir utiliser Winsock, il faut l'initialiser, pour ceci une fonction existe : WSAStartup();

Prototype :

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);


Ce prototype nous indique que nous devons passer un mot en premiere argument et l'adresse d'une structure de type WSADATA en second argument.

Exemple :
Listing 2.4.1 : int InitWinsock(void)
(
ouvrir dans une nouvelle fenêtre)

 1  int InitWinsock(void)
 2  {
 3       WORD wVersionRequested;
 4       WSADATA WSAData;              /* structure WSADATA définie dans winsock.h */
 5       int err;
 6  
 7       wVersionRequested = MAKEWORD(1,1);      /* 1.1 version voulut de Winsock */     
 8       err = WSAStartup(wVersionRequested, &WSAData); /* appel de notre fonction */         
 9                        /* controle d'erreur */
 10       if(err == 0)    fprintf(stdout, "[*] Winsock a ete initialise avec succes.\n");
 11      else {
 12          fprintf(stderr, "Erreur lors de l'initialisation de Winsock, code d'erreur : %d\n", GetLastError());
 13          return(-1);
 14      }
 15  
 16      return(0);
 17  }


Maintenant nous pouvons utiliser les fonctions de la librairie Winsock 1.1 sans problèmes particuliers.




2.5  Création du socket

Maintenant que l'API Winsock est initialisée nous allons pouvoir créer notre socket, pour réaliser ceci nous devons remplir plusieurs tâches :

  • Déclarer notre socket

  • Déclarer une structure sockaddr_in qui est définie encore une fois dans winsock.h

  • Remplir cette structure avec les options voulues

  • Créer notre socket grâce à la fonction socket();

  • Associer notre adresse locale au socket créé grâce à la fonction bind();




SOCKET socket(int af, int type, int protocol);

Cette fonction va permettre de créer notre socket pour ensuite travailler sur celui-ci. Le premier paramètre définit la famille du socket, vous trouverez d'ailleurs les différentes familles existantes dans winsock.h, nous utiliserons AF_INET car cette famille représente les communications Internetwork: UDP, TCP, etc. Le second paramètre définit le type du socket, toujours une définition de tous les types existants dans winsock.h, nous nous contenterons du type SOCK_STREAM car c'est bien ce que nous recherchons, un socket stream. Enfin le troisième type représente le protocole voulut, nous utiliserons IPPROTO_IP.

int bind(SOCKET s, const struct sockaddr FAR * addr, int namelen);

Cette fonction va associer notre adresse locale au socket passé en argument. Le premier paramètre n'a normalement plus de secret pour vous, c'est un socket. Le second paramètre est l'adresse d'une structure de type sockaddr, celle que l'on a rempli juste avant l'utilisation de la fonction socket() d'ailleurs. Et pour finir le dernier paramètre est la taille de cette même structure donc nous nous contenterons d'un simple sizeof(struct sockaddr_in);.

Exemple :
Listing 2.5.1 : SOCKET CreateSocket(void)
(
ouvrir dans une nouvelle fenêtre)

 1  SOCKET CreateSocket(void)
 2  {
 3       SOCKET sock;                     /* déclaration de notre socket */
 4       struct sockaddr_in sin;                 /* déclaration de la structure sockaddr_in */
 5      
 6       memset(&sin, 0x0, sizeof(struct sockaddr_in));
 7       sin.sin_addr.s_addr    = htonl(inaddr_any);      /* définis l'adresse du server */
 8       sin.sin_family     = AF_INET;         /* famille du socket */
 9       sin.sin_port        = htons(1337); /* port sur lequel va etre assigner le server */
 10      
 11       sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); /* appel de la fonction socket */
 12       
 13       if(sock != INVALID_SOCKET)    fprintf(stdout, "[*] Socket cree avec succes.\n");
 14       else {
 15           fprintf(stderr, "Erreur lors de la creation du socket, code d'erreur : %d\n", WSAGetLastError());
 16           return(-1);
 17       }
 18            
 19       if(bind(sock, (sockaddr *)&sin, sizeof(struct sockaddr_in)) != SOCKET_ERROR) /* associe l'adresse local au socket */
 20           fprintf(stdout, "[*] Adresse local associee au socket avec succes.\n");
 21      else {
 22           fprintf(stderr, "Erreur lors de l'association de l'adresse local au socket, code d'erreur : %d\n", WSAGetLastError());
 23           return(-1);
 24      }
 25      
 26      return(sock); /* retourne le socket */
 27  }


Si vous êtes asser observateur vous avez pu remarquer que j'utilise une fonction nommée htons(); pour passer le port sur lequel va écouter le serveur à ma structure "sin". Pourquoi est-ce que j'ai utilisé cette fonction me direz vous? En fait, c'est un problème auquel sont confronté tous les ordinateurs communiquants entre eux sur le réseau : L'ordre des octets. Pour stocker en mémoire une valeur tenant sur 2 octets, les processeurs le font différemment selon leur architecture. Certains placent en première position l'octet de poids faible pour mettre en seconde position l'octet de poids fort , cette organisation est nommée "little endian". D'autres processeurs font autrement et rangent d'abord l'octet de poids fort, suivi de celui de poids faible, ceux ci sont qualifiés de "big endian".

Stockage en mémoire de la valeur : 0x1425

   big endian                    	 little endian
+--------------+ +-------------+
| 0x14, | 0x25 + | 0x25 | 0x14 |
+--------------+ +-------------+

La librairie Winsock met à notre disposition quatres fonctions permettant de transformer un entier long ou court depuis l'ordre des octets de l'hôte vers celui du réseau, et inversement. Toutes ces routines sont encore une fois déclarées dans winsock.h :

Prototypes :

u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);

Je ne vais pas les décrire une par une, mais je vous conseille d'aller jeter un oeil dans ce que tout bon programmeur Windows se doit de posséder : "Win 32 Programmer's Reference".
Toutes ces fonctions y sont très bien détaillées.




2.6  Ouverture du service et attente d'un client

Notre socket est créé, maintenant nous allons le mettre en écoute et attendre la connexion d'un client. Pour réaliser ceci nous allons utiliser la fonction listen(), cette étape effectuée nous attendrons la connexion d'un client sur notre server. Une fois cette condition remplie, nous nous servirons de la fonction accept() qui permettra de gérer la demande du client.
Donc si nous récapitulons dans un ordre logique cela donne :

  • Passer le socket en mode écoute ( listen() )

  • Attendre une connexion cliente et la gérer ( accept() )




int listen(SOCKET s, int backlog);


Bon je pense qu'ici, je n'aurais pas trop besoin de détailler, en ce qui concerne le second argument c'est ce que l'on appel le backlog, cela correspond en fait au nombre maximum de connexions qui seront ecoutées en même temps par le serveur. Passons à l'étude de la fonction accept();.

socket accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);


Cette fonction va nous permettre de traiter la ou les connexions clientes. Pour étudier celle-ci je passerais directement au second paramètre qui est en fait l'adresse d'une structure de type sockaddr dans laquelle la fonction se chargera de stocker les informations sur le client en cas de connexion, le dernier paramètre est la taille de cette structure que nous définirons par une simple sizeof(struct sockaddr);.

Maintenant passons au code.

Exemple :
Listing 2.6.1 : int Server(SOCKET server_socket)
(
ouvrir dans une nouvelle fenêtre)

 1  int Server(SOCKET server_socket)
 2  {
 3       SOCKET client_socket;
 4       struct sockaddr_in adresse;
 5      int adresse_size = 0;
 6      
 7      memset(&adresse, 0, sizeof(struct sockaddr_in));
 8       
 9       /* passe en mode écoute le serveur */
 10       if(listen(server_socket, 0) == 0)    fprintf(stdout, "[*] Serveur passe en mode ecoute avec succes.\n");
 11       else {
 12           fprintf(stderr, "Erreur lors du passage en mode ecoute du serveur, code d'erreur : %d\n", WSAGetLastError());
 13           return(-1);
 14       }    
 15       
 16       while(1) {
 17          adresse_size = sizeof(adresse);
 18          /* accept les connexions clientes */
 19          client_socket = accept(server_socket, (struct sockaddr *)&adresse, &adresse_size);
 20          if(client_socket != INVALID_SOCKET) fprintf(stdout, "[*] Client accepte avec succes\n");
 21           else {
 22               fprintf(stderr, "erreur lors de l'acceptation du client, code d'erreur : %d\n", WSAGetLastError());
 23               return(-1);
 24          }         
 25      }
 26           
 27      return(0);
 28  }






2.7  Gestion de la connexion cliente

Maintenant que notre client est connecté et que le serveur l'a accepté, il va falloir réaliser une action pour rendre ce serveur un peu plus attrayant aux yeux de notre cher client. Pour ceci, le but est d'attendre que le client nous envoit des données pour qu'ensuite le serveur réachemine ces mêmes données à ce même client et ceci sera réalisé à chaque données envoyées par le client (écho).
Comme précédemment nous allons récapituler dans un ordre précis toutes ces actions :

+--- Attendre que le client envois des données ( recv() )
| | -> Données reçues
| - Renvoyer les mêmes données à ce client ( send() )
| |
+----+



int recv(SOCKET s, char FAR * buf, int len, int flags);


Cette fonction va permettre de recevoir des données par l'intermédiaire du socket passé en argument. Ceci est très utile quand on veut communiquer avec le client, souvent même indispensable. Maintenant passons en revu les arguments, le premier argument est le socket sur lequel nous voulons lire les données reçues, le second est un buffer de type char * dans lequel seront stockées les données reçues, le troisième argument est la longueur du buffer précédemment utilisé, et pour finir le quatrième argument est un peu obsolète, nous le laisserons de coté pour lui attribuer la valeur 0.

int send(SOCKET s, const char FAR * buf, int len, int flags);


La fonction send() est indispensable, celle-ci va nous permettre d'envoyer des données soit en tant que client au serveur ou inversement en tant que serveur vers le client. Les arguments sont les mêmes que pour la fonction recv(), sauf pour le buffer, celui-ci au lieu de se voir attribuer les données reçu devra contenir ce que vous voulez envoyer.

Note : Ces deux fonctions renvoient, en cas de succès, le nombre d'octets envoyés pour la fonction send() ou reçus pour la fonction recv().

Maintenant que nous avons pris conscience de l'utilité de ces fonctions, passons à l'application ; le code.

exemple :
Listing 2.7.1 : void traite_connexion(SOCKET socket_client)
(
ouvrir dans une nouvelle fenêtre)

 1  void traite_connexion(SOCKET socket_client)
 2  {
 3       int ret;
 4       char *buffer = NULL;
 5           
 6       buffer = (char *)malloc(1024 * sizeof(char));              /* allocation de mémoire pour le buffer qui va
 7                                    recevoir les données */
 8       if(buffer == NULL) {
 9           fprintf(stderr, "Erreur d'allocation memoire.\n");
 10           exit(-1);
 11       }
 12                
 13       while(1) {
 14          ret = recv(socket_client, buffer, 1024, 0);        /* reception et stockage des données dans buffer */
 15           if(ret != SOCKET_ERROR)    fprintf(stdout, "[*] Donnees recu.\n");
 16           else {
 17               fprintf(stderr, "Erreur lors de la reception des donnees, code d'erreur : %d\n", WSAGetLastError());
 18              break;
 19          }
 20  
 21          buffer[ret] = '\0';                    /* ajout du caractère de fin pour le strlen() qui va suivre */
 22                        
 23           ret = send(socket_client, buffer, strlen(buffer), 0);    /* envois du contenu de buffer au client */
 24           if(ret != SOCKET_ERROR)    fprintf(stdout, "[*] Donnees envoyer.\n");
 25           else {
 26               fprintf(stderr, "Erreur lors de l'envois des donnees, code d'erreur : %d\n", WSAGetLastError());
 27              break;
 28          }
 29      }    
 30      
 31      closesocket(socket_client);                    /* ferme le socket gerant la connexion cliente */
 32       free(buffer);                             /* libère la mémoire allouée pour notre buffer */
 33  }


Voila c'est terminé en ce qui concerne ce serveur "écho", j'espère que cela à pu vous éclaircir les idées sur le concept de serveur et encore mieux sur sa programmation grâce à la bibliothéque Winsock.
Vous pourrez trouver le code source complet de ce serveur ici : echoserver.c (ce fichier se trouve dans l'archive de cet article)




2.8  Programmation d'un client TCP

Après vous avoir montré comment programmer un serveur grâce à Winsock, je vais ici vous apprendre à créer un client TCP vraiment basique qui permettra de dialoguer avec le précédent serveur. Etudions la structure d'un client de ce type.

               +---------------------------+
| Initialisation de Winsock |
+---------------------------+
|
|
+---------------------------------+
| Création du socket ( socket() ) |
+---------------------------------+
|
|
+--------------------------+
| Connexion ( connect() ) |
+--------------------------+
|
|
+--------------------------------------------------------------+
| Gestion de diverses manières la connexion ( recv(), send() ) |
+--------------------------------------------------------------+

Pour réaliser ce modèle vous devrez prendre connaissance d'une seule nouvelle fonction, car vous connaissez déjà la plupart d'entres-elles nécessaires au client grâce au travail effectué avec le serveur. Cette fonction est la fonction connect();. Etudions donc celle-ci.

Prototype :

int connect(SOCKET s, const struct sockaddr FAR * name, int namelen);


Cette fonction va nous permettre d'établir un lien entre nous et le serveur, une connexion plus précisément. Ce qui par la suite, nous permettra de dialoguer avec l'application serveur. Elle prend comme premier paramètre le socket avec lequel vous voulez établir la connexion, le second paramètre est l'adresse d'une structure de type sockaddr, et enfin le dernier paramètre représente la taille de cette structure.

Exemple :
Listing 2.8.1 : connect()
(
ouvrir dans une nouvelle fenêtre)

 1  SOCKET sock;
 2  struct sockaddr_in sin;
 3  
 4  
 5  sock = socket(AF_INET, SOCK_STREAM, 0);
 6  
 7  memset(&sin, 0x0, sizeof(struct sockaddr_in));
 8  sin.sin_addr.s_addr    = inet_addr("127.0.0.1");
 9  sin.sin_family        = AF_INET;
 10  sin.sin_port        = htons(1337);
 11  
 12  connect(sock, (sockaddr *)&sin, sizeof(struct sockaddr_in));


Je n'incluerais pas le code du client dans cette section, cela ne ferais que l'alourdir... Il est disponible ici : TCPclient.c (ce fichier se trouve dans l'archive de cet article)




2.9  Conclusion

Cette introduction aux sockets streams est terminée, maintenant passons à quelque chose de plus interressant : Les raw sockets.





<<  1  Winsock 2 et architectureSommaire3  Les raw sockets  >>

 Accés rapide

1  Winsock 2 et architecture
2  Les Sockets Streams

Qu'est ce qu'un socket ?

Programmation d'un serveur TCP

Les fichiers nécessaires

Initialisation de l'API Winsock 1.1

Création du socket

Ouverture du service et attente d'un client

Gestion de la connexion cliente

Programmation d'un client TCP

Conclusion

3  Les raw sockets
4  Les différents protocoles
5  Forger ses propres paquets
6  Programmer un sniffer grâce aux raw sockets
7  Références
8  Remerciements
Voir le sommaire complet

 Liens utiles

  • Publier un article
  • Envoyer cette page
  • Ecrire à l'auteur

  •  Mini-Chat

    Thienou (00h11): salut
    Thienou (00h13): Oula mon inscription date de 11 ans je me sent vieux :)
    neowolf25 (17h59): MMF2 en "pay what you want" jusqu'à demain sur
    neowolf25 (17h59): https://www.hu
    mblebundle.com/
    weekly

    Miuka (21h15): Coin coin de 2014
    Miuka (21h15): Des gens qui ont migré sur le forum Clickteam ou ailleurs ?
    Strike (09h45): Salut les vieux !
    Hikarion (12h46): Salut les djeunz
    Hikarion (13h38): A qui profite le scandale ?
    Hikarion (13h44): le chat irc est toujours actif ?

    Votre message



     Archives

     Dev4all Newsletter

    Restez à jour avec la newsletter mensuelle !

    Votre e-mail


    1800 abonnés

     Recommander Dev4all

    Recommandez Dev4all à un ami. Cela fera grandir notre communauté !

    E-mails de vos amis




    [ Accueil | S'inscrire | Mon Dev4all | Communauté | Téléchargements | Articles | Forums | Chat ]

    [ A propos de Dev4all | Aide | La charte Dev4all | Contact ]

    © 2000-2018 MTProd. Tous droits réservés.
    L'utilisation de Dev4all implique l'acceptation et le respect de la charte Dev4all.