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 :
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 :
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.
int
main()
{
WSAData wsaData;
if
(WSAStartup(MAKEWORD(2
, 2
), &
wsaData) !=
0
)
{
std::
cout <<
"Erreur initialisation WinSock : "
<<
WSAGetLastError();
return
-
1
;
}
SOCKET server =
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if
(server ==
INVALID_SOCKET)
{
std::
cout <<
"Erreur initialisation socket : "
<<
WSAGetLastError();
return
-
2
;
}
const
unsigned
short
port =
9999
;
sockaddr_in addr;
addr.sin_addr.s_addr =
INADDR_ANY;
addr.sin_port =
htons(port);
addr.sin_family =
AF_INET;
int
res =
bind(server, (sockaddr*
)&
addr, sizeof
(addr));
if
(res !=
0
)
{
std::
cout <<
"Erreur bind : "
<<
WSAGetLastError();
return
-
3
;
}
res =
listen(server, SOMAXCONN);
if
(res !=
0
)
{
std::
cout <<
"Erreur listen : "
<<
WSAGetLastError();
return
-
4
;
}
std::
cout <<
"Serveur demarre sur le port "
<<
port <<
std::
endl;
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(addr);
SOCKET newClient =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClient !=
INVALID_SOCKET)
{
char
buff[INET6_ADDRSTRLEN] =
{
0
}
;
std::
string clientAddress =
inet_ntop(addr.sin_family, (void
*
)&
(addr.sin_addr), buff, INET6_ADDRSTRLEN);
std::
cout <<
"Connexion de "
<<
clientAddress.c_str() <<
":"
<<
addr.sin_port <<
std::
endl;
}
closesocket(server);
WSACleanup();
return
0
;
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
for
(;;)
{
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(addr);
SOCKET newClient =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClient !=
INVALID_SOCKET)
{
char
buff[INET6_ADDRSTRLEN] =
{
0
}
;
std::
string clientAddress =
inet_ntop(addr.sin_family, (void
*
)&
(addr.sin_addr), buff, INET6_ADDRSTRLEN);
std::
cout <<
"Connexion de "
<<
clientAddress <<
":"
<<
addr.sin_port <<
std::
endl;
}
else
break
;
}
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 à :
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.
#include
"Sockets.hpp"
#include
<iostream>
#include
<string>
int
main()
{
if
(!
Sockets::
Start())
{
std::
cout <<
"Erreur initialisation WinSock : "
<<
Sockets::
GetError();
return
-
1
;
}
SOCKET server =
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if
(server ==
INVALID_SOCKET)
{
std::
cout <<
"Erreur initialisation socket : "
<<
Sockets::
GetError();
return
-
2
;
}
const
unsigned
short
port =
9999
;
sockaddr_in addr;
addr.sin_addr.s_addr =
INADDR_ANY;
addr.sin_port =
htons(port);
addr.sin_family =
AF_INET;
int
res =
bind(server, (sockaddr*
)&
addr, sizeof
(addr));
if
(res !=
0
)
{
std::
cout <<
"Erreur bind : "
<<
Sockets::
GetError();
return
-
3
;
}
res =
listen(server, SOMAXCONN);
if
(res !=
0
)
{
std::
cout <<
"Erreur listen : "
<<
Sockets::
GetError();
return
-
4
;
}
std::
cout <<
"Serveur demarre sur le port "
<<
port <<
std::
endl;
for
(;;)
{
sockaddr_in from =
{
0
}
;
socklen_t addrlen =
sizeof
(addr);
SOCKET newClient =
accept(server, (SOCKADDR*
)(&
from), &
addrlen);
if
(newClient !=
INVALID_SOCKET)
{
std::
string clientAddress =
Sockets::
GetAddress(from);
std::
cout <<
"Connexion de "
<<
clientAddress <<
":"
<<
addr.sin_port <<
std::
endl;
}
else
break
;
}
Sockets::
CloseSocket(server);
Sockets::
Release();
return
0
;
}
Article précédent | Article suivant |
---|---|
<< TCP - Mise en place du protocole | Multi-threading et mutex >> |