TP Sockets TCP Windows
TP Sockets TCP Windows
TP Sockets TCP Windows
Sommaire
L’interface socket 2
Pré-requis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Manuel du pogrammeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Modèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Couche Transport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Numéro de ports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Caractéristiques des sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Manipulations 5
Objectifs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Étape n°0 : préparation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Étape n°1 : création de la socket (côté client) . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Étape n°2 : connexion au serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Étape n°3 : vérification du bon fonctionnement de la connexion . . . . . . . . . . . . . . . . . 10
Étape n°4 : échange des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Étape n°5 : réalisation d’un serveur TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Étape n°6 : mise en attente des connexions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Étape n°7 : accepter les demandes connexions . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Bilan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Questions de révision 24
1
L’INTERFACE SOCKET
L’interface socket
Pré-requis
La mise en oeuvre de l’interface socket nécessite de connaître :
– L’architecture client/serveur
– L’adressage IP et les numéros de port
– Notions d’API (appels systèmes sous Unix) et de programmation en langage C
– Les protocoles TCP et UDP, les modes connecté et non connecté
Définition
« La notion de socket a été introduite dans les distributions de Berkeley (un fameux
système de type UNIX, dont beaucoup de distributions actuelles utilisent des morceaux
de code), c’est la raison pour laquelle on parle parfois de sockets BSD (Berkeley Software
Distribution).
Il s’agit d’un modèle permettant la communication inter processus (IPC - Inter Process Communication)
afin de permettre à divers processus de communiquer aussi bien sur une même machine qu’à travers un
réseau TCP/IP. » [Wikipedia]
Manuel du pogrammeur
Le développeur utilisera donc concrètement une interface pour programmer une application TCP/IP
grâce par exemple :
– à l’API Socket BSD sous Unix/Linux ou
– à l’API WinSocket sous Microsoft ©Windows
Les pages man principales sous Unix/Linux concernant la programmation réseau sont regroupées dans le
chapitre 7 :
– socket(7) : interface de programmation des sockets
– packet(7) : interface par paquet au niveau périphérique
– raw(7) : sockets brutes (raw) IPv4 sous Linux
– ip(7) : implémentation Linux du protocole IPv4
– udp(7) : protocole UDP pour IPv4
– tcp(7) : protocole TCP
L'accès aux pages man se fera donc avec la commande man, par exemple : man 7 socket
Modèle
Rappel : une socket est un point de communication par lequel un processus peut émettre et recevoir des
données.
Ce point de communication devra être relié à une adresse IP et un numéro de port dans le cas des
protocoles Internet.
Une socket est communément représentée comme un point d’entrée initial au niveau TRANSPORT du
modèle à couches DoD dans la pile de protocole.
Couche Transport
Rappel : la couche Transport est responsable du transport des messages complets de bout en bout (soit
de processus à processus) au travers du réseau.
En programmation, si on utilise comme point d’entrée initial le niveau TRANSPORT, il faudra alors
choisir un des deux protocoles de cette couche :
– TCP (Transmission Control Protocol) est un protocole de transport fiable, en mode connecté (RFC
793).
– UDP (User Datagram Protocol) est un protocole souvent décrit comme étant non-fiable, en mode
non-connecté (RFC 768), mais plus rapide que TCP.
Numéro de ports
Rappel : un numéro de port sert à identifier un processus (l’application) en cours de communication
par l’intermédiaire de son protocole de couche application (associé au service utilisé, exemple : 80 pour
HTTP).
Pour chaque port, un numéro lui est attribué (codé sur 16 bits), ce qui implique qu'il existe un maximum
de 65 536 ports (216 ) par machine et par protocoles TCP et UDP.
L’attribution des ports est faite par le système d’exploitation, sur demande d’une application. Ici, il faut
distinguer les deux situations suivantes :
– cas d’un processus client : le numéro de port utilisé par le client sera envoyé au processus serveur.
Dans ce cas, le processus client peut demander à ce que le système d’exploitation lui attribue n’importe
quel port, à condition qu’il ne soit pas déjà attribué.
– cas d’un processus serveur : le numéro de port utilisé par le serveur doit être connu du processus client.
Dans ce cas, le processus serveur doit demander un numéro de port précis au système d’exploitation
qui vérifiera seulement si ce numéro n’est pas déjà attribué.
Une liste des ports dits réservés est disponible dans le chier /etc/services sous Unix/Linux.
Une socket appartient à une famille. Il existe plusieurs types de sockets. Chaque famille possède son
adressage.
Manipulations
Objectifs
L’objectif de cette partie est la mise en oeuvre d’une communication client/serveur en utilisant une socket
TCP sous Windows.
Winsock (WINdows SOCKet ) est une bibliothèque logicielle pour Windows dont le but est d'implémenter
une interface de programmation inspirée des sockets BSD. Son développement date de 1991 (Microsoft
n'a pas implémenté Winsock 1.0.). Il y a peu de diérences avec les sockets BSD, mais Winsock fournit des
fonctions additionnelles pour être conforme au modèle de programmation Windows, par exemple les fonctions
WSAGetLastError(), WSAStartup() et WSACleanup().
Pour ce TP, vous aurez besoin d’outils de compilation et fabrication de programmes sous Windows. MinGW
(Minimalist GNU for Windows) est une adaptation des logiciels de développement et de compilation
du GNU (GCC : GNU Compiler Collection) à la plate-forme Windows (Win32). Vous pouvez l’installer
indépendamment ou intégrer à un EDI comme Dev-Cpp ou Code::Blocks.
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
/* ... */
return 0;
}
Un client TCP en C (itération 0)
Dans cet exemple, on utilise la version 2 de Winsock (winsock2.h et ws2_32.lib). Vous pouvez aussi
utiliser la version 1 (winsock.h et wsock32.lib). Avec les compilateurs type GCC, il faudra ajouter
-lws2_32 à l'édition des liens.
...
Extrait de la documentation de l’appel socket
À l’aide d’un éditeur de texte (notepad++ ou d’un EDI comme Dev-Cpp ou Code::Blocks sous Windows),
tapez (à la main, pas de copier/coller, histoire de bien mémoriser !) le programme suivant dans un fichier
que vous nommerez "clientTCPWin-1.c" :
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
if (descripteurSocket == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
iResult = closesocket(descripteurSocket);
if (iResult == SOCKET_ERROR)
{
printf("Erreur fermeture socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
return 0;
}
Un client TCP en C (itération 1)
Pour le paramètre protocol, on a utilisé la valeur 0 (voir commentaire). On aurait pu préciser le protocole
TCP de la manière suivante : IPPROTO_TCP.
int connect(
_In_ SOCKET s,
_In_ const struct sockaddr *name,
_In_ int namelen
);
...
Extrait de la page de documentation de l’appel connect
On rappelle que l’adressage du processus distant dépend du domaine de communication (cad la famille
de protocole employée). Ici, nous avons choisi le domaine AF_INET pour les protocoles Internet IPv4.
Dans cette famille, un processus sera identifié par :
– une adresse IPv4
– un numéro de port
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
Il suffit donc d’initialiser une structure sockaddr_in avec les informations distantes du serveur (adresse
IPv4 et numéro de port).
Pour écrire ces informations dans la structure d’adresse, il nous faudra utiliser :
– inet_addr() pour convertir une adresse IP depuis la notation IPv4 décimale pointée vers une forme
binaire (dans l’ordre d’octet du réseau)
– htons() pour convertir le numéro de port (sur 16 bits) depuis l’ordre des octets de l’hôte vers celui
du réseau.
L'ordre des octets du réseau est en fait big-endian. Il est donc plus prudent d'appeler des fonctions qui
respectent cet ordre pour coder des informations dans les en-têtes des protocoles réseaux.
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
SOCKET descripteurSocket;
int iResult;
if (descripteurSocket == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
{
printf("Erreur fermeture socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
return 0;
}
Un client TCP en C (itération 2)
Ceci peut s’expliquer tout simplement parce qu’il n’y a pas de processus serveur à cette adresse !
La fonction WSAGetLastError() retourne seulement un code d'erreur. L'ensemble des code d'erreurs
sont déclarés dans le chier d'en-tête winsock2.h. Par exemple ici, le code d'erreur 10061 correspond à
l'étiquette WSAECONNREFUSED (connexion refusée). Pour obtenir le message associé à un code d'erreur, il faut
utiliser la fonction FormatMessageW().
wchar_t *s = NULL;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL,
SUBLANG_DEFAULT), (LPWSTR)&s, 0, NULL);
fprintf(stderr, "%S\n", s);
LocalFree(s);
Affichage du message associé à un code d’erreur
Dans l'architecture client/serveur, on rappelle que c'est le client qui a l'initiative de l'échange. Il faut donc
que le serveur soit en écoute avant que le client fasse sa demande.
Normalement les octets envoyés ou reçus respectent un protocole de couche APPLICATION. Ici, pour
les tests, notre couche APPLICATION sera vide ! C'est-à-dire que les données envoyées et reçues ne
respecteront aucun protocole et ce seront de simples caractères ASCII.
Les appels recv() et send() sont spéciques aux sockets en mode connecté (TCP).
Faire communiquer deux processus sans aucun protocole de couche APPLICATION est tout de même
difficile ! On va simplement fixer les règles d’échange suivantes :
– le client envoie en premier une chaîne de caractères
– et le serveur lui répondra "ok"
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
SOCKET descripteurSocket;
int iResult;
if (descripteurSocket == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
if (iResult == SOCKET_ERROR)
{
printf("Erreur fermeture socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
return 0;
}
Un client TCP en C (itération 3)
On utilise la même procédure de test que précédemment en démarrant un serveur netcat sur le port
5000 :
nc -l -p 5000
Dans netcat, pour envoyer des données au client, il sut de saisir son message et de valider par la touche
Entrée.
Que se passe-t-il si le serveur s’arrête (en tapant Ctrl-C par exemple !) au lieu d’envoyer
"ok" ?
Hello world !
^C
int shutdown(
_In_ SOCKET s,
_In_ int how
);
...
Extrait de la documentation de l’appel shutdown
int bind(
_In_ SOCKET s,
_In_ const struct sockaddr *name,
_In_ int namelen
);
...
Extrait de la documentation de l’appel bind
On rappelle que l’adressage d’un processus (local ou distant) dépend du domaine de communication
(cad la famille de protocole employée). Ici, nous avons choisi le domaine AF_INET pour les protocoles
Internet IPv4.
Dans cette famille, un processus sera identifié par :
– une adresse IPv4
– un numéro de port
Rappel : l’interface socket propose une structure d’adresse générique sockaddr et le domaine AF_INET
utilise une structure compatible sockaddr_in.
Il suffit donc d’initialiser une structure sockaddr_in avec les informations locales du serveur (adresse
IPv4 et numéro de port).
Pour écrire ces informations dans la structure d’adresse, il nous faudra utiliser :
– htonl() pour convertir une adresse IP (sur 32 bits) depuis l’ordre des octets de l’hôte vers celui du
réseau
– htons() pour convertir le numéro de port (sur 16 bits) depuis l’ordre des octets de l’hôte vers celui du
réseau.
Normalement il faudrait indiquer l'adresse IPv4 de l'interface locale du serveur qui acceptera les demandes
de connexions. Il est ici possible de préciser avec INADDR_ANY que toutes les interfaces locales du serveur
accepteront les demandes de connexion des clients.
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
SOCKET socketEcoute;
int iResult;
if (socketEcoute == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
return 0;
}
Un serveur TCP en C (itération 1)
Attention, tout de même de bien comprendre qu’un numéro de port identifie un processus communiquant !
Exécutons deux fois le même serveur et on obtient alors :
bind: Address already in use
Explication : l'attachement local au numéro de port 5000 du deuxième processus échoue car ce numéro
de port est déjà attribué par le système d'exploitation au premier processus serveur.
The listen function places a socket in a state in which it is listening for an incoming
connection.
int listen(
_In_ SOCKET s,
_In_ int backlog
);
Si la le est pleine, le serveur sera dans une situation de DOS (Deny Of Service ) car il ne peut plus traiter
les nouvelles demandes de connexion.
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
SOCKET socketEcoute;
int iResult;
if (socketEcoute == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
return 0;
}
Un serveur TCP en C (itération 2)
SOCKET accept(
_In_ SOCKET s,
_Out_ struct sockaddr *addr,
_Inout_ int *addrlen
);
s [in] : A descriptor that identifies a socket that has been placed in a listening state
with the listen function. The connection is actually made with the socket that is
returned by accept.
addr [out] : An optional pointer to a buffer that receives the address of the connecting
entity, as known to the communications layer. The exact format of the addr parameter is
determined by the address family that was established when the socket from the sockaddr
structure was created.
addrlen [in, out] : An optional pointer to an integer that contains the length of structure
pointed to by the addr parameter.
...
Extrait de la documentation de l’appel accept
Explication :imaginons qu'un client se connecte à notre socket d'écoute. L'appel accept() va retourner
une nouvelle socket connectée au client qui servira de socket de dialogue. La socket d'écoute reste
inchangée et peut donc servir à accepter des nouvelles connexions.
Le principe est simple mais un problème apparaît pour le serveur : comment dialoguer avec le client
connecté et continuer à attendre des nouvelles connexions ? Il y a plusieurs solutions à ce problème
notamment la programmation multi-tâche car ici le serveur a besoin de paralléliser plusieurs traitements.
On va pour l’instant ignorer ce problème et mettre en oeuvre un serveur basique : c’est-à-dire mono-client
(ou plus exactement un client après l’autre) !
int main()
{
WSADATA WSAData; // variable initialisée par WSAStartup
SOCKET socketEcoute;
int iResult;
if (socketEcoute == INVALID_SOCKET)
{
printf("Erreur creation socket : %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// On fixe la taille de la file d’attente (pour les demandes de connexion non encore
traitées)
if (listen(socketEcoute, SOMAXCONN) == SOCKET_ERROR)
{
printf("Erreur listen socket : %d\n", WSAGetLastError());
}
while(1)
{
memset(messageEnvoi, 0x00, LG_MESSAGE*sizeof(char));
memset(messageRecu, 0x00, LG_MESSAGE*sizeof(char));
printf("Attente d’une demande de connexion (quitter avec Ctrl-C)\n\n");
// c’est un appel bloquant
socketDialogue = accept(socketEcoute, (SOCKADDR *)&pointDeRencontreDistant, &
longueurAdresse);
if (socketDialogue == INVALID_SOCKET)
{
printf("Erreur accept socket : %d\n", WSAGetLastError());
closesocket(socketEcoute);
WSACleanup();
return 1;
}
return 1;
}
return 0;
}
Un serveur TCP en C (itération 3)
Message ok
envoyé (3 octets)
Message ok
envoyé (3 octets)
^C
Il est évidemment possible de tester notre serveur avec des clients TCP existants comme telnet ou
netcat.
Bilan
L’échange entre un client et un serveur TCP peut être maintenant schématisé de la manière suivante :
Questions de révision
L’idée de base des questions de révision est de vous donner une chance de voir si vous avez identifié et
compris les points clés de ce TP.
Question 3. Quelles sont les deux informations qui définissent un point de communication en IPv4 ?
Question 6. À quelle couche du modèle DoD est reliée l’interface de programmation socket ?
Question 7. Quel protocole de niveau Transport permet d’établir une communication en mode connecté ?
Question 8. Quel protocole de niveau Transport permet d’établir une communication en mode non-
connecté ?
Question 10. À quels protocoles correspond le domaine PF_INET ou AF_INET ? Est-ce le seul utilisable
avec l’interface socket ? En citer au moins un autre.