I. Introduction

Beaucoup de monde parle des sockets en C/C++ comme étant quelque chose de vraiment difficile. Je dois avouer que le manque de documentation sur l'Internet cause cette réaction. J'ai beaucoup cherché sur le sujet et mes résultats ont été plutôt décevants. J'ai trouvé quelques documents que je me permets de citer, comme référence ou bien pour complémenter celle-ci. J'ai aussi fait le choix de l'écrire en français parce qu'il n'y a aucune documentation sur les sockets en français. Enfin, très peu.

Cette documentation n'est pas complète. Il manque beaucoup de théorie sur la réseautique, les couches TCP/IP, les "host byte order", les sockets DGRAM et beaucoup d'autres choses. Avec ce tutoriel d'introduction, vous devriez être capables de faire des sockets en utilisant le C ou le C++, mais pas de les maîtriser.

II. Qu'est-ce qu'un socket ?

Pour communiquer entre deux applications ou ordinateurs, il vous faut un téléphone, un socket. Un socket est attaché à un port (une porte au sens imagé). Vous ouvrez votre porte et attendez qu'un colis soit acheminé jusqu'à celle-ci. Une fois le colis reçu, vous pouvez soit envoyer un autre colis ou bien fermer la porte, le port. Vous devez initialiser le socket, faire un lien avec le port, attendre pour un paquet, et fermer le socket. Pour avoir plus d'informations sur les protocoles de transfert TCP/IP ou bien sur les sockets en général, voir la section "Références".

III. Utilisation du socket

Créer un socket en C/C++ est relativement simple, contrairement à ce que la majorité du monde pense. La création d'un socket est faite par une chaîne de commandes. Pour commencer, il faut initialiser WSAStartup() et à la fin du programme, appeler WSACleanup(). Un groupe de variables sera aussi nécessaire pour faire cela. Bon, assez de bavardages et passons au code lui-même.

 
Sélectionnez
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
    WSADATA WSAData;
    WSAStartup(MAKEWORD(2,0), &WSAData);
    /* ... */
    WSACleanup();
    return 0;
}

winsock2.h : la librairie dont vous vous servirez avec les sockets. Certaines personnes choisissent la librairie de la version 1, winsock.h. Dépendant de vos besoins. Si vous choisissez la version 1, prenez bien soin de choisir la librairie "wsock32.lib" au lieu de "ws2_32.lib".

WSADATA WSAData : l'initialisation d'une variable WSADATA. La variable va être utilisée pour le démarrage de WSAStartup(). Elle n'aura pas d'autre utilisation, généralement.

WSAStartup(MAKEWORD(2,0), &WSAData) : ce qui dit à votre ordinateur que vous allez utiliser des sockets. Il y a deux paramètres à se rappeler, MAKEWORD(2,0) la version de winsock que vous désirez utiliser. Ça peut varier, dépendant des #include que vous aurez choisis. Si vous avez choisi d'utiliser winsock 1 (winsock.h), remplacez-le par MAKEWORD(1,0). Le deuxième paramètre à se rappeler, &WSAData, vraiment simple. Vous y mettez la variable de type WSADATA que vous avez définie plus haut. À la fin de votre programme, il faut nettoyer votre WSA en faisant WSACleanup(); Attention, ne pas faire de WSACleanup() tant que vous n'aurez pas terminé avec les sockets, sinon vous devrez initialiser WSAStartup() une deuxième fois.

Votre winsock est maintenant initialisé. Il suffit alors de créer le socket lui-même.

 
Sélectionnez
SOCKET sock;
SOCKADDR_IN sin;
sin.sin_addr.s_addr	= inet_addr("127.0.0.1");
sin.sin_family		= AF_INET;
sin.sin_port		= htons(4148);
sock = socket(AF_INET,SOCK_STREAM,0);
bind(sock, (SOCKADDR *)&sin, sizeof(sin));

Et voilà. Votre socket est initialisé. Notez par contre que celui-ci est initialisé pour vous connecter sur vous-même au port 4148. L'utilité est donc nulle, sauf si vous avez un serveur à ce port-ci. Voyez la section "Exemples de codes" pour avoir des exemples plus pertinents.

SOCKET sock : initialisez une variable de type SOCKET qui sera utilisée pour définir le socket.

SOCKADDR_IN sin : la struct du SOCKADDR contient les informations techniques du socket.

    Par exemple :

    • .sin_addr.s_addr : qui définit l'adresse du serveur. Si vous codez un serveur, vous n'avez pas à définir d'adresse, vous utiliseriez donc : sin.sin_addr.s_addr = htonl(INADDR_ANY);

    • .sin_family : la "famille" du socket, le type si on veut. Pour l'Internet, les programmeurs utilisent généralement AF_INET.

    • .sin_port : le port sur lequel vous voulez vous connecter ou bien écouter. htons(4148) pourrait être remplacé par htons(23) si vous voulez le port telnet, etc. Pour davantage d'explications sur les fonctions htons, htonl, noths et nothl, je vous suggère de voir "The beej guide" référencé dans la section "Références".

socket(AF_INET, SOCK_STREAM, 0) : la création du socket en tant que tel. Le 1er paramètre est la famille du socket comme vous l'avez configuré auparavant dans la structure du SOCKADDR_IN, AF_INET dans ce cas-ci. Le 2ème paramètre, SOCK_STREAM, c'est le type du socket. Il existe aussi SOCK_DGRAM, dont je parlerai plus loin dans le texte. Les SOCK_STREAM ouvrent une connexion directe entre les 2 ordinateurs et permettent ensuite d'envoyer les paquets que vous désirez, tandis que le SOCK_DGRAM envoie un paquet directement à la destination sans faire d'accept() ou de connect().

bind(sock, (SOCKADDR *)&sin, sizeof(sin)); : la commande qui va attacher votre socket directement au port et à l'adresse que vous avez définis dans la struct SOCKADDR_IN. Il y a trois paramètres à retenir. Le premier est sock, le socket que vous avez initialisé plut tôt. Le deuxième est la structure SOCKADDR_IN, celle que vous avez définie plus haut. Le troisième et dernier paramètre est la taille de cette structure : sizeof(sin).

Maintenant votre chemin se divise en deux choix, le serveur ou le client. Si vous voulez faire un serveur, vous devrez faire une boucle qui accept() les connexions. Dans ce cas, c'est un petit peu plus compliqué.

 
Sélectionnez
listen(sock, 0);
int val = 0;
while(1)
{
    int sizeof_csin = sizeof(csin);
    val = accept(sock, (SOCKADDR *)&csin, &sizeof_csin)
    if(val != INVALID_SOCKET)
    {
        // Fonctions à éxécuter sur le socket.
    }
}

OK, procédons étape par étape :

    • listen(sock, 0) : listen() va écouter le port sur le socket. Le 1er paramètre, sock, est le socket sur lequel le listen() écoutera. Le 2ème paramètre s'appelle le BACKLOG. C'est le nombre maximum de connexions qui seront écoutées en même temps.

    • La variable int val sera utilisée pour prendre la valeur de retour du accept().

    • accept(sock, (SOCKADDR *)&csin, &sizeof_csin) : la fonction qui va nous permettre d'accepter une connexion, une autre fonction très simple. 1er paramètre : le socket. Le 2ème paramètre est votre SOCKADDR_IN que vous avez créé pour prendre les informations du client connecté sur votre serveur. Le 3ème et dernier paramètre est un pointeur vers la variable qui va recevoir le nombre d'octets écrits dans votre SOCKADDRIN. La variable doit être initialisée avec la taille de cette structure.

J'ai rajouté un if() pour vérifier si le socket est accepté, si quelqu'un est connecté, et si il l'est, il va effectuer les fonctions que vous auriez mises au préalable dans le if(), par exemple un send().

Maintenant, passons au deuxième choix, le client. Rien de plus simple. Il n'y a qu'à faire un connect().

 
Sélectionnez
connect(sock, (SOCKADDR *)&sin, sizeof(sin))

    • connect : relativement semblable à accept(), seulement dans le &sin, vous mettez le SOCKADDR_IN qui décrit le serveur et la taille de la structure n'est pas passée par un pointeur (puisqu'il n'y a pas de retour).

Bravo, maintenant vous êtes connectés. Mais qu'est-ce que je vais faire à mon socket, maintenant que je suis connecté ?! Nous y arrivons.

IV. Commandes reliées au sockets

Les commandes reliées au socketing les plus utilisées sont send(), recv(), sendto(), recvfrom(), closesocket(), shutdown(), getpeername() et gethostname().

send() : commande pour envoyer une string au client ou au serveur. Tellement simple d'utilisation. send(socket, message, grosseur, 0); : le 1er paramètre étant le socket, le 2ème étant le message à envoyer. Notez qu'un simple \n sur Internet est \r\n, si vous ne faites pas de \r\n, un serveur/client ne le considérera pas comme un retour de chariot. Le dernier paramètre ne vous sera probablement jamais utile, vous marquerez donc 0. Exemple : send(sock, "Hello world!\r\n", 14, 0);

recv() : une autre commande très simple. En fait elle est presque identique au send(). recv(socket, buffer, grosseur, 0); : les paramètres sont identiques au send() sauf qu'au lieu d'envoyer une string, vous le stockez dans une variable, le buffer. Exemple : recv(sock, buff, sizeof(buff), 0);

sendto() avant de faire du SOCK_DGRAM, je suggère fortement de commencer par le SOCK_STREAM. C'est plus simple pour commencer et de toute façon, c'est ce qui est le plus utilisé. L'art d'envoyer un paquet à un IP particulier, sans avoir à se connecter. Initialisez votre WSAStartup() et vous êtes prêts à l'utiliser. Il est TRÈS important de noter que lors de la création du socket, il faut préciser SOCK_DGRAM et non pas SOCK_STREAM. sendto(socket, message, longueur, 0, sin, sizeof(sin)); : le premier paramètre, le socket lui-même. Ensuite, vous tapez votre message, la chaîne de caractères à envoyer, qui peut aussi bien être un buffer stocké dans une variable, suivi de la longueur du message. Vous continuerez avec un nouveau SOCKADDR_IN qui aura les informations de la destination que vous aurez programmée à l'avance. Terminez cela avec un sizeof(sin). Remplacez la variable sin par celle du SOCKADDR_IN, bien sûr.

recvfrom() : la fonction recvfrom est presque identique à sendto(). Vous avez besoin d'utiliser SOCK_DGRAM lors de la création du socket(), comme pour le sendto(). recvfrom(socket, message, longueur, 0, sin, &sizeof_sin)); : le sin sera celui du client qui vous aura envoyé un paquet.

closesocket() : l'art de fermer un socket d'une façon facile et propre. Chaque socket créé avec socket() ou accept() doit être fermé avec closesocket(). Pour résumer cette fonction en une ligne, closesocket(socket).

shutdown() : c'est un peu comme un close() sauf que vous avez beaucoup plus de contrôle avec cette fonction... et que ce n'est pas un close(). Par exemple, vous pouvez bloquer le flux reçu, envoyé ou les deux. Deux paramètres à retenir, le 1er, le socket. Et le 2ème, un int de 0 à 2. (0 = les recv() ne seront plus acceptées, 1 = les send() ne seront plus acceptées, 2 = ni les send,() ni les recv() ne seront acceptées)

getpeername() : cette fonction, très utile, sert à savoir qui est connecté sur vous, des infos sur le client. Vous devez créer un SOCKADDR_IN pour stocker les informations, par contre. L'utilisation de cette fonction va comme ceci : getpeername(socket, sin, &sizeof_sin); : le sin étant le nouveau SOCKADDR_IN créé pour les besoins de la cause.

gethostname() : retourne le nom de la machine locale. gethostname(hostname, sizeof(hostname)); : hostname étant un tableau de char destiné à stocker le nom de votre propre machine.

V. Exemples de codes

Voici un client simple pour se connecter à IRC. Notez que les recv() et les send() pour s'enregistrer et pouvoir l'utiliser comme un client ne sont pas incorporées.

 
Sélectionnez
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
    WSADATA WSAData;
    SOCKET sock;
    SOCKADDR_IN sin;
    char buffer[255];
    WSAStartup(MAKEWORD(2,0), &WSAData);
    /* Tout est configuré pour se connecter sur IRC, haarlem, Undernet. */
    sock = socket(AF_INET, SOCK_STREAM, 0);
    sin.sin_addr.s_addr = inet_addr("62.250.14.6");
    sin.sin_family = AF_INET;
    sin.sin_port = htons(6667);
    connect(sock, (SOCKADDR *)&sin, sizeof(sin));
    recv(sock, buffer, sizeof(buffer), 0);
    closesocket(sock);
    WSACleanup();
    return 0;
}

Maintenant, voici un serveur simple qui envoie un "Hello world!" à quiconque se connecte puis le déconnecte immédiatement.

 
Sélectionnez
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
    WSADATA WSAData;
    SOCKET sock;
    SOCKET csock;
    SOCKADDR_IN sin;
    SOCKADDR_IN csin;
    WSAStartup(MAKEWORD(2,0), &WSAData);
    sock = socket(AF_INET, SOCK_STREAM, 0);
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(23);
    bind(sock, (SOCKADDR *)&sin, sizeof(sin));
    listen(sock, 0);
    while(1) /* Boucle infinie. Exercice : améliorez ce code. */
    {
        int sinsize = sizeof(csin);
        if((csock = accept(sock, (SOCKADDR *)&csin, &sinsize)) != INVALID_SOCKET)
        {
            send(csock, "Hello world!\r\n", 14, 0);
            closesocket(csock);
        }
    }
    /* On devrait faire closesocket(sock); puis WSACleanup(); mais puisqu'on a entré une boucle infinie ... */
    return 0;
}

Ces codes sources sont vraiment basiques, mais ils pourront toujours vous aider pour voir la structure d'un code C/C++ utilisant les sockets. Avec ces instruments et quelques autres documents sur les sockets, vous devriez être capables de maîtriser les principes de base et beaucoup plus. Je vous suggère fortement de jeter un œil sur la section "Références" pour plus d'informations sur le sujet en général.

VI. Références

Voici une liste de références dont je me suis servi pour apprendre les sockets et pour vous les expliquer. Certaines sont complètes, d'autres non. Elles sont toutes de bonne qualité, par contre. Malheureusement, elles sont en anglais.

Beej's guide to network programming (http://www.ecst.csuchico.edu/~beej/guide/net/) : un des guides les plus intéressants sur l'Internet. Relativement complet. Toutefois, le tutoriel touche à l'environnement UNIX inclusivement. Il est bien de noter que les sockets en UNIX ne sont pas plus compliqués qu'en Windows, je dirais même plus simples. Quelques changements de librairies, enlevez les WSA* et vous avez des sockets UNIX.

BSD Sockets: A Quick And Dirty Primer (http://www.cis.temple.edu/~ingargio/old/cis307s96/readings/docs/sockets.html) : encore là, nous avons un joli tutoriel, qui explique simplement comment utiliser les sockets, pour BSD, mais encore très utile pour Windows, si vous faites les changements appropriés.

RFC1180: A tutorial to TCP/IP (http://www.faqs.org/rfc/rfc1180.txt) : voici un texte sur le TCP/IP. Ce n'est pas totalement complet, il manque l'historique du TCP/IP et certaines autres choses mais en général, c'est relativement complet. Un tutoriel sur le TCP/IP compacté en 30 pages.

VII. Remerciements

L'équipe de rédaction de Developpez.com remercie The Walrus pour cette article.
Nous remercions également Dut et Melem pour la mise au gabarit du document ainsi que Wachter pour sa relecture orthographique.