Serveur en C++
Voici le code d'un serveur que j'ai pu faire en C++.
Que je vais vous détailler pas à pas au fur à mesure pour comprendre pourquoi chaque fonction est appelée et a quoi, elle sert.
Source github
void ClassSocket::createsocket(void)
{
struct rlimit rlp;
struct sockaddr_in sin;
int optval;
if (getrlimit(RLIMIT_NOFILE, &rlp) == -1) {
exit(-1);
}
_maxsd = rlp.rlim_cur;
_fds = (struct s_fds *)malloc(sizeof(struct s_fds) * _maxsd);
if ((_sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
exit(-1);
}
sin.sin_port = htons(_port);
sin.sin_addr.s_addr = inet_addr(_addr);
sin.sin_family = AF_INET;
optval = 1;
if ((setsockopt(_sd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int))) == -1) {
exit(-1);
}
if (bind(_sd, (struct sockaddr *)&sin, sizeof(struct sockaddr)) == -1) {
exit(-1);
}
if (listen(_sd, _maxsd) == -1) {
exit(-1);
}
_fds[_sd].type = FD_SERV;
_fds[_sd].fct_read = &ClassSocket::acceptclient;
}
Du coup dans un premier temps je récupère la limite max du nombre de socket que je pourrais utilise sur le serveur.
Via la fonction getrlimit avec le flag RLIMIT_NOFILE
struct rlimit rlp;
if ((_maxsd = getrlimit(RLIMIT_NOFILE, &rlp)) == -1) {
printf("getrlimit ERROR\n");
exit(-1);
}
printf("limite souple:%d\n", rlp.rlim_cur);
printf("limite stricte (plafond de rlim_cur):%d\n", rlp.rlim_cur);
Description fonction getrlimit
getrlimit() et setrlimit() lisent ou écrivent les limites des ressources systèmes. Chaque ressource a une limite souple et une limite stricte définies par la structure rlimit (l'argument rlim de getrlimit() et setrlimit()) :
RLIMIT_NOFILE Le nombre maximal de descripteurs de fichier qu'un processus peut ouvrir simultanément. Les tentatives d'ouverture (open(2), pipe(2), dup(2), etc) dépassant cette limite renverront l'erreur EMFILE.
man getrlimitEnsuite, je fais une allocation dynamique avec malloc sur ma structure de socket qui me permettra de gérer les utilisateurs connectés.
man mallocJe fais une allocation dynamique du nombre maximum renvoyez par getrlimit qui sera mon maximum utilisateur possible sur le serveur.
struct s_fds
{
//# define FD_FREE 0 FD disponible
//# define FD_SERV 1 FD du serveur
//# define FD_CLIENT 2 FD d'un client
int type;
//Pointeur sur fonction read (lecture)
void (*fct_read)(int);
//Pointeur sur fonction write (ecriture)
void (*fct_write)(int);
//Buffer read
char buf_read[BUF_SIZE + 1];
//Buffer write
char buf_write[BUF_SIZE + 1];
};
_fds = (struct s_fds *)malloc(sizeof(struct s_fds) * _maxsd);
Penser a free toutes vos allocations fait avec malloc pour éviter des Leaks.
free(_fds);
_fds = NULL;
man free
Penser fermer vos FD (file descriptor/socket descriptor) une fois utilisation finie.
close(_sd);
man close
Du coup j'ouvre la socket principal du serveur via la fonction socket.
Afin avoir le socket descriptor principal du serveur.
if ((_sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
exit(-1);
}
Description fonction socket
socket() crée un point de communication, et renvoie un descripteur.
AF_INET Protocoles Internet IPv4.
SOCK_STREAM Support de dialogue garantissant l'intégrité, fournissant un flux de données binaires, et intégrant un mécanisme pour les transmissions de données hors-bande.
man socketsetsockopt() permet de pouvoir reutiliser une adresse local.
if ((setsockopt(_sd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int))) == -1) {
exit(-1);
}
Description fonction setsockopt
Les appels système getsockopt() et setsockopt() manipulent les options associées à une socket. Des options peuvent exister à plusieurs niveaux de protocole ; ils sont toujours présents au niveau "socket" le plus élevé.
Lors de la manipulation d'options de socket, le niveau auquel l'option réside et le nom de l'option doivent être spécifiés. Pour manipuler les options au niveau du socket, le niveau est spécifié comme SOL_SOCKET. Pour manipuler des options à tout autre niveau, le numéro de protocole du protocole approprié contrôlant l'option est fourni. Par exemple, pour indiquer qu'une option doit être interprétée par le protocole TCP, le niveau doit être défini sur numéro de protocole de TCP ; voir getprotoent(3).
SO_REUSEADDR Permet la réutilisation des adresses locales
man setsockoptJ'ajoute le port ainsi que address choisie aupres de ma structure sin.
AF_INET pour IPV4
man htonsman inet_addr
struct sockaddr_in sin;
sin.sin_port = htons(_port);
sin.sin_addr.s_addr = inet_addr(_addr);
sin.sin_family = AF_INET;
if (bind(_sd, (struct sockaddr *)&sin, sizeof(struct sockaddr)) == -1) {
exit(-1);
}
Description fonction bind
Quand une socket est créée avec l'appel système socket(2), elle existe dans l'espace des noms mais n'a pas de nom assigné). bind() affecte l'adresse spécifiée dans addr à la socket référencée par le descripteur de fichier sockfd. addrlen indique la taille, en octets, de la structure d'adresse pointée par addr. Traditionnellement cette opération est appelée « affectation d'un nom à une socket ».
man bindJ'appel la fonction listen avec le socket descriptor renvoyez par la fonction socket.
Et J'appel la fonction aussi avec le nombre maximun de socket descriptor renvoyez par la fonction getrlimit.
if (listen(_sd, _maxsd) == -1) {
exit(-1);
}
Description fonction listen
listen() marque la socket référencée par sockfd comme une socket passive, c'est-à-dire comme une socket qui sera utilisée pour accepter les demandes de connexions entrantes en utilisant accept(2).
man listenEnsuite j'enregistre ce socket descriptor dans la scucture de socket.
Et je lui donne FD_SERV afin de savoir que ce socket descriptor est utilise par le serveur.
Ensuite le lui donne une pointeur sur fonction du acceptclient qui sera appeler lors de la connection d'un nouveaux client.
_fds[_sd].type = FD_SERV;
_fds[_sd].fct_read = &ClassSocket::acceptclient;
Ensuite passons a la main loop qui nous permettra de gérer les connexions ainsi que la lecture des message envoyer par les clients.
Il y aura 3 étapes sur la main loop.
void ClassSocket::mainloop(void)
{
while (1)
{
initfd();
do_select();
check_fd();
}
}
initfd()
Initialisation des FD par defaults
void ClassSocket::initfd(void)
{
int i;
i = 0;
FD_ZERO(&_writefd);
FD_ZERO(&_readfd);
while (i < _maxsd)
{
if (_fds[i].type != FD_FREE)
{
FD_SET(i, &_readfd);
FD_SET(i, &_writefd);
}
++i;
}
}
man FD_ZERO et FD_SET
do_select()
Les fonctions select() et pselect() permettent à un programme de surveiller plusieurs descripteurs de fichier, attendant qu'au moins l'un des descripteurs de fichier devienne « prêt » pour certaines classes d'opérations d'entrées-sorties (par exemple, entrée possible). Un descripteur de fichier est considéré comme prêt s'il est possible d'effectuer l'opération d'entrées-sorties correspondante (par exemple, un read(2)) sans bloquer.
void ClassSocket::do_select(void)
{
struct timeval tv;
tv.tv_sec = 1;
_r = select(_sd + 1, &_readfd, &_writefd, NULL, &tv);
}
man select
check_fd()
Permet de vérifier s'il y a une écriture ou lecture en cours a traite.
void ClassSocket::check_fd(void)
{
int i = 0;
while ((i < _maxsd) && (_r > 0))
{
if (FD_ISSET(i, &_readfd)) {
_fds[i].fct_read(i);
}
if (FD_ISSET(i, &_writefd)) {
_fds[i].fct_write(i);
}
if (FD_ISSET(i, &_readfd) || FD_ISSET(i, &_writefd)) {
_r--;
}
i++;
}
}
acceptclient
void ClassSocket::acceptclient(int i)
{
int cs;
struct sockaddr_in csin;
socklen_t csin_len;
(void)i;
csin_len = sizeof(csin);
if ((cs = accept(_sd, (struct sockaddr *)&csin, &csin_len)) == -1)
{
exit(-1);
}
ClassSocket::cleanclient(&_fds[cs]);
if (_client >= MAX_USER)
{
close(cs);
return ;
}
_fds[cs].type = FD_CLIENT;
_fds[cs].fct_read = &ClassSocket::clientread;
_fds[cs].fct_write = &ClassSocket::clientwrite;
_client++;
send(cs, "hey\n", 4, 0);
thread t1(thread_write, cs);
t1.detach();
}
Du coup lors arrive une nouveaux client qui ce connect a mon serveur.
Il passera par acceptclient et la valeur de i sera le socket descriptor.
Ensuite j'utilise cleanclient afin de pouvoir mettre par default les valeur de ma structure de socket.
Ensuite, je donne le type à ma structure de socket pour savoir que c'est un client FD_CLIENT et ensuite, je le lui donne les pointeurs sur fonction pour écriture et la lecture.
Pour écriture, tout dépendra de la gestion du serveur qui soit synchrone ou asynchrone.
Du coup la il est asynchrone.
Du coup je créer un thread pour ce serveur pour chaque client afin qui puisse répondre tous les clients tous les 1 secondes avec le temps ou le serveur est installer (ou qui est cours de lancement).