Cours programmation réseau en C++

TCP - Premiers pas en tant que serveur

Bousk

Maintenant que nous savons comment réaliser un client TCP, voyons comment réaliser un serveur.

Passons d'abord en revue les nouvelles fonctions spécifiques à un programme serveur avant de créer notre premier serveur.

Commentez

- Titre : Cours programmation réseau en C++

- Auteur : Bousk

- Parution : 29 novembre 2016

- Mise à jour : 9 mai 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Cyrille (Bousk) Bousquet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Fonctions spécifiques au serveur

- I-A. Bind - int bind(SOCKET sckt, const struct addr* name, int namelen);

- I-B. Listen - int listen(SOCKET sckt, int backlog) ;

- I-C. Accept - SOCKET accept(SOCKET sckt, struct sockaddr* addr, int* addrlen);

- I-D. Récupérer l'IP d'un client

- I-D-1. Windows - const char* inet_ntop(int family, void* src, char* dst, size_t size);

- I-D-2. Unix - const char* inet_ntop(int family, const void* src, char* dst, socklen_t size);

- II. Notre premier serveur

- III. Proposition de corrigé

I - Fonctions spécifiques au serveur

I-A - Bind - int bind(SOCKET sckt, const struct addr* name, int namelen);

La fonction bind est utilisée pour assigner une adresse locale à un socket.

- sckt est le socket auquel est assigné l'adresse.

- name est la structure à assigner au socket.

- namelen est la taille de cette structure, généralement un sizeof fera l'affaire.

Retourne SOCKET_ERROR sous Windows et -1 sous Unix en cas d'erreur, 0 sinon.

La structure name contiendra le port public à assigner ainsi que l'information des adresses distantes qui peuvent se connecter à notre socket. Sa création classique, pour accepter toutes les connexions entrantes, sera ainsi :

sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY; // indique que toutes les sources seront acceptées
addr.sin_port = htons(port); // toujours penser à traduire le port en réseau
addr.sin_family = AF_INET; // notre socket est TCP
int res = bind(server, (sockaddr*)&addr, sizeof(addr));
if (res != 0)
// erreur

Si le port demandé est 0, le système assignera lui-même un port valide automatiquement.

Généralement les ports jusqu'à 1024 sont réservés pour le système.

La valeur INADDR_ANY pour s_addr indique au système d'assigner ce socket à toutes les interfaces disponibles sur la machine, et ainsi d'accepter toutes les sources de connexion, locales et distantes. Si vous hésitez sur la valeur à assigner, vous devriez sûrement utiliser celle-ci.

I-B - Listen - int listen(SOCKET sckt, int backlog) ;

Place le socket dans un état lui permettant d'écouter les connexions entrantes.

sckt est le socket auquel les clients vont se connecter.

backlog est le nombre de connexions en attente qui peuvent être gérées. La valeur SOMAXCONN peut être utilisée pour laisser le système choisir une valeur correcte selon sa configuration.

Retourne SOCKET_ERROR sous Windows et -1 sous Unix en cas d'erreur, 0 sinon.

res = listen(server, SOMAXCONN);
if (res != 0)
// erreur

Si vous appelez listen avant bind, le système fera l'équivalent d'un appel à bind avec INADDR_ANY et le port 0, soit le choix d'un port aléatoire disponible.

Notez que c'est exactement ce qui se passe quand vous appelez connect pour un socket client. Mais dans le cadre d'un serveur, on préfère assigner un port en particulier et connu dans la majorité des cas.

I-C - Accept - SOCKET accept(SOCKET sckt, struct sockaddr* addr, int* addrlen);

Accepte une connexion entrante.

- sckt est le socket serveur qui attend les connexions.

- addr recevra l'adresse du socket qui se connecte.

- addrlen est la taille de la structure pointée par addr.

Cette fonction est bloquante en attendant qu'une connexion entrante arrive.

Retourne INVALID_SOCKET sous Windows et -1 sous Unix en cas d'erreur, un socket représentant le nouveau client sinon, la structure addr contient alors les informations du client connecté.

sockaddr_in addr = { 0 };
int len = sizeof(addr);
SOCKET newClient = accept(server, (sockaddr*)&addr, &len);
if (newClient == INVALID_SOCKET)
// erreur

Le socket ainsi récupéré sert d'identifiant pour communiquer, recevoir et envoyer des données, avec le client connecté. On l'appellera le socket client.

I-D - Récupérer l'IP d'un client

I-D-1 - Windows - const char* inet_ntop(int family, void* src, char* dst, size_t size);

I-D-2 - Unix - const char* inet_ntop(int family, const void* src, char* dst, socklen_t size);

Permet de récupérer l'adresse IP d'un socket, IPv4 ou IPv6, sous forme lisible.

- family est la famille du socket.

- src le pointeur vers l'adresse du socket.

- dst un pointeur vers un tampon où stocker l'adresse sous forme lisible.

- size la taille maximale du tampon.

char buff[INET6_ADDRSTRLEN] = {0};
return inet_ntop(addr.sin_family, (void*)&(addr.sin_addr), buff, INET6_ADDRSTRLEN);

L'utilisation de INET6_ADDRSTRLEN permet d'utiliser cette fonction indépendamment du fait qu'il s'agisse d'une adresse IPv4 ou IPv6 puisqu'une adresse IPv6 peut être écrite en utilisant jusque 45 caractères (INET6_ADDRSTRLEN vaut au moins 46) alors qu'une adresse IPv4 s'étend au maximum sur 15 caractères (et son équivalent INET_ADDRSTRLEN vaut au moins 16).

Cette fonction est bien entendu également utilisable dans du code client, mais l'intérêt parait moindre puisqu'un client se connecte au serveur via une adresse et un port connus.

II - Notre premier serveur

Pour notre premier serveur, on se contentera uniquement d'accepter la connexion de clients.

Notre programme devrait avoir l'architecture suivante :

À chaque connexion, on affichera l'adresse IP et le port du client.

Training Time ! Mettez en place un serveur simple suivant le schéma précédent auquel vous vous connectez avec le client réalisé dans la partie TCP - Premiers pas avec TCP.

Vous devriez retrouver le résultat présenté dans la partie premiers pas, à savoir :

Quand vous démarrez le serveur.

Puis

Quand un client se connecte au serveur.

III - Proposition de corrigé

La première version devrait être assez simple à mettre en place : il suffit de dérouler le schéma en écrivant chaque étape du code correspondant.

Ainsi, le programme final devrait ressembler, sous Windows, à ceci :

Premier serveur

1. int main()

2. {

3. WSAData wsaData;

4. if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

5. {

6. std::cout << "Erreur initialisation WinSock : " << WSAGetLastError();

7. return -1;

8. }

10. SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

11. if (server == INVALID_SOCKET)

12. {

13. std::cout << "Erreur initialisation socket : " << WSAGetLastError();

14. return -2;

15. }

17. const unsigned short port = 9999;

18. sockaddr_in addr;

19. addr.sin_addr.s_addr = INADDR_ANY;

20. addr.sin_port = htons(port);

21. addr.sin_family = AF_INET;

23. int res = bind(server, (sockaddr*)&addr, sizeof(addr));

24. if (res != 0)

25. {

26. std::cout << "Erreur bind : " << WSAGetLastError();

27. return -3;

28. }

30. res = listen(server, SOMAXCONN);

31. if (res != 0)

32. {

33. std::cout << "Erreur listen : " << WSAGetLastError();

34. return -4;

35. }

37. std::cout << "Serveur demarre sur le port " << port << std::endl;

39. sockaddr_in from = { 0 };

40. socklen_t addrlen = sizeof(addr);

41. SOCKET newClient = accept(server, (SOCKADDR*)(&from), &addrlen);

42. if (newClient != INVALID_SOCKET)

43. {

44. char buff[INET6_ADDRSTRLEN] = { 0 };

45. std::string clientAddress = inet_ntop(addr.sin_family, (void*)&(addr.sin_addr), buff, INET6_ADDRSTRLEN);

46. std::cout << "Connexion de " << clientAddress.c_str() << ":" << addr.sin_port << std::endl;

47. }

48. closesocket(server);

49. WSACleanup();

50. return 0;

51. }

Vous retrouvez l'initialisation de la bibliothèque socket propre à Windows, puis on crée un socket de manière identique au code client.

Ensuite ça diverge puisqu'on associe un port spécifique au serveur (on peut bind le port 0 et laisser le système choisir, mais connaître le port spécifique d'un serveur est généralement plus intéressant) avant de le passer en mode écoute et prêt à accepter des connexions entrantes.

Une fois une connexion acceptée, on se contente d'afficher l'IP et le port du client avant de fermer le programme. Oui, ce code n'accepte qu'un unique client, il faut bien commencer quelque part. Il s'agit de la retranscription exacte du schéma plus haut.

Une évolution directe serait de pouvoir accepter plusieurs clients et afficher leurs informations à chaque fois, en remplaçant les lignes 40 à 49 par :

1. for (;;)

2. {

3. sockaddr_in from = { 0 };

4. socklen_t addrlen = sizeof(addr);

5. SOCKET newClient = accept(server, (SOCKADDR*)(&from), &addrlen);

6. if (newClient != INVALID_SOCKET)

7. {

8. char buff[INET6_ADDRSTRLEN] = { 0 };

9. std::string clientAddress = inet_ntop(addr.sin_family, (void*)&(addr.sin_addr), buff, INET6_ADDRSTRLEN);

10. std::cout << "Connexion de " << clientAddress << ":" << addr.sin_port << std::endl;

11. }

12. else

13. break;

14. }

Enfin, on constate que du code peut être mis en commun avec les parties précédentes, notamment déjà l'initialisation.

Puisque le but final reste d'avoir à disposition une bibliothèque réseau utilisable, commençons déjà par factoriser ces parties.

Reprenons les fichiers Sockets.hpp et Sockets.cpp à notre disposition. Ajoutons une fonction bien utile pour la récupération de l'adresse IP depuis un socket dont le prototype sera std::string GetAddress(const sockaddr_in& addr); :

std::string GetAddress(const sockaddr_in& addr)
{
char buff[INET6_ADDRSTRLEN] = { 0 };
return inet_ntop(addr.sin_family, (void*)&(addr.sin_addr), buff, INET6_ADDRSTRLEN);
}

Retourne une chaîne vide en cas d'erreur.

Le programme final devrait ressembler à :

1. #include "Sockets.hpp"

3. #include <iostream>

4. #include <string>

6. int main()

7. {

8. if (!Sockets::Start())

9. {

10. std::cout << "Erreur initialisation WinSock : " << Sockets::GetError();

11. return -1;

12. }

14. SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

15. if (server == INVALID_SOCKET)

16. {

17. std::cout << "Erreur initialisation socket : " << Sockets::GetError();

18. return -2;

19. }

21. const unsigned short port = 9999;

22. sockaddr_in addr;

23. addr.sin_addr.s_addr = INADDR_ANY;

24. addr.sin_port = htons(port);

25. addr.sin_family = AF_INET;

27. int res = bind(server, (sockaddr*)&addr, sizeof(addr));

28. if (res != 0)

29. {

30. std::cout << "Erreur bind : " << Sockets::GetError();

31. return -3;

32. }

34. res = listen(server, SOMAXCONN);

35. if (res != 0)

36. {

37. std::cout << "Erreur listen : " << Sockets::GetError();

38. return -4;

39. }

41. std::cout << "Serveur demarre sur le port " << port << std::endl;

43. for (;;)

44. {

45. sockaddr_in from = { 0 };

46. socklen_t addrlen = sizeof(addr);

47. SOCKET newClient = accept(server, (SOCKADDR*)(&from), &addrlen);

48. if (newClient != INVALID_SOCKET)

49. {

50. std::string clientAddress = Sockets::GetAddress(from);

51. std::cout << "Connexion de " << clientAddress << ":" << addr.sin_port << std::endl;

52. }

53. else

54. break;

55. }

56. Sockets::CloseSocket(server);

57. Sockets::Release();

58. return 0;

59. }

Article précédent

Article suivant

<< TCP - Mise en place du protocole

Multi-threading et mutex >>