Cours programmation réseau en C++

TCP - Envoi et réception depuis le serveur

Bousk

Nous pouvons désormais nous connecter à notre serveur, il est temps de lui envoyer des données et en recevoir de sa part.

Commentez

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

- Auteur : Bousk

- Parution : 31 mars 2017

- 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 ® 2017 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. Envoyer et recevoir des données depuis un socket serveur

- II. send et recv

- III. Architecture du serveur et ses clients

- III-A. À chaque client son thread

- III-B. Un unique thread : vérifier l'état des descripteurs

- III-B-1. select - int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

- III-B-1-a. Connaître l'erreur d'un socket donné

- III-B-1-b. Windows - int getsockopt(SOCKET sckt, int level, int optname, char* optval, int* optlen);

- III-B-1-c. Unix - int getsockopt(int socketfd, int level, int optname, void* optval, socklen_t* optlen);

- III-C. Alternative à select : poll

- III-C-1. Windows - int WSAPoll(WSAPOLLFD fdarray[], unsigned long nfds, int timeout);

- III-C-2. Unix - int poll(struct pollfd* fds, nfds_t nfds, int timeout);

- III-C-3. Une utilisation portable de poll et WSAPoll

- III-C-4. poll ou select ?

- III-D. Un unique thread : socket non bloquant

- III-D-1. Windows - int ioctlsocket(SOCKET socket, long command, unsigned long* parameter);

- III-D-2. Unix - int fcntl(int fd, int cmd, …);

- III-D-3. Changements liés au mode non bloquant

- III-D-3-a. Serveur à un seul thread

I - Envoyer et recevoir des données depuis un socket serveur

Pour envoyer des données à et depuis un client connecté, on utilisera la même fonction que dans le code client : send.

De même, la réception de données se fait via recv.

II - send et recv

Pour rappel, les prototypes de ces fonctions sont int send(int socket, const void* datas, size_t len, int flags); et int recv(int socket, void* buffer, size_t len, int flags);.

Je vous renvoie vers la première partie du cours qui traite de ces fonctions plus en détail.

Nous avons vu que le paramètre socket est le socket auquel nous voulons envoyer les données ou duquel nous voulons les recevoir. Ici il s'agira donc du socket du client que nous avons créé via l'appel à accept vu dans la partie précédente.

III - Architecture du serveur et ses clients

Afin de gérer ses clients, notre serveur va maintenant devoir maintenir une liste de clients connectés en enregistrant les retours de accept qui représente chaque client effectivement connecté à notre serveur, et en supprimant ceux dont le socket retourne une erreur indiquant qu'ils ont été déconnectés.

« Gérer ses clients » signifie recevoir et traiter les données qu'ils envoient, les requêtes, puis envoyer d'éventuelles réponses.

Souvenez-vous aussi que send et surtout recv sont bloquants. La première solution à laquelle on pense est généralement d'avoir un thread par client. Commençons donc par celle-ci.

III-A - À chaque client son thread

La première option sera donc de créer un thread par client, tandis que le thread principal servira à l'initialisation et à accepter les connexions entrantes.

Voilà grossièrement à quoi devrait ressembler notre programme :

Pour ce qui est du client, nous réutiliserons le client de la partie 2 Envoi et réception.

Pour le code serveur, repartons sur le code précédent et créons dans un premier temps un thread par client dans la boucle d'accept, en lieu et place où nous nous contentions d'afficher les informations du client connecté. Nous allons maintenant renvoyer au client ce qu'il nous a envoyé :

Boucle d'acceptation des nouveaux clients et lancement du thread pour chacun

1. for (;;)

2. {

3. sockaddr_in from = { 0 };

4. socklen_t addrlen = sizeof(from);

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

6. if (newClient != INVALID_SOCKET)

7. {

8. std::thread([newClient, from]() {

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

10. const unsigned short clientPort = ntohs(from.sin_port);

11. std::cout << "Connexion de " << clientAddress.c_str() << ":" << clientPort << std::endl;

12. bool connected = true;

13. for(;;)

14. {

15. char buffer[200] = { 0 };

16. int ret = recv(newClient, buffer, 199, 0);

17. if (ret == 0 || ret == SOCKET_ERROR)

18. break;

19. std::cout << "[" << clientAddress << ":" << clientPort << "]" << buffer << std::endl;

20. ret = send(newClient, buffer, ret, 0);

21. if (ret == 0 || ret == SOCKET_ERROR)

22. break;

23. }

24. std::cout << "Deconnexion de [" << clientAddress << ":" << clientPort << "]" << std::endl;

25. }).detach();

26. }

27. else

28. break;

29. }

Lignes 8 à 25, se trouvent les changements et le code qui permet de lancer un thread reproduisant le comportement du schéma précédent : chaque client exécutera son recv puis son send dans son propre thread.

En termes de traitement de la requête, nous faisons au plus simple : il n'y a aucun traitement et nous nous contentons de retourner à l'expéditeur ce qu'il nous a envoyé.

Télécharger le code source de l'exemple.

III-B - Un unique thread : vérifier l'état des descripteurs

Une autre approche du serveur est d'avoir l'ensemble des traitements sur un unique thread.

D'abord, créons une structure très simple pour agréger les sockets de chaque client avec leur adresse :

1. struct Client {

2. SOCKET sckt;

3. sockaddr_in addr;

4. };

Qui nous permettra de facilement avoir l'information d'IP et port du client en question.

Pour garder en mémoire nos clients connectés, ayons recours à un std::vector :

std ::vector<Client> clients ;

III-B-1 - select - int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

Permet de récupérer le statut d'écriture, lecture ou erreur d'un ou plusieurs sockets, sous Windows, ou descripteurs de fichiers, sous Unix.

- nfds est l'identifiant du descripteur le plus élevé, plus un

- ce paramètre est ignoré sous Windows, mais présent pour compatibilité.

- readfds est un pointeur vers un ensemble de sockets pour lesquelles tester le statut de lecture.

- writefds est un pointeur vers un ensemble de sockets pour lesquelles tester le statut d'écriture.

- exceptfds est un pointeur vers un ensemble de sockets pour lesquelles tester le statut d'erreur.

- timeout est un pointeur vers une structure pour le temps maximum que select doit attendre et bloquer avant de retourner, une valeur de nullptr permet de bloquer jusqu'à ce qu'un des sockets soit prêt à lire ou écrire.

Pour les fd_set, on utilisera les macros de FD_ZERO pour l'initialiser et FD_SET pour mettre les valeurs des sockets, de cette forme :

fd_set set;
FD_ZERO(&set);
FD_SET(server, &set);

Où server est notre socket serveur.

select retourne le nombre de sockets qui sont prêts à lire, écrire ou ayant une erreur, peut retourner 0 si aucun socket n'est prêt. Retourne -1 en cas d'erreur.

Pour vérifier qu'un socket, ou descripteur de fichier, particulier a été défini dans la structure, on utilisera la macro FD_ISSET.

Pour vérifier qu'une connexion entrante est en attente, que l'appel à accept ne sera pas bloquant, on doit vérifier que notre socket serveur est prêt en écriture :

1. fd_set set;

2. timeval timeout = { 0 };

3. FD_ZERO(&set);

4. FD_SET(server, &set);

5. int selectReady = select(server + 1, &set, nullptr, nullptr, &timeout);

6. if (selectReady == -1)

7. {

8. std::cout << "Erreur select pour accept : " << Sockets::GetError() << std::endl;

9. break;

10. }

11. else if (selectReady > 0)

12. {

13. // notre socket server est prêt à être lu

14. sockaddr_in from = { 0 };

15. socklen_t addrlen = sizeof(from);

16. SOCKET newClientSocket = accept(server, (SOCKADDR*)(&from), &addrlen);

17. if (newClientSocket != INVALID_SOCKET)

18. {

19. Client newClient;

20. newClient.sckt = newClientSocket;

21. newClient.addr = from;

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

23. const unsigned short clientPort = ntohs(from.sin_port);

24. std::cout << "Connexion de " << clientAddress.c_str() << ":" << clientPort << std::endl;

25. }

26. }

Pour vérifier qu'un de nos clients est prêt à recevoir des données, l'utilisation sera identique, mais notre structure fd_set sera utilisé pour vérifier tous les clients via un seul appel à select :

1. fd_set setReads;

2. fd_set setWrite;

3. fd_set setErrors;

4. int highestFd = 0;

5. timeval timeout = { 0 };

6. for (auto& client : clients)

7. {

8. FD_SET(client.sckt, &setReads);

9. FD_SET(client.sckt, &setWrite);

10. FD_SET(client.sckt, &setErrors);

11. if (client.sckt > highestFd)

12. highestFd = client.sckt;

13. }

14. int selectResult = select(highestFd + 1, &setReads, &setWrite, &setErrors, &timeout);

15. if (selectResult == -1)

16. // erreur

17. else if (selectResult > 0)

18. // au moins 1 client a une action à exécuter

Si selectResult est strictement positif, au moins un de nos clients est prêt à recevoir ou envoyer des données, ou a une erreur :

1. auto itClient = clients.begin();

2. while (itClient != clients.end())

3. {

4. const std::string clientAddress = Sockets::GetAddress(itClient->addr);

5. const unsigned short clientPort = ntohs(itClient->addr.sin_port);

7. bool hasError = false;

8. if (FD_ISSET(itClient->sckt, &setErrors))

9. {

10. std::cout << "Erreur" << std::endl;

11. hasError = true;

12. }

13. else if (FD_ISSET(itClient->sckt, &setReads))

14. {

15. char buffer[200] = { 0 };

16. int ret = recv(itClient->sckt, buffer, 199, 0);

17. if (ret == 0 || ret == SOCKET_ERROR)

18. {

19. std::cout << "Erreur reception" << std::endl;

20. hasError = true;

21. }

22. else

23. {

24. std::cout << "[" << clientAddress << ":" << clientPort << "]" << buffer << std::endl;

25. if (FD_ISSET(itClient->sckt, &setWrite))

26. {

27. ret = send(itClient->sckt, buffer, ret, 0);

28. if (ret == 0 || ret == SOCKET_ERROR)

29. {

30. std::cout << "Erreur envo" << std::endl;

31. hasError = true;

32. }

33. }

34. }

35. }

36. if (hasError)

37. {

38. //!< Déconnecté

39. std::cout << "Deconnexion de [" << clientAddress << ":" << clientPort << "]" << std::endl;

40. itClient = clients.erase(itClient);

41. }

42. else

43. {

44. ++itClient;

45. }

46. }

Ce code nécessitera quelques static_cast selon la plateforme et les options de compilation.

III-B-1-a - Connaître l'erreur d'un socket donné

Puisque select permet de connaître l'état de plusieurs sockets à la fois, nous ne pouvons pas utiliser notre Sockets::GetError() pour déterminer l'erreur d'un socket en particulier.

Pour connaître l'erreur d'un socket en particulier, il faudra utiliser la fonction getsockopt.

III-B-1-b - Windows - int getsockopt(SOCKET sckt, int level, int optname, char* optval, int* optlen);

Permet de récupérer certaines informations d'un socket, dont l'erreur qui l'affecte.

- sckt est le socket en question.

- level est le niveau relatif à l'option que nous voulons récupérer. Pour les erreurs il s'agira de SOL_SOCKET.

- optname est le nom de l'option à récupérer. Pour les erreurs il s'agira de SO_ERROR.

- optval est un tampon pour récupérer la valeur de l'option.

- optlen est un pointeur vers la taille du tampon.

Son utilisation sera donc :

getsockopt

int err;
int errsize = sizeof(err);
getsockopt(sckt, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &errsize);

Retourne 0 si aucune erreur est survenue, SOCKET_ERROR sinon.

III-B-1-c - Unix - int getsockopt(int socketfd, int level, int optname, void* optval, socklen_t* optlen);

La principale différence vient du type des paramètres, un int remplace le SOCKET pour le descripteur de socket, le tampon de la valeur de l'option sera un void* et la taille du tampon sera représentée par un socklen_t.

Puisque notre code possède déjà de quoi faire abstraction du SOCKET et socklen_t, et que le langage permet de convertir automatiquement un char* vers un void*, l'appel pourra être uniformisé entre Windows et Unix sous cette forme :

getsockopt cross-platform

socklen_t err;
int errsize = sizeof(err);
if (getsockopt(sckt, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &errsize) != 0)
{
// erreur lors de la recuperation d'erreur…
std::cout << "Erreur lors de la determination de l'erreur : " << Sockets::GetError() << std::endl;
}

Télécharger le code source de l'exemple

III-C - Alternative à select : poll

Les systèmes plus récents (Windows Vista et supérieurs) proposent une alternative à select avec poll. Leur fonctionnement est identique : vérifier l'état d'un ensemble de descripteurs. Le principal avantage qui aura un intérêt est que poll peut gérer plus que 1024 descripteurs à la fois.

III-C-1 - Windows - int WSAPoll(WSAPOLLFD fdarray[], unsigned long nfds, int timeout);

Permet de récupérer l'état d'un ensemble de descripteurs de sockets.

- fdarray est un tableau de structures WSAPOLLFD (voir détails ci-dessous).

- nfds est le nombre de structures WSAPOLLFD dans fdarray.

- timeout est la durée maximale d'attente avant retour.

- timeout < 0 indique une attente infinie : un appel bloquant.

- timeout == 0 indique un appel non bloquant.

- timeout > 0 pour définir un temps d'attente en millisecondes.

La structure WSAPOLLFD possède trois champs :

- fd de type SOCKET pour accueillir le descripteur de socket ;

- events de type short servant de champ de bits des états à vérifier ;

- revents de type short qui sera modifié par l'appel avec les flags des états trouvés pour le socket en question.

Voir la documentation plus complète de  WSAPoll sur la MSDN.

III-C-2 - Unix - int poll(struct pollfd* fds, nfds_t nfds, int timeout);

Version Unix de WSAPoll.

- fds est un tableau de structure pollfd (voir détails ci-dessous).

- nfds est la taille du tableau fds.

- timeout est la durée maximale d'attente avant retour.

- timeout < 0 indique une attente infinie : un appel bloquant.

- timeout == 0 indique un appel non bloquant.

- timeout > 0 pour définir un temps d'attente en millisecondes.

La structure pollfd possède également trois champs :

- fd de type int pour accueillir le descripteur de fichier ;

- events de type short servant de champ de bits des états à vérifier ;

- revents de type short qui sera modifié par l'appel avec les flags des états trouvés pour le socket en question.

III-C-3 - Une utilisation portable de poll et WSAPoll

Malgré les différences de prototypes et types, les fonctions sont suffisamment similaires pour que la portabilité soit simple. Les structures WSAPOLLFD et pollfd sont identiques, et Windows définit d'ailleurs lui-même une struct pollfd. Inutile de passer par la déclaration WSAPOLLFD donc.

Le second paramètre nfds est déclaré comme unsigned long sur Windows, et est un typedef vers unsigned int sur Unix. Ayons recours à la technique habituelle, et définissons nfds_t pour Windows afin d'utiliser ce type dans notre code indépendamment de la plateforme.

Sockets.hpp

#ifdef _WIN32
…
typedef unsigned long nfds_t;
#define poll WSAPoll
…
#else
…
#include <poll.h>
…
#endif

Les noms des flags de chaque état sont quelque peu différents d'une plateforme à l'autre, mais Windows s'en sort bien en définissant les valeurs que l'on s'attend à avoir en Posix. Les valeurs qui nous intéressent sont :

- POLLIN pour vérifier que le socket est prêt en lecture ;

- POLLOUT pour vérifier que le socket est prêt en écriture.

Les valeurs intéressantes à vérifier dans revents au retour de poll sont :

- POLLERR pour vérifier qu'une erreur est survenue ;

- POLLNVAL si le socket n'était pas initialisé ;

- POLLHUP si la connexion a été interrompue ;

- POLLIN si l'écriture est possible.

- Notez que si vous envoyez plus de données que la place disponible, le send sera toujours bloquant ;

- POLLOUT si des données sont disponibles en lecture.

Nous pouvons modifier le code précédent utilisant select pour utiliser poll :

poll remplace select

1. std::map<SOCKET, Client> clients;

2. std::vector<pollfd> clientsFds;

3. for (;;)

4. {

5. {

6. pollfd pollServerFd;

7. pollServerFd.fd = server;

8. pollServerFd.events = POLLIN;

9. int pollReady = poll(&pollServerFd, 1, 0);

10. if (pollReady == -1)

11. {

12. std::cout << "Erreur poll pour accept : " << Sockets::GetError() << std::endl;

13. break;

14. }

15. if (pollReady > 0)

16. {

17. sockaddr_in from = { 0 };

18. socklen_t addrlen = sizeof(from);

19. SOCKET newClientSocket = accept(server, (SOCKADDR*)(&from), &addrlen);

20. if (newClientSocket != INVALID_SOCKET)

21. {

22. Client newClient;

23. newClient.sckt = newClientSocket;

24. newClient.addr = from;

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

26. const unsigned short clientPort = ntohs(from.sin_port);

27. std::cout << "Connexion de " << clientAddress.c_str() << ":" << clientPort << std::endl;

28. clients[newClientSocket] = newClient;

29. pollfd newClientPollFd;

30. newClientPollFd.fd = newClientSocket;

31. newClientPollFd.events = POLLIN | POLLOUT;

32. clientsFds.push_back(newClientPollFd);

33. }

34. }

35. }

36. if (!clients.empty())

37. {

38. int pollResult = poll(clientsFds.data(), static_cast<nfds_t>(clientsFds.size()), 0);

39. if (pollResult == -1)

40. {

41. std::cout << "Erreur poll pour clients : " << Sockets::GetError() << std::endl;

42. break;

43. }

44. else if (pollResult > 0)

45. {

46. auto itPollResult = clientsFds.cbegin();

47. while (itPollResult != clientsFds.cend())

48. {

49. const auto clientIt = clients.find(itPollResult->fd);

50. if (clientIt == clients.cend())

51. {

52. itPollResult = clientsFds.erase(itPollResult);

53. continue;

54. }

55. const auto& client = clientIt->second;

56. const std::string clientAddress = Sockets::GetAddress(client.addr);

57. const unsigned short clientPort = ntohs(client.addr.sin_port);

58. bool disconnect = false;

59. if (itPollResult->revents & POLLERR)

60. {

61. socklen_t err;

62. int errsize = sizeof(err);

63. if (getsockopt(client.sckt, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &errsize) != 0)

64. {

65. std::cout << "Impossible de determiner l'erreur : " << Sockets::GetError() << std::endl;

66. }

67. if (err != 0)

68. std::cout << "Erreur : " << err << std::endl;

69. disconnect = true;

70. }

71. else if (itPollResult->revents & (POLLHUP | POLLNVAL))

72. {

73. disconnect = true;

74. }

75. else if (itPollResult->revents & POLLIN)

76. {

77. char buffer[200] = { 0 };

78. int ret = recv(client.sckt, buffer, 199, 0);

79. if (ret == 0)

80. {

81. std::cout << "Connexion terminee" << std::endl;

82. disconnect = true;

83. }

84. else if (ret == SOCKET_ERROR)

85. {

86. std::cout << "Erreur reception : " << Sockets::GetError() << std::endl;

87. disconnect = true;

88. }

89. else

90. {

91. std::cout << "[" << clientAddress << ":" << clientPort << "]" << buffer << std::endl;

92. if (itPollResult->revents & POLLOUT)

93. {

94. ret = send(client.sckt, buffer, ret, 0);

95. if (ret == 0 || ret == SOCKET_ERROR)

96. {

97. std::cout << "Erreur envoi : " << Sockets::GetError() << std::endl;

98. disconnect = true;

99. }

100. }

101. }

102. }

103. if (disconnect)

104. {

105. std::cout << "Deconnexion de " << "[" << clientAddress << ":" << clientPort << "]" << std::endl;

106. itPollResult = clientsFds.erase(itPollResult);

107. clients.erase(clientIt);

108. }

109. else

110. {

111. ++itPollResult;

112. }

113. }

114. }

115. }

116. }

Notez également le changement de clients pour un std::map<SOCKET, Client>. Ce changement peut également être fait dans l'exemple utilisant select.

III-C-4 - poll ou select ?

Un avantage de poll est la différenciation entre les flags d'entrée, à vérifier, le champ events de la structure, et les flags de sortie, retournés, le champ revents de la structure. Ça permet de conserver une collection de sockets et flags à appeler sans nécessiter la moindre réinitialisation après appel à poll.

Un autre point fort est que poll peut être utilisé sur plus de 1024 descripteurs à la fois, là où select est limité à 1024.

En dehors de ça, select est surtout le premier implémenté historiquement et l'utilisation de l'un ou l'autre est le plus souvent interchangeable. Pour un développement récent, si vous ne comptez pas avoir un code portable sur d'anciens systèmes, autant utiliser poll à mon avis. Sauf si celui-ci n'est pas disponible sur la plateforme ciblée.

D'autres systèmes existent aujourd'hui tel epoll ou kqueue. Ils feront sans doute l'objet d'articles plus avancée par la suite.

Télécharger le code source de l'exemple

III-D - Un unique thread : socket non bloquant

Une autre façon de faire, qui peut aller de pair avec l'utilisation de select, est de déclarer explicitement le socket comme non bloquant.

Cette option a ma préférence dans le cadre d'un serveur, parce que plus simple dans l'écriture et les traitements, selon moi.

III-D-1 - Windows - int ioctlsocket(SOCKET socket, long command, unsigned long* parameter);

Permet de changer le mode d'entrée/sortie d'un socket.

- socket est le socket sur lequel appliquer la modification.

- command est l'identifiant de la commande à appliquer au socket.

- parameter est un pointeur vers un paramètre à appliquer à la commande.

Retourne -1 en cas d'échec, 0 sinon.

Dans le cas qui nous intéresse, rendre le socket non bloquant, l'appel sera :

Rendre un socket non bloquant sous Windows

1. u_long mode = 1;

2. ioctlsocket(s, FIONBIO, &mode);

Pour avoir la liste des commandes possibles et leur paramètre, référez-vous à la documentation sur MSDN.

III-D-2 - Unix - int fcntl(int fd, int cmd, …);

Permet de changer une propriété d'un descripteur de fichier.

- fd est le descripteur de fichier auquel appliquer la modification, dans notre cas le socket.

- cmd la commande à appliquer.

- un éventuel dernier paramètre selon la commande souhaitée.

La valeur de retour dépendra de la commande à exécuter.

fcntl(socket, F_SETFL, O_NONBLOCK);

Dans ce cas retournera -1 en cas d'erreur, autre chose sinon.

Pour avoir la liste des commandes possibles et leur paramètre, ainsi que les valeurs de retour pour chaque commande, référez-vous à la documentation.

III-D-3 - Changements liés au mode non bloquant

Que se passe-t-il désormais pour les fonctions auparavant bloquantes ?

accept retournera INVALID_SOCKET si aucune nouvelle connexion n'est arrivée au lieu d'attendre la prochaine connexion, le nouveau socket sinon.

recv retournera une erreur (-1 ou SOCKET_ERROR). Il faudra alors récupérer le code erreur de la bibliothèque socket afin de vérifier s'il s'agit d'une erreur légitime à traiter en tant que telle ou la valeur de WSAEWOULDBLOCK (pour Windows) ou EWOULDBLOCK (pour Unix) indiquant que recv a retourné sans lire de données au lieu de bloquer.

send aura le même comportement que recv si la mise en file d'envois aurait dû être bloquante : retour d'une valeur d'erreur, puis il faudra vérifier si l'erreur de la bibliothèque socket est WSAEWOULDBLOCK/EWOULDBLOCK ou non.

III-D-3-a - Serveur à un seul thread

Puisque Windows et Unix sont ici encore différents, l'un utilisant WSAEWOULDBLOCK l'autre EWOULDBLOCK, commençons par uniformiser ceci dans notre bibliothèque. Ajoutons une énumération d'erreurs à notre code, par exemple dans un nouveau fichier :

Errors.hpp

1. #ifndef BOUSK_DVP_COURS_ERRORS_HPP

2. #define BOUSK_DVP_COURS_ERRORS_HPP

4. #pragma once

6. #ifdef _WIN32

7. #include <WinSock2.h>

8. #else

9. #include <cerrno>

10. #define SOCKET int

11. #define INVALID_SOCKET ((int)-1)

12. #define SOCKET_ERROR (int(-1))

13. #endif

15. namespace Sockets

16. {

17. int GetError();

18. enum class Errors {

19. #ifdef _WIN32

20. WOULDBLOCK = WSAEWOULDBLOCK

21. #else

22. WOULDBLOCK = EWOULDBLOCK

23. #endif

24. };

25. }

27. #endif // BOUSK_DVP_COURS_ERRORS_HPP

J'ai également déplacé int GetError(); de Sockets.cpp vers Errors.cpp afin de centraliser tout ce qui est lié aux erreurs dans ces nouveaux fichiers.

Nous avons maintenant une façon élégante et portable de vérifier l'aspect « erreur, opération bloquante qui n'a pas bloqué » via cette nouvelle valeur Sockets::Errors::WOULDBLOCK.

Il est maintenant temps de modifier notre programme principal.

D'abord, il faut définir notre socket serveur comme non bloquant :

if (!Sockets::SetNonBlocking(server))
{
std::cout << "Erreur settings non bloquant : " << Sockets::GetError();
return -3;
}

Ensuite, la modification de la boucle principale qui aura maintenant cette forme :

Boucle principale : acceptation des nouveaux clients et gestion des connectés et déconnectés

1. std::vector<Client> clients;

2. for (;;)

3. {

4. {

5. sockaddr_in from = { 0 };

6. socklen_t addrlen = sizeof(from);

7. SOCKET newClientSocket = accept(server, (SOCKADDR*)(&from), &addrlen);

8. if (newClientSocket != INVALID_SOCKET)

9. {

10. if (!Sockets::SetNonBlocking(newClientSocket))

11. {

12. std::cout << "Erreur settings nouveau socket non bloquant : " << Sockets::GetError() << std::endl;

13. Sockets::CloseSocket(newClientSocket);

14. continue;

15. }

16. Client newClient;

17. newClient.sckt = newClientSocket;

18. newClient.addr = from;

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

20. const unsigned short clientPort = ntohs(from.sin_port);

21. std::cout << "Connexion de " << clientAddress.c_str() << ":" << clientPort << std::endl;

22. clients.push_back(newClient);

23. }

24. }

25. {

26. auto itClient = clients.begin();

27. while ( itClient != clients.end() )

28. {

29. const std::string clientAddress = Sockets::GetAddress(itClient->addr);

30. const unsigned short clientPort = ntohs(itClient->addr.sin_port);

31. char buffer[200] = { 0 };

32. bool disconnect = false;

33. int ret = recv(itClient->sckt, buffer, 199, 0);

34. if (ret == 0)

35. {

36. //!< Déconnecté

37. disconnect = true;

38. }

39. if (ret == SOCKET_ERROR)

40. {

41. int error = Sockets::GetError();

42. if (error != static_cast<int>(Sockets::Errors::WOULDBLOCK))

43. {

44. disconnect = true;

45. }

46. //!< il n'y avait juste rien à recevoir

47. }

48. std::cout << "[" << clientAddress << ":" << clientPort << "]" << buffer << std::endl;

49. ret = send(itClient->sckt, buffer, ret, 0);

50. if (ret == 0 || ret == SOCKET_ERROR)

51. {

52. disconnect = true;

53. }

54. if (disconnect)

55. {

56. std::cout << "Deconnexion de [" << clientAddress << ":" << clientPort << "]" << std::endl;

57. itClient = clients.erase(itClient);

58. }

59. else

60. ++itClient;

61. }

62. }

63. }

Notez qu'il faut définir chaque socket accepté comme non bloquant également.

En théorie, il faudrait également vérifier que l'erreur retournée par send ne soit pas WOULDBLOCK. En pratique il est très difficile de faire bloquer send, il faudrait vouloir envoyer une très grande quantité de données, ce que ne fait pas ce programme. Plus d'informations sous le terme de « send buffer size » dans votre moteur de recherche préféré et dans une partie précédente Puis-je vraiment envoyer 64 ko de données ?

Télécharger le code source de l'exemple

Article précédent

Article suivant

<< Multi-threading et mutex

TCP - Mode non bloquant pour le client >>