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é :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
for
(;;)
{
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(from);
SOCKET newClient =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClient !=
INVALID_SOCKET)
{
std::
thread([newClient, from]() {
const
std::
string clientAddress =
Sockets::
GetAddress(from);
const
unsigned
short
clientPort =
ntohs(from.sin_port);
std::
cout <<
"Connexion de "
<<
clientAddress.c_str() <<
":"
<<
clientPort <<
std::
endl;
bool
connected =
true
;
for
(;;)
{
char
buffer[200
] =
{
0
}
;
int
ret =
recv(newClient, buffer, 199
, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
break
;
std::
cout <<
"["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
buffer <<
std::
endl;
ret =
send(newClient, buffer, ret, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
break
;
}
std::
cout <<
"Deconnexion de ["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
std::
endl;
}
).detach();
}
else
break
;
}
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é.
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 :
2.
3.
4.
struct
Client {
SOCKET sckt;
sockaddr_in addr;
}
;
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
fd_set set;
timeval timeout =
{
0
}
;
FD_ZERO(&
set);
FD_SET(server, &
set);
int
selectReady =
select(server +
1
, &
set, nullptr
, nullptr
, &
timeout);
if
(selectReady ==
-
1
)
{
std::
cout <<
"Erreur select pour accept : "
<<
Sockets::
GetError() <<
std::
endl;
break
;
}
else
if
(selectReady >
0
)
{
// notre socket server est prêt à être lu
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(from);
SOCKET newClientSocket =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClientSocket !=
INVALID_SOCKET)
{
Client newClient;
newClient.sckt =
newClientSocket;
newClient.addr =
from;
const
std::
string clientAddress =
Sockets::
GetAddress(from);
const
unsigned
short
clientPort =
ntohs(from.sin_port);
std::
cout <<
"Connexion de "
<<
clientAddress.c_str() <<
":"
<<
clientPort <<
std::
endl;
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
fd_set setReads;
fd_set setWrite;
fd_set setErrors;
int
highestFd =
0
;
timeval timeout =
{
0
}
;
for
(auto
&
client : clients)
{
FD_SET(client.sckt, &
setReads);
FD_SET(client.sckt, &
setWrite);
FD_SET(client.sckt, &
setErrors);
if
(client.sckt >
highestFd)
highestFd =
client.sckt;
}
int
selectResult =
select(highestFd +
1
, &
setReads, &
setWrite, &
setErrors, &
timeout);
if
(selectResult ==
-
1
)
// erreur
else
if
(selectResult >
0
)
// 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
auto
itClient =
clients.begin();
while
(itClient !=
clients.end())
{
const
std::
string clientAddress =
Sockets::
GetAddress(itClient->
addr);
const
unsigned
short
clientPort =
ntohs(itClient->
addr.sin_port);
bool
hasError =
false
;
if
(FD_ISSET(itClient->
sckt, &
setErrors))
{
std::
cout <<
"Erreur"
<<
std::
endl;
hasError =
true
;
}
else
if
(FD_ISSET(itClient->
sckt, &
setReads))
{
char
buffer[200
] =
{
0
}
;
int
ret =
recv(itClient->
sckt, buffer, 199
, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
{
std::
cout <<
"Erreur reception"
<<
std::
endl;
hasError =
true
;
}
else
{
std::
cout <<
"["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
buffer <<
std::
endl;
if
(FD_ISSET(itClient->
sckt, &
setWrite))
{
ret =
send(itClient->
sckt, buffer, ret, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
{
std::
cout <<
"Erreur envo"
<<
std::
endl;
hasError =
true
;
}
}
}
}
if
(hasError)
{
//!
< Déconnecté
std::
cout <<
"Deconnexion de ["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
std::
endl;
itClient =
clients.erase(itClient);
}
else
{
++
itClient;
}
}
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 :
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 :
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;
}
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.
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.
#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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
std::
map<
SOCKET, Client>
clients;
std::
vector<
pollfd>
clientsFds;
for
(;;)
{
{
pollfd pollServerFd;
pollServerFd.fd =
server;
pollServerFd.events =
POLLIN;
int
pollReady =
poll(&
pollServerFd, 1
, 0
);
if
(pollReady ==
-
1
)
{
std::
cout <<
"Erreur poll pour accept : "
<<
Sockets::
GetError() <<
std::
endl;
break
;
}
if
(pollReady >
0
)
{
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(from);
SOCKET newClientSocket =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClientSocket !=
INVALID_SOCKET)
{
Client newClient;
newClient.sckt =
newClientSocket;
newClient.addr =
from;
const
std::
string clientAddress =
Sockets::
GetAddress(from);
const
unsigned
short
clientPort =
ntohs(from.sin_port);
std::
cout <<
"Connexion de "
<<
clientAddress.c_str() <<
":"
<<
clientPort <<
std::
endl;
clients[newClientSocket] =
newClient;
pollfd newClientPollFd;
newClientPollFd.fd =
newClientSocket;
newClientPollFd.events =
POLLIN |
POLLOUT;
clientsFds.push_back(newClientPollFd);
}
}
}
if
(!
clients.empty())
{
int
pollResult =
poll(clientsFds.data(), static_cast
<
nfds_t>
(clientsFds.size()), 0
);
if
(pollResult ==
-
1
)
{
std::
cout <<
"Erreur poll pour clients : "
<<
Sockets::
GetError() <<
std::
endl;
break
;
}
else
if
(pollResult >
0
)
{
auto
itPollResult =
clientsFds.cbegin();
while
(itPollResult !=
clientsFds.cend())
{
const
auto
clientIt =
clients.find(itPollResult->
fd);
if
(clientIt ==
clients.cend())
{
itPollResult =
clientsFds.erase(itPollResult);
continue
;
}
const
auto
&
client =
clientIt->
second;
const
std::
string clientAddress =
Sockets::
GetAddress(client.addr);
const
unsigned
short
clientPort =
ntohs(client.addr.sin_port);
bool
disconnect =
false
;
if
(itPollResult->
revents &
POLLERR)
{
socklen_t err;
int
errsize =
sizeof
(err);
if
(getsockopt(client.sckt, SOL_SOCKET, SO_ERROR, reinterpret_cast
<
char
*>
(&
err), &
errsize) !=
0
)
{
std::
cout <<
"Impossible de determiner l'erreur : "
<<
Sockets::
GetError() <<
std::
endl;
}
if
(err !=
0
)
std::
cout <<
"Erreur : "
<<
err <<
std::
endl;
disconnect =
true
;
}
else
if
(itPollResult->
revents &
(POLLHUP |
POLLNVAL))
{
disconnect =
true
;
}
else
if
(itPollResult->
revents &
POLLIN)
{
char
buffer[200
] =
{
0
}
;
int
ret =
recv(client.sckt, buffer, 199
, 0
);
if
(ret ==
0
)
{
std::
cout <<
"Connexion terminee"
<<
std::
endl;
disconnect =
true
;
}
else
if
(ret ==
SOCKET_ERROR)
{
std::
cout <<
"Erreur reception : "
<<
Sockets::
GetError() <<
std::
endl;
disconnect =
true
;
}
else
{
std::
cout <<
"["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
buffer <<
std::
endl;
if
(itPollResult->
revents &
POLLOUT)
{
ret =
send(client.sckt, buffer, ret, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
{
std::
cout <<
"Erreur envoi : "
<<
Sockets::
GetError() <<
std::
endl;
disconnect =
true
;
}
}
}
}
if
(disconnect)
{
std::
cout <<
"Deconnexion de "
<<
"["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
std::
endl;
itPollResult =
clientsFds.erase(itPollResult);
clients.erase(clientIt);
}
else
{
++
itPollResult;
}
}
}
}
}
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.
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 :
2.
u_long mode =
1
;
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
#ifndef BOUSK_DVP_COURS_ERRORS_HPP
#define BOUSK_DVP_COURS_ERRORS_HPP
#pragma once
#ifdef _WIN32
#include
<WinSock2.h>
#else
#include
<cerrno>
#define SOCKET int
#define INVALID_SOCKET ((int)-1)
#define SOCKET_ERROR (int(-1))
#endif
namespace
Sockets
{
int
GetError();
enum
class
Errors {
#ifdef _WIN32
WOULDBLOCK =
WSAEWOULDBLOCK
#else
WOULDBLOCK =
EWOULDBLOCK
#endif
}
;
}
#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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
std::
vector<
Client>
clients;
for
(;;)
{
{
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(from);
SOCKET newClientSocket =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClientSocket !=
INVALID_SOCKET)
{
if
(!
Sockets::
SetNonBlocking(newClientSocket))
{
std::
cout <<
"Erreur settings nouveau socket non bloquant : "
<<
Sockets::
GetError() <<
std::
endl;
Sockets::
CloseSocket(newClientSocket);
continue
;
}
Client newClient;
newClient.sckt =
newClientSocket;
newClient.addr =
from;
const
std::
string clientAddress =
Sockets::
GetAddress(from);
const
unsigned
short
clientPort =
ntohs(from.sin_port);
std::
cout <<
"Connexion de "
<<
clientAddress.c_str() <<
":"
<<
clientPort <<
std::
endl;
clients.push_back(newClient);
}
}
{
auto
itClient =
clients.begin();
while
( itClient !=
clients.end() )
{
const
std::
string clientAddress =
Sockets::
GetAddress(itClient->
addr);
const
unsigned
short
clientPort =
ntohs(itClient->
addr.sin_port);
char
buffer[200
] =
{
0
}
;
bool
disconnect =
false
;
int
ret =
recv(itClient->
sckt, buffer, 199
, 0
);
if
(ret ==
0
)
{
//!
< Déconnecté
disconnect =
true
;
}
if
(ret ==
SOCKET_ERROR)
{
int
error =
Sockets::
GetError();
if
(error !=
static_cast
<
int
>
(Sockets::Errors::
WOULDBLOCK))
{
disconnect =
true
;
}
//!
< il n'y avait juste rien à recevoir
}
std::
cout <<
"["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
buffer <<
std::
endl;
ret =
send(itClient->
sckt, buffer, ret, 0
);
if
(ret ==
0
||
ret ==
SOCKET_ERROR)
{
disconnect =
true
;
}
if
(disconnect)
{
std::
cout <<
"Deconnexion de ["
<<
clientAddress <<
":"
<<
clientPort <<
"]"
<<
std::
endl;
itClient =
clients.erase(itClient);
}
else
++
itClient;
}
}
}
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 |
---|---|
TCP - Mode non bloquant pour le client >> |