Différences entre versions de « C socket »
(58 versions intermédiaires par 2 utilisateurs non affichées) | |||
Ligne 7 : | Ligne 7 : | ||
Il y a deux types de sockets, une qui est démarrée par la partie serveur en ''écoute'' et l'autre démarrée par la partie cliente qui se connecte à la première. | Il y a deux types de sockets, une qui est démarrée par la partie serveur en ''écoute'' et l'autre démarrée par la partie cliente qui se connecte à la première. | ||
− | Ci-contre une image résumant les différentes étapes pour arriver à l' | + | Ci-contre une image résumant les différentes étapes pour arriver à l'envoi de données. |
|| | || | ||
[[Fichier:Socket workflow.png|centré|200px]] | [[Fichier:Socket workflow.png|centré|200px]] | ||
Ligne 26 : | Ligne 26 : | ||
**SOCK_DGRAM: UDP | **SOCK_DGRAM: UDP | ||
**SOCK_RAW: socket à l'état brut (bas niveau) | **SOCK_RAW: socket à l'état brut (bas niveau) | ||
− | *protocol → valeur du champ ''protocol'' de l' | + | *protocol → valeur du champ ''protocol'' de l'en-tête de niveau 3 (généralement ''0'') |
− | *la valeur de retour est le fichier descripteur de la socket ou ''-1'' en cas d'erreur (''errno'' est | + | *la valeur de retour est le fichier descripteur de la socket ou ''-1'' en cas d'erreur (''errno'' est positionnée) |
Ligne 41 : | Ligne 41 : | ||
* optname → le nom de l'option (''SO_REUSEADDR'', ''SO_REUSEPORT'', ''SO_KEEPALIVE'', ...) | * optname → le nom de l'option (''SO_REUSEADDR'', ''SO_REUSEPORT'', ''SO_KEEPALIVE'', ...) | ||
* optval, optlen → utilisé pour accéder aux options de la socket; | * optval, optlen → utilisé pour accéder aux options de la socket; | ||
− | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est | + | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est positionnée) |
Il existe plusieurs options de socket, les plus utilisées étant : | Il existe plusieurs options de socket, les plus utilisées étant : | ||
* ''SO_REUSEADDR'' → permet de réutiliser l'adresse de la socket tout de suite même si cette dernière est dans l'état ''wait''; | * ''SO_REUSEADDR'' → permet de réutiliser l'adresse de la socket tout de suite même si cette dernière est dans l'état ''wait''; | ||
Ligne 66 : | Ligne 66 : | ||
</source> | </source> | ||
* addrlen → la taille de l'objet addr; | * addrlen → la taille de l'objet addr; | ||
− | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est | + | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est positionnée) |
Ligne 75 : | Ligne 75 : | ||
* sockfd → le fichier descripteur de la socket; | * sockfd → le fichier descripteur de la socket; | ||
* backlog → taille maximum de la file d'attente de la socket après laquelle le système répond avec ''ECONNREFUSED''; | * backlog → taille maximum de la file d'attente de la socket après laquelle le système répond avec ''ECONNREFUSED''; | ||
− | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est | + | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est positionnée) |
Ligne 85 : | Ligne 85 : | ||
* addr → l'adresse du client; | * addr → l'adresse du client; | ||
* addrlen → la taille de la structure addr; | * addrlen → la taille de la structure addr; | ||
− | * le code retour varie entre un entier positif qui correspond au descripteur de la socket cliente et ''-1'' en cas d'erreur (''errno'' est | + | * le code retour varie entre un entier positif qui correspond au descripteur de la socket cliente et ''-1'' en cas d'erreur (''errno'' est positionnée) |
+ | |||
== Côté client == | == Côté client == | ||
Après avoir créé la socket avec ''socket'' on peut se connecter à la partie serveur: | Après avoir créé la socket avec ''socket'' on peut se connecter à la partie serveur: | ||
Ligne 94 : | Ligne 95 : | ||
* addr → une structure symbolisant l'adresse (cf. ci-dessus) | * addr → une structure symbolisant l'adresse (cf. ci-dessus) | ||
* addrlen → la taille de l'objet addr; | * addrlen → la taille de l'objet addr; | ||
− | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est | + | * le code retour varie entre ''0'' et ''-1'' en cas d'erreur (''errno'' est positionnée) |
= Lecture / écriture = | = Lecture / écriture = | ||
Ligne 107 : | Ligne 108 : | ||
</source> | </source> | ||
* sockfd → le fichier descripteur de la socket; | * sockfd → le fichier descripteur de la socket; | ||
− | * buf → le tableau de caractères | + | * buf → le tableau de caractères où mettre le message reçu; |
* count → le nombre de caractères à recevoir; | * count → le nombre de caractères à recevoir; | ||
− | * le code retour correspond au nombre de caractères | + | * le code retour correspond au nombre de caractères reçus ou ''-1'' si une erreur survient (errno est positionné); |
+ | |||
===Écriture=== | ===Écriture=== | ||
Pour écrire dans une socket ou peut utiliser ''write'': | Pour écrire dans une socket ou peut utiliser ''write'': | ||
Ligne 120 : | Ligne 122 : | ||
* buf → le tableau de caractères contenant le message à envoyer; | * buf → le tableau de caractères contenant le message à envoyer; | ||
* count → le nombre de caractères à envoyer; | * count → le nombre de caractères à envoyer; | ||
− | * le code retour correspond au nombre de caractères | + | * le code retour correspond au nombre de caractères envoyés ou ''-1'' si une erreur survient (errno est positionné); |
+ | |||
===Modification=== | ===Modification=== | ||
Pour modifier un descripteur fichier de socket on utilise ''fcntl'': | Pour modifier un descripteur fichier de socket on utilise ''fcntl'': | ||
Ligne 131 : | Ligne 134 : | ||
* sockfd → le fichier descripteur de la socket; | * sockfd → le fichier descripteur de la socket; | ||
* cmd → la commande de modification (descripteur, drapeaux, ...); | * cmd → la commande de modification (descripteur, drapeaux, ...); | ||
− | * args → les arguments | + | * args → les arguments correspondant à la commande |
− | Cette commande va nous permettre de modifier le | + | Cette commande va nous permettre de modifier le drapeau du descripteur de la socket (''F_SETFL'') pour le rendre non bloquant (''O_NONBLOCK'') ! |
== Fonctions spécifiques == | == Fonctions spécifiques == | ||
− | Il existe des fonctions spécifiques pour manipuler les sockets. Ces fonctions | + | Il existe des fonctions spécifiques pour manipuler les sockets. Ces fonctions ressemblent aux précédentes mais acceptent des drapeaux en plus pour pouvoir modifier le descripteur ''à la volée''. |
===Lecture=== | ===Lecture=== | ||
Pour lire dans une socket on peut utiliser ''recv'': | Pour lire dans une socket on peut utiliser ''recv'': | ||
Ligne 145 : | Ligne 148 : | ||
</source> | </source> | ||
* sockfd → le fichier descripteur de la socket; | * sockfd → le fichier descripteur de la socket; | ||
− | * buf → le tableau de caractères | + | * buf → le tableau de caractères où mettre le message reçu; |
* count → le nombre de caractères à recevoir; | * count → le nombre de caractères à recevoir; | ||
* flags → liste de drapeaux (eg. ''O_NONBLOCK''); | * flags → liste de drapeaux (eg. ''O_NONBLOCK''); | ||
− | * le code retour correspond au nombre de caractères | + | * le code retour correspond au nombre de caractères reçus ou ''-1'' si une erreur survient (errno est positionné); |
+ | |||
===Écriture=== | ===Écriture=== | ||
Pour écrire dans une socket ou peut utiliser ''send'': | Pour écrire dans une socket ou peut utiliser ''send'': | ||
Ligne 154 : | Ligne 158 : | ||
#include <unistd.h> | #include <unistd.h> | ||
− | ssize_t send(int fd, const void *buf, size_t count); | + | ssize_t send(int fd, const void *buf, size_t count, int flags); |
</source> | </source> | ||
* sockfd → le fichier descripteur de la socket; | * sockfd → le fichier descripteur de la socket; | ||
Ligne 160 : | Ligne 164 : | ||
* count → le nombre de caractères à envoyer; | * count → le nombre de caractères à envoyer; | ||
* flags → liste de drapeaux (eg. ''MSG_CONFIRM'', ''MSG_DONTWAIT '', ...); | * flags → liste de drapeaux (eg. ''MSG_CONFIRM'', ''MSG_DONTWAIT '', ...); | ||
+ | * le code retour correspond au nombre de caractères envoyés ou ''-1'' si une erreur survient (errno est positionné); | ||
+ | |||
+ | = Cas d'utilisation = | ||
+ | == Création == | ||
+ | Pour commencer, il faut créer le descripteur de fichier: | ||
+ | <source lang="c"> | ||
+ | int fdsocket; | ||
+ | if ((fdsocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { | ||
+ | printf("Echéc de la création: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | </source> | ||
+ | == Paramétrage des options == | ||
+ | Il faut maintenant configurer la réutilisation de l'adresse et du port | ||
+ | <source lang="c"> | ||
+ | int opt = 1; | ||
+ | if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) != 0) { | ||
+ | printf("Echéc de paramètrage: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | </source> | ||
+ | == Attachement == | ||
+ | On peut maintenant attacher la socket à un port et une adresse: | ||
+ | <source lang="c"> | ||
+ | struct sockaddr_in adresse; | ||
+ | |||
+ | adresse.sin_family = AF_INET; | ||
+ | // Ecoute sur toutes les adresses (INADDR_ANY <=> 0.0.0.0) | ||
+ | adresse.sin_addr.s_addr = INADDR_ANY; | ||
+ | // Conversion du port en valeur réseaux (Host TO Network Short) | ||
+ | adresse.sin_port = htons(8080); | ||
+ | |||
+ | if (bind(fdsocket, (struct sockaddr *) &adresse, sizeof(adresse)) != 0) { | ||
+ | printf("Echéc d'attachement: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | == Mise en écoute == | ||
+ | La socket est prête à passer à l'écoute des connexions des clients: | ||
+ | <source lang="c"> | ||
+ | // Taille de la file d'attente | ||
+ | #define BACKLOG 3 | ||
+ | |||
+ | if (listen(fdsocket, BACKLOG) != 0) { | ||
+ | printf("Echéc de la mise en écoute: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | </source> | ||
+ | == Acceptation des connexions == | ||
+ | Il est possible d'accepter les nouvelles connexions: | ||
+ | <source lang="c"> | ||
+ | int clientSocket; | ||
+ | // Structure contenant l'adresse du client | ||
+ | struct sockaddr_in clientAdresse; | ||
+ | unsigned int addrLen = sizeof(clientAdresse); | ||
+ | if ((clientSocket = accept(fdsocket, (struct sockaddr *) &clientAdresse, &addrLen)) != -1) { | ||
+ | // Convertion de l'IP en texte | ||
+ | char ip[INET_ADDRSTRLEN]; | ||
+ | inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN); | ||
+ | printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port); | ||
+ | } | ||
+ | </source> | ||
+ | Il est possible d'utiliser ''fcntl'' pour passer le descripteur de la socket en non bloquant, avant l'appel à la fonction ''accept''. Si le descripteur reste bloquant, la fonction ''accept'' ne rendra pas la main tant qu'un client ne se sera connecté, ayant pour résultat le blocage complet du programme ! | ||
+ | <source lang="c"> | ||
+ | // Passage en mode non bloquant | ||
+ | fcntl(fdsocket, F_SETFL, O_NONBLOCK); | ||
+ | </source> | ||
+ | On peut remarquer, au passage, l'utilisation de ''inet_ntop'' pour convertir l'adresse du client du format binaire au format text. | ||
+ | |||
+ | == Lecture / Écriture == | ||
+ | === Mode connecté (TCP) === | ||
+ | On peut maintenant utiliser le descripteur de la socket du client pour lire et écrire. | ||
+ | * avec les fonctions génériques: | ||
+ | <source lang="c"> | ||
+ | #define BUFFER_LEN 200 | ||
+ | // Descripteur de la socket du client | ||
+ | int clientSocket; | ||
+ | char buffer[BUFFER_LEN]; | ||
+ | // Passage en mode non bloquant, sinon read attend | ||
+ | fcntl(clientSocket, F_SETFL, O_NONBLOCK); | ||
+ | int len = read(clientSocket, buffer, BUFFER_LEN); | ||
+ | |||
+ | write(clientSocket, "Coucou\n", strlen("Coucou\n")); | ||
+ | </source> | ||
+ | * avec les fonctions spécifiques: | ||
+ | <source lang="c"> | ||
+ | #define BUFFER_LEN 200 | ||
+ | // Descripteur de la socket du client | ||
+ | int clientSocket; | ||
+ | char buffer[BUFFER_LEN]; | ||
+ | |||
+ | int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT); | ||
+ | |||
+ | send(clientSocket, "Coucou\n", strlen("Coucou\n"), MSG_DONTWAIT); | ||
+ | </source> | ||
+ | |||
+ | Analysons le retour de la fonction ''read'' ou ''recv'' lorsque l'on utilise un descripteur non bloquant d'après la [http://man7.org/linux/man-pages/man2/recvmsg.2.html documentation] : | ||
+ | <pre> | ||
+ | NAME | ||
+ | recv, recvfrom, recvmsg - receive a message from a socket | ||
+ | [...] | ||
+ | MSG_DONTWAIT (since Linux 2.2) | ||
+ | Enables nonblocking operation; if the operation would block, the call fails with the error EAGAIN or EWOULDBLOCK (this can also be enabled using the O_NONBLOCK flag with the F_SETFL fcntl(2)). | ||
+ | [...] | ||
+ | RETURN VALUE | ||
+ | [...] | ||
+ | These calls return the number of bytes received, or -1 if an error | ||
+ | occurred. In the event of an error, errno is set to indicate the | ||
+ | error. | ||
+ | When a stream socket peer has performed an orderly shutdown, the | ||
+ | return value will be 0 (the traditional "end-of-file" return). | ||
+ | [...] | ||
+ | </pre> | ||
+ | Le retour est : | ||
+ | * lorsque le descripteur est vide (aucune donnée de la part du client) ''-1'' et ''errno'' sera positionné à la valeur ''EAGAIN'', il ne faut donc pas fermer la socket si le retour est ''-1'' ! Il faut d'abord vérifier que ''errno'' '''n'a pas''' la valeur ''EAGAIN''; | ||
+ | * lorsque le retour est ''0'', cela signifie que l'extrémité est fermée, il faut donc fermer le descripteur local; | ||
+ | * lorsque la valeur est supérieure à ''0'' elle correspond au nombre d'octets reçus. | ||
+ | |||
+ | On peut donc en déduire le morceau de code suivant pour gérer le retour de ''read'' ou ''recv'': | ||
+ | <source lang="c"> | ||
+ | int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT); | ||
+ | |||
+ | if (len == -1 && errno != EAGAIN) { | ||
+ | // Une erreur est survenue | ||
+ | } else if (len == 0) { | ||
+ | // Le client s'est déconnecté (extrémité de la socket fermée) | ||
+ | } else if (len > 0) { | ||
+ | // Le client à envoyé des données | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | === Mode datagramme (UDP) === | ||
+ | ===Lecture=== | ||
+ | Pour lire dans une socket on peut utiliser ''recvfrom'': | ||
+ | <source lang="c"> | ||
+ | #include <unistd.h> | ||
+ | |||
+ | ssize_t recvfrom(int fd, void *buf, size_t count, int flags, const struct sockaddr *addr, socklen_t addrlen); | ||
+ | </source> | ||
+ | * sockfd → le fichier descripteur de la socket; | ||
+ | * buf → le tableau de caractères où mettre le message reçu; | ||
+ | * count → le nombre de caractères à recevoir; | ||
+ | * flags → liste de drapeaux (eg. ''O_NONBLOCK''); | ||
+ | * addr → une structure symbolisant l'adresse du client (cf. ci-dessus) | ||
+ | * addrlen → la taille de l'objet addr; | ||
+ | * le code retour correspond au nombre de caractères reçus ou ''-1'' si une erreur survient (errno est positionné); | ||
+ | |||
+ | ===Écriture=== | ||
+ | Pour écrire dans une socket ou peut utiliser ''sendto'': | ||
+ | <source lang="c"> | ||
+ | #include <unistd.h> | ||
+ | |||
+ | ssize_t sendto(int fd, const void *buf, size_t count, const struct sockaddr *addr, socklen_t addrlen); | ||
+ | </source> | ||
+ | * sockfd → le fichier descripteur de la socket; | ||
+ | * buf → le tableau de caractères contenant le message à envoyer; | ||
+ | * count → le nombre de caractères à envoyer; | ||
+ | * flags → liste de drapeaux (eg. ''MSG_DONTWAIT '', ...); | ||
+ | * addr → une structure symbolisant l'adresse du client (cf. ci-dessus) | ||
+ | * addrlen → la taille de l'objet addr; | ||
* le code retour correspond au nombre de caractères envoyer ou ''-1'' si une erreur survient (errno est positionné); | * le code retour correspond au nombre de caractères envoyer ou ''-1'' si une erreur survient (errno est positionné); | ||
= Exemples = | = Exemples = | ||
− | == Serveur | + | == Serveur TCP == |
− | Voici un exemple de serveur ''echo'' qui renvoie le message au client. | + | Voici un exemple de serveur ''echo'' qui renvoie le message au client. Les connexions utilisent TCP et un simple client comme telnet suffit pour l'utiliser. |
<source lang="c"> | <source lang="c"> | ||
#include <unistd.h> | #include <unistd.h> | ||
Ligne 174 : | Ligne 339 : | ||
#include <errno.h> | #include <errno.h> | ||
#include <arpa/inet.h> | #include <arpa/inet.h> | ||
+ | #include <fcntl.h> | ||
// Port d'écoute de la socket | // Port d'écoute de la socket | ||
Ligne 181 : | Ligne 347 : | ||
// Taille de la file d'attente | // Taille de la file d'attente | ||
#define BACKLOG 3 | #define BACKLOG 3 | ||
− | // | + | // Nombre de connexions clients |
− | # define | + | #define NB_CLIENTS 2 |
// Taille du tampon de lecture des messages | // Taille du tampon de lecture des messages | ||
#define BUFFER_LEN 200 | #define BUFFER_LEN 200 | ||
Ligne 191 : | Ligne 357 : | ||
int initSocket(struct sockaddr_in * adresse); | int initSocket(struct sockaddr_in * adresse); | ||
int waitForClient(int * serverSocket); | int waitForClient(int * serverSocket); | ||
+ | void addClientToTab(int clientSocket, int clients[]); | ||
+ | void manageClient(int clients[]); | ||
int main(void) { | int main(void) { | ||
+ | // Création et initialisation du tableau contenant les descripteurs des sockets clients | ||
+ | int clients[NB_CLIENTS]; | ||
+ | for (int i = 0; i < NB_CLIENTS; i++) { | ||
+ | clients[i] = -1; | ||
+ | } | ||
// Structure contenant l'adresse | // Structure contenant l'adresse | ||
struct sockaddr_in adresse; | struct sockaddr_in adresse; | ||
Ligne 198 : | Ligne 371 : | ||
// Descripteur de la socket du serveur | // Descripteur de la socket du serveur | ||
int serverSocket = initSocket(&adresse); | int serverSocket = initSocket(&adresse); | ||
+ | int clientSocket; | ||
while (1) { | while (1) { | ||
// Descripteur de la socket du client, on attend une connexion | // Descripteur de la socket du client, on attend une connexion | ||
− | + | if ((clientSocket = waitForClient(&serverSocket)) != -1) { | |
− | // | + | // On ajoute le nouveau client au tableau des descripteurs |
− | + | addClientToTab(clientSocket, clients); | |
− | char buffer[BUFFER_LEN] = | + | } |
− | + | manageClient(clients); | |
− | + | } | |
+ | return EXIT_SUCCESS; | ||
+ | } | ||
+ | // Initialisation de la structure sockaddr_in | ||
+ | void initAdresse(struct sockaddr_in * adresse) { | ||
+ | (*adresse).sin_family = AF_INET; | ||
+ | (*adresse).sin_addr.s_addr = IP; | ||
+ | (*adresse).sin_port = htons( PORT); | ||
+ | } | ||
+ | // Démarrage de la socket serveur | ||
+ | int initSocket(struct sockaddr_in * adresse) { | ||
+ | // Descripteur de socket | ||
+ | int fdsocket; | ||
+ | // Nombre d'options | ||
+ | int opt = 1; | ||
+ | // Création de la socket en TCP | ||
+ | if ((fdsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) { | ||
+ | printf("Echéc de la création: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Création de la socket\n"); | ||
+ | // Paramètrage de la socket | ||
+ | if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, | ||
+ | sizeof(opt)) != 0) { | ||
+ | printf("Echéc de paramètrage: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Paramètrage de la socket\n"); | ||
+ | // Attachement de la socket sur le port et l'adresse IP | ||
+ | if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) { | ||
+ | printf("Echéc d'attachement: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Attachement de la socket sur le port %i\n", PORT); | ||
+ | // Passage en écoute de la socket | ||
+ | if (listen(fdsocket, BACKLOG) != 0) { | ||
+ | printf("Echéc de la mise en écoute: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | // Passage en mode non bloquant | ||
+ | fcntl(fdsocket, F_SETFL, O_NONBLOCK); | ||
+ | printf("Mise en écoute de la socket\n"); | ||
+ | return fdsocket; | ||
+ | } | ||
+ | // Attente de connexion d'un client | ||
+ | int waitForClient(int * serverSocket) { | ||
+ | int clientSocket; | ||
+ | // Structure contenant l'adresse du client | ||
+ | struct sockaddr_in clientAdresse; | ||
+ | int addrLen = sizeof(clientAdresse); | ||
+ | if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse, | ||
+ | (socklen_t*) &addrLen)) != -1) { | ||
+ | // Convertion de l'IP en texte | ||
+ | char ip[INET_ADDRSTRLEN]; | ||
+ | inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN); | ||
+ | printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port); | ||
+ | // Passage en mode non bloquant | ||
+ | int opt = 1; | ||
+ | setsockopt(clientSocket, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(1)); | ||
+ | } | ||
+ | return clientSocket; | ||
+ | } | ||
+ | // Ajoute les clients au tableau | ||
+ | void addClientToTab(int clientSocket, int clients[]) { | ||
+ | // On vérifie si on à de la place de libre | ||
+ | int found = -1; | ||
+ | for (int i = 0; i < NB_CLIENTS; i++) { | ||
+ | // On cherche de la place | ||
+ | if (clients[i] == -1) { | ||
+ | // On ajoute le client | ||
+ | printf("Ajout du client à l'index %i\n", i); | ||
+ | clients[i] = clientSocket; | ||
+ | // Nouvelle connexion, envoie du message de bienvenu | ||
+ | send(clientSocket, "Entrez 'exit' pour quitter\n", strlen("Entrez 'exit' pour quitter\n"), | ||
+ | MSG_DONTWAIT); | ||
+ | found = 0; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | if (found == -1) { | ||
+ | // On a plus de place de libre | ||
+ | puts("Plus de place, désolé"); | ||
+ | close(clientSocket); | ||
+ | } | ||
+ | } | ||
+ | // On traite l'input des clients | ||
+ | void manageClient(int clients[]) { | ||
+ | // Création d'un tampon pour stocker les messages des clients dans la heap | ||
+ | static char buffer[BUFFER_LEN + 1]; | ||
+ | for (int i = 0; i < NB_CLIENTS; i++) { | ||
+ | // On vérifie les messages | ||
+ | int clientSocket = clients[i]; | ||
+ | if (clientSocket == -1) { | ||
+ | continue; | ||
+ | } | ||
+ | int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT); | ||
+ | // Booléen pour suivre l'état de la socket | ||
+ | int isClosed = 0; | ||
+ | if (len == -1 && errno != EAGAIN) { | ||
+ | // Une erreur est survenue | ||
+ | printf("Errno [%i] : %s\n", errno, strerror(errno)); | ||
+ | isClosed = 1; | ||
+ | } else if (len == 0) { | ||
+ | // Le client s'est déconnecté (extrémité de la socket fermée) | ||
+ | isClosed = 1; | ||
+ | } else if (len > 0) { | ||
// Ajout du terminateur de chaîne | // Ajout du terminateur de chaîne | ||
buffer[len] = '\0'; | buffer[len] = '\0'; | ||
− | // On renvoie le texte au client | + | if (strncmp(buffer, EXIT_WORD, 4) == 0) { |
− | + | // Le client veut se déconnecter | |
− | + | send(clientSocket, "Bye\n", strlen("Bye\n"), 0); | |
+ | isClosed = 1; | ||
+ | } else { | ||
+ | // On renvoie le texte au client dans un buffer assez grand | ||
+ | int len = strlen("Vous avez dit : ") + strlen(buffer) + 1; | ||
+ | char response[len]; | ||
+ | strcpy(response, "Vous avez dit : "); | ||
+ | strcat(response, buffer); | ||
+ | // Un seul envoie permet de ne pas surcharger le réseau | ||
+ | send(clientSocket, response, strlen(response), 0); | ||
+ | } | ||
+ | } | ||
+ | if (isClosed == 1) { | ||
+ | // La socket est fermé ou le client veut quitter le serveur ! | ||
+ | printf("Fermeture de la connexion avec le client\n"); | ||
+ | // Fermeture de la socket | ||
+ | close(clientSocket); | ||
+ | // On fait de la place dans le tableau | ||
+ | clients[i] = -1; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | On peut interroger notre serveur avec ''telnet'' (''yum -y install telnet''): | ||
+ | <pre> | ||
+ | # telnet 127.0.0.1 8080 | ||
+ | Trying 127.0.0.1... | ||
+ | Connected to 127.0.0.1. | ||
+ | Escape character is '^]'. | ||
+ | Entrez 'exit' pour quitter | ||
+ | coucou | ||
+ | Vous avez dit : coucou | ||
+ | </pre> | ||
+ | |||
+ | == Serveur UDP == | ||
+ | Si on essaie de reproduire le même type de serveur mais cette fois-ci avec une socket en UDP, cela devient beaucoup plus simple. En effet, en UDP, plus besoin de tenir un tableau avec les descripteurs des sockets des clients car... on est en mode déconnecté (datagramme) ! Entendez par là que les clients envoient un message sans attendre de réponse et sans se soucier de savoir si le message est arrivé au serveur. | ||
+ | |||
+ | <source lang="c"> | ||
+ | #include <unistd.h> | ||
+ | #include <stdio.h> | ||
+ | #include <sys/socket.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <netinet/in.h> | ||
+ | #include <string.h> | ||
+ | #include <errno.h> | ||
+ | #include <arpa/inet.h> | ||
+ | #include <fcntl.h> | ||
+ | |||
+ | // Port d'écoute de la socket | ||
+ | #define PORT 8080 | ||
+ | // Adresse d'écoute (toutes les adresses) | ||
+ | #define IP INADDR_ANY | ||
+ | // Taille de la file d'attente | ||
+ | #define BACKLOG 3 | ||
+ | // Taille du tampon de lecture des messages | ||
+ | #define BUFFER_LEN 200 | ||
+ | |||
+ | void initAdresse(struct sockaddr_in * adresse); | ||
+ | int initSocket(struct sockaddr_in * adresse); | ||
+ | void manageClient(int serverSocket); | ||
+ | |||
+ | int main(void) { | ||
+ | // Structure contenant l'adresse | ||
+ | struct sockaddr_in adresse; | ||
+ | initAdresse(&adresse); | ||
+ | // Descripteur de la socket du serveur | ||
+ | int serverSocket = initSocket(&adresse); | ||
+ | while (1) { | ||
+ | // Descripteur de la socket du client, on attend une connexion | ||
+ | manageClient(serverSocket); | ||
+ | } | ||
+ | return EXIT_SUCCESS; | ||
+ | } | ||
+ | // Initialisation de la structure sockaddr_in | ||
+ | void initAdresse(struct sockaddr_in * adresse) { | ||
+ | (*adresse).sin_family = AF_INET; | ||
+ | (*adresse).sin_addr.s_addr = IP; | ||
+ | (*adresse).sin_port = htons( PORT); | ||
+ | } | ||
+ | // Démarrage de la socket serveur | ||
+ | int initSocket(struct sockaddr_in * adresse) { | ||
+ | // Descripteur de socket | ||
+ | int fdsocket; | ||
+ | // Nombre d'options | ||
+ | int opt = 1; | ||
+ | // Création de la socket en UDP | ||
+ | if ((fdsocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) { | ||
+ | printf("Echéc de la création: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Création de la socket\n"); | ||
+ | // Paramètrage de la socket | ||
+ | if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, | ||
+ | sizeof(opt)) != 0) { | ||
+ | printf("Echéc de paramètrage: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Paramètrage de la socket\n"); | ||
+ | // Attachement de la socket sur le port et l'adresse IP | ||
+ | if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) { | ||
+ | printf("Echéc d'attachement: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | printf("Attachement de la socket sur le port %i\n", PORT); | ||
+ | return fdsocket; | ||
+ | } | ||
+ | // On traite l'input des clients | ||
+ | void manageClient(int serverSocket) { | ||
+ | // Création d'un tampon pour stocker les messages des clients | ||
+ | static char buffer[BUFFER_LEN + 1]; | ||
+ | // Structure contenant l'adresse du client | ||
+ | struct sockaddr_in clientAdresse; | ||
+ | unsigned int addrLen = sizeof(clientAdresse); | ||
+ | // On vérifie si on a reçu un message | ||
+ | int len = recvfrom(serverSocket, buffer, BUFFER_LEN, MSG_DONTWAIT, | ||
+ | (struct sockaddr*) &clientAdresse, &addrLen); | ||
+ | if (len == -1 && errno != EAGAIN) { | ||
+ | printf("Problème de socket %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } else if (len > 0) { | ||
+ | printf("Message reçu de %s:%i\n", inet_ntoa(clientAdresse.sin_addr), | ||
+ | ntohs(clientAdresse.sin_port)); | ||
+ | // Ajout du terminateur de chaîne | ||
+ | buffer[len] = '\0'; | ||
+ | // On renvoie le texte au client dans un buffer assez grand | ||
+ | len = strlen("Vous avez dit : ") + strlen(buffer) + 1; | ||
+ | char response[len]; | ||
+ | strcpy(response, "Vous avez dit : "); | ||
+ | strcat(response, buffer); | ||
+ | // Un seul envoie permet de ne pas surcharger le réseau | ||
+ | sendto(serverSocket, response, strlen(response), MSG_DONTWAIT, | ||
+ | (struct sockaddr*) &clientAdresse, addrLen); | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | On peut interroger notre serveur avec ''netcat'' (''yum -y install nc''): | ||
+ | <pre> | ||
+ | # nc -u 127.0.0.1 8080 | ||
+ | coucou | ||
+ | Vous avez dit : coucou | ||
+ | </pre> | ||
+ | |||
+ | == Cas du multicast == | ||
+ | Il est intéressant parfois de s'abonner à un groupe multicast pour recevoir des messages. | ||
+ | |||
+ | Pour cela il suffit d'utiliser la structure ''mreq'': | ||
+ | <source lang="c"> | ||
+ | struct ip_mreq mreq; | ||
+ | mreq.imr_multiaddr.s_addr = inet_addr(ip); // ip correspond à l'adresse multicast eg. "235.0.0.1" | ||
+ | mreq.imr_interface.s_addr = htonl(INADDR_ANY); // cette ip correspond à l'adresse locale | ||
+ | </source> | ||
+ | |||
+ | Il faut ensuite positionner l'option sur le descripteur de fichier de la socket concernée: | ||
+ | <source lang="c"> | ||
+ | if (setsockopt(fdsocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*) &mreq, sizeof(mreq)) < 0) { | ||
+ | printf("Echéc de paramètrage: %s\n", strerror(errno)); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | = Notification évènementielle avec epoll = | ||
+ | Depuis la version 2.5 du noyau Linux il est possible d'utiliser une série de fonctions, appartenant à l'espace utilisateur (user-space), permettant de suivre l'état d'un descripteur de fichier. | ||
+ | La différence avec read, recv ou encore recvfrom c'est que l'on va pouvoir surveiller plusieurs descripteurs de fichiers en même temps, laissant le CPU libre pour effectuer autre chose ! | ||
+ | |||
+ | == Utilisation == | ||
+ | Tout d'abord il faut créer un descripteur de fichier de type ''epoll'': | ||
+ | <source lang="c"> | ||
+ | #include <sys/epoll.h> | ||
+ | |||
+ | int epoll_create1(int flags); | ||
+ | </source> | ||
+ | * flags → EPOLL_CLOEXEC pour éviter les situations de compétition en environnement parallélisé; | ||
+ | * le code retour correspond au descripteur ''epoll'' ou ''-1'' en cas d'erreur (''errno'' est positionnée). | ||
+ | |||
+ | Une fois le descripteur ''epoll'' créé, il faut lui associer un événement: | ||
+ | <source lang="c"> | ||
+ | #include <sys/epoll.h> | ||
+ | |||
+ | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); | ||
+ | </source> | ||
+ | * epfd → le descripteur ''epoll'' à modifier; | ||
+ | * op → une opération parmi: | ||
+ | ** EPOLL_CTL_ADD → pour ajouter un événement sur un descripteur de fichier; | ||
+ | ** EPOLL_CTL_MOD → pour modifier un événement sur un descripteur de fichier; | ||
+ | ** EPOLL_CTL_DEL → pour supprimer un événement sur un descripteur de fichier; | ||
+ | * fd → un descripteur de fichier à surveiller; | ||
+ | * event → la structure contenant l'événement à surveiller (''EPOLLIN'', ''EPOLLOUT'', EPOLLRDHUP'', ...); | ||
+ | * le code retour varie entre ''0'' en cas de succès ou ''-1'' en cas d'erreur (''errno'' est positionnée). | ||
+ | |||
+ | Voici la structure ''epoll_event'': | ||
+ | <source lang="c"> | ||
+ | #include <sys/epoll.h> | ||
+ | |||
+ | struct epoll_event { | ||
+ | uint32_t events; /* Epoll events */ | ||
+ | epoll_data_t data; /* User data variable */ | ||
+ | }; | ||
+ | |||
+ | typedef union epoll_data { | ||
+ | void *ptr; | ||
+ | int fd; | ||
+ | uint32_t u32; | ||
+ | uint64_t u64; | ||
+ | } epoll_data_t; | ||
+ | </source> | ||
+ | |||
+ | Enfin, il ne reste plus qu'à ''attendre'' des événements: | ||
+ | <source lang="c"> | ||
+ | #include <sys/epoll.h> | ||
+ | |||
+ | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); | ||
+ | </source> | ||
+ | * epfd → le descripteur ''epoll'' à modifier; | ||
+ | * events → un tableau de ''epoll_event contenant les événements; | ||
+ | * maxevents → le nombre maximum d'événements à traiter (selon la taille du tableau); | ||
+ | * timeout → le temps, en millisecondes que la fonction attend: | ||
+ | ** ''-1'' → la fonction devient bloquante; | ||
+ | ** ''0'' → retourne immédiatement; | ||
+ | ** ''> 0'' → attend le nombre de millisecondes spécifiées; | ||
+ | * le code retour correspond au nombre d'événements ou ''-1'' en cas d'erreur (''errno'' est positionnée). | ||
+ | |||
+ | == Améliorations architecturales == | ||
+ | === Gestion des utilisateurs=== | ||
+ | Tout d'abord, le fait de laisser ''epoll'' gérer les descripteurs de socket nous permet de supprimer le tableau ''clients'' ainsi que la variable ''NB_CLIENTS''. En effet, la référence vers le descripteur est maintenue par ''epoll'' et nous pouvons gérer maintenant un nombre ''illimité'' de clients. | ||
+ | |||
+ | === Performances === | ||
+ | La boucle ''while'' de notre serveur consomme énormément de CPU, environ 100% de CPU par client ! On aurait pu utiliser un temporisateur avec ''nanosleep'' pour baisser la consommation CPU entre 5% et 10%... | ||
+ | Le fait de basculer sur une fonction bloquante comme ''epoll_wait'' qui utilise des signaux (interruptions) permet de réduire considérablement la consommation CPU. Gardez à l'esprit qu'il n'y a rien de mieux que les interruptions, la preuve, lorsque l'on fait un ''top'', notre programme a complètement disparu de la liste ! | ||
+ | |||
+ | == Modification du serveur == | ||
+ | Modifions le code du serveur TCP pour y intégrer ''epoll'': | ||
+ | <source lang="c"> | ||
+ | #include <unistd.h> | ||
+ | #include <stdio.h> | ||
+ | #include <sys/socket.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <netinet/in.h> | ||
+ | #include <string.h> | ||
+ | #include <errno.h> | ||
+ | #include <arpa/inet.h> | ||
+ | #include <sys/epoll.h> | ||
+ | |||
+ | // Port d'écoute de la socket | ||
+ | #define PORT 8080 | ||
+ | // Adresse d'écoute (toutes les adresses) | ||
+ | #define IP INADDR_ANY | ||
+ | // Taille de la file d'attente | ||
+ | #define BACKLOG 3 | ||
+ | // Nombre de connexions clients | ||
+ | #define NB_EVENTS 2 | ||
+ | // Taille du tampon de lecture des messages | ||
+ | #define BUFFER_LEN 200 | ||
+ | // Commande pour arrêter le serveur | ||
+ | #define EXIT_WORD "exit" | ||
+ | |||
+ | void initAdresse(struct sockaddr_in * adresse); | ||
+ | int initSocket(struct sockaddr_in * adresse); | ||
+ | int waitForClient(int * serverSocket); | ||
+ | void addClientToTab(int * epfd, int * clientSocket); | ||
+ | void manageClient(int * epfd, int * clientSocket); | ||
+ | void addEpollEvent(int * epfd, int * socket); | ||
+ | void delEpollEvent(int * epfd, int * socket); | ||
+ | |||
+ | int main(void) { | ||
+ | // Création du descripteur EPOLL | ||
+ | int epfd = epoll_create1(0); | ||
+ | // Structure contenant les événements EPOLL | ||
+ | struct epoll_event events[NB_EVENTS]; | ||
+ | // Structure contenant l'adresse | ||
+ | struct sockaddr_in adresse; | ||
+ | initAdresse(&adresse); | ||
+ | // Descripteur de la socket du serveur | ||
+ | int serverSocket = initSocket(&adresse); | ||
+ | addEpollEvent(&epfd, &serverSocket); | ||
+ | int clientSocket; | ||
+ | while (1) { | ||
+ | int nfds = epoll_wait(epfd, events, NB_EVENTS, -1); | ||
+ | for (int i = 0; i < nfds; i++) { | ||
+ | if (events[i].data.fd == serverSocket) { | ||
+ | if ((clientSocket = waitForClient(&serverSocket)) != -1) { | ||
+ | // On ajoute le nouveau client au tableau des descripteurs | ||
+ | addClientToTab(&epfd, &clientSocket); | ||
+ | } | ||
+ | } else if (events[i].events & EPOLLIN) { | ||
+ | // On a reçu quelque chose | ||
+ | manageClient(&epfd, &events[i].data.fd); | ||
+ | } | ||
} | } | ||
− | |||
− | |||
− | |||
} | } | ||
+ | close(serverSocket); | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
Ligne 263 : | Ligne 827 : | ||
struct sockaddr_in clientAdresse; | struct sockaddr_in clientAdresse; | ||
int addrLen = sizeof(clientAdresse); | int addrLen = sizeof(clientAdresse); | ||
− | |||
if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse, | if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse, | ||
− | (socklen_t*) &addrLen)) | + | (socklen_t*) &addrLen)) != -1) { |
− | printf(" | + | // Convertion de l'IP en texte |
− | + | char ip[INET_ADDRSTRLEN]; | |
− | + | inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN); | |
+ | printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port); | ||
+ | // Passage en mode non bloquant | ||
+ | int opt = 1; | ||
+ | setsockopt(clientSocket, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(1)); | ||
} | } | ||
− | |||
− | |||
− | |||
− | |||
return clientSocket; | return clientSocket; | ||
+ | } | ||
+ | // Ajoute les clients au tableau | ||
+ | void addClientToTab(int * epfd, int * clientSocket) { | ||
+ | addEpollEvent(epfd, clientSocket); | ||
+ | // Nouvelle connexion, envoie du message de bienvenu | ||
+ | send(*clientSocket, "Entrez 'exit' pour quitter\n", | ||
+ | strlen("Entrez 'exit' pour quitter\n"), MSG_DONTWAIT); | ||
+ | } | ||
+ | // On traite l'input des clients | ||
+ | void manageClient(int * epfd, int * clientSocket) { | ||
+ | // Création d'un tampon pour stocker les messages des clients dans la heap | ||
+ | static char buffer[BUFFER_LEN + 1]; | ||
+ | int len = recv(*clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT); | ||
+ | // Booléen pour suivre l'état de la socket | ||
+ | int isClosed = 0; | ||
+ | if (len == -1 && errno != EAGAIN) { | ||
+ | // Une erreur est survenue | ||
+ | printf("Errno [%i] : %s\n", errno, strerror(errno)); | ||
+ | isClosed = 1; | ||
+ | } else if (len == 0) { | ||
+ | // Le client s'est déconnecté (extrémité de la socket fermée) | ||
+ | isClosed = 1; | ||
+ | } else if (len > 0) { | ||
+ | // Ajout du terminateur de chaîne | ||
+ | buffer[len] = '\0'; | ||
+ | if (strncmp(buffer, EXIT_WORD, 4) == 0) { | ||
+ | // Le client veut se déconnecter | ||
+ | send(*clientSocket, "Bye\n", strlen("Bye\n"), 0); | ||
+ | isClosed = 1; | ||
+ | } else { | ||
+ | // On renvoie le texte au client dans un buffer assez grand | ||
+ | int len = strlen("Vous avez dit : ") + strlen(buffer) + 1; | ||
+ | char response[len]; | ||
+ | strcpy(response, "Vous avez dit : "); | ||
+ | strcat(response, buffer); | ||
+ | // Un seul envoie permet de ne pas surcharger le réseau | ||
+ | send(*clientSocket, response, strlen(response), 0); | ||
+ | } | ||
+ | } | ||
+ | if (isClosed == 1) { | ||
+ | // On enlève la socket des événements EPOLL | ||
+ | delEpollEvent(epfd, clientSocket); | ||
+ | // La socket est fermé ou le client veut quitter le serveur ! | ||
+ | printf("Fermeture de la connexion avec le client\n"); | ||
+ | // Fermeture de la socket | ||
+ | close(*clientSocket); | ||
+ | } | ||
+ | } | ||
+ | // Ajoute un descripteur de socket aux événements EPOLL | ||
+ | void addEpollEvent(int * epfd, int * socket) { | ||
+ | printf("Ajout de la socket %i dans les événements EPOLL\n", *socket); | ||
+ | // Structure contenant les événements EPOLL | ||
+ | struct epoll_event event; | ||
+ | // Initialisation de la structure | ||
+ | memset(&event, 0, sizeof(struct epoll_event)); | ||
+ | // On enregistre la socket du serveur comme descripteur EPOLL | ||
+ | event.data.fd = *socket; | ||
+ | // On surveille les message entrant, la fermeture de la socket | ||
+ | event.events = EPOLLIN | EPOLLRDHUP; | ||
+ | epoll_ctl(*epfd, EPOLL_CTL_ADD, *socket, &event); | ||
+ | } | ||
+ | // Efface un descripteur de socket des événements EPOLL | ||
+ | void delEpollEvent(int * epfd, int * socket) { | ||
+ | printf("Suppression de la socket %i des événements EPOLL\n", *socket); | ||
+ | // Structure contenant les événements EPOLL | ||
+ | struct epoll_event event; | ||
+ | // Initialisation de la structure | ||
+ | memset(&event, 0, sizeof(struct epoll_event)); | ||
+ | // On enregistre la socket du serveur comme descripteur EPOLL | ||
+ | event.data.fd = *socket; | ||
+ | // On surveille les message entrant, la fermeture de la socket | ||
+ | event.events = EPOLLIN | EPOLLRDHUP; | ||
+ | epoll_ctl(*epfd, EPOLL_CTL_DEL, *socket, &event); | ||
} | } | ||
</source> | </source> | ||
− | + | Voici ce que l'on peut observer dans la console: | |
− | + | <pre> | |
+ | Création de la socket | ||
+ | Paramètrage de la socket | ||
+ | Attachement de la socket sur le port 8080 | ||
+ | Mise en écoute de la socket | ||
+ | Ajout de la socket 4 dans les événements EPOLL | ||
+ | Connexion de 127.0.0.1:17607 | ||
+ | Ajout de la socket 5 dans les événements EPOLL | ||
+ | Suppression de la socket 5 des événements EPOLL | ||
+ | Fermeture de la connexion avec le client | ||
+ | Connexion de 127.0.0.1:18119 | ||
+ | Ajout de la socket 5 dans les événements EPOLL | ||
+ | Connexion de 127.0.0.1:18631 | ||
+ | Ajout de la socket 6 dans les événements EPOLL | ||
+ | Suppression de la socket 5 des événements EPOLL | ||
+ | Fermeture de la connexion avec le client | ||
+ | </pre> |
Version actuelle datée du 30 mai 2020 à 16:32
Introduction
Les sockets permettent de connecter deux programmes qui s'exécutent sur deux machines différentes. Cette connexion se fait à travers le réseau grâce à l'utilisation d'un port (TCP ou UDP) et d'une adresse IP. Il y a deux types de sockets, une qui est démarrée par la partie serveur en écoute et l'autre démarrée par la partie cliente qui se connecte à la première. Ci-contre une image résumant les différentes étapes pour arriver à l'envoi de données. |
Création
Côté serveur
Tout d'abord il faut créer l'objet socket:
#include <sys/socket.h>
#include <netinet/in.h>
int socket(int domain, int type, int protocol)
- domain → integer, communication domain e.g., AF_INET (IPv4 protocol) , AF_INET6 (IPv6 protocol)
- type → type de communication
- SOCK_STREAM: TCP
- SOCK_DGRAM: UDP
- SOCK_RAW: socket à l'état brut (bas niveau)
- protocol → valeur du champ protocol de l'en-tête de niveau 3 (généralement 0)
- la valeur de retour est le fichier descripteur de la socket ou -1 en cas d'erreur (errno est positionnée)
Une fois le descripteur de socket créé, il est possible de le configurer:
#include <sys/socket.h>
#include <netinet/in.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
- sockfd → le fichier descripteur de la socket;
- level → niveau d'application de l'option (SOL_SOCKET, TCP, UDP, ...);
- optname → le nom de l'option (SO_REUSEADDR, SO_REUSEPORT, SO_KEEPALIVE, ...)
- optval, optlen → utilisé pour accéder aux options de la socket;
- le code retour varie entre 0 et -1 en cas d'erreur (errno est positionnée)
Il existe plusieurs options de socket, les plus utilisées étant :
- SO_REUSEADDR → permet de réutiliser l'adresse de la socket tout de suite même si cette dernière est dans l'état wait;
- SO_REUSEPORT → permet de réutiliser le port de la socket tout de suite même si cette dernière est dans l'état wait;
- SO_KEEPALIVE → envoie des informations périodiquement pour tester si l'extrémité du tunnel est toujours présente;
Il faut maintenant attacher la socket à un port Internet et une adresse IP:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
- sockfd → le fichier descripteur de la socket;
- addr → une structure symbolisant l'adresse
struct sockaddr_in {
short sin_family; // e.g: AF_INET
unsigned short sin_port; // e.g: htons(3490)
struct in_addr sin_addr; // détaillé ci-dessous
char sin_zero[8]; // '0' habituellement
};
struct in_addr {
unsigned long s_addr; // initialiser avec inet_aton()
};
- addrlen → la taille de l'objet addr;
- le code retour varie entre 0 et -1 en cas d'erreur (errno est positionnée)
On peut maintenant passer la socket en état d'écoute, elle est prête à recevoir des connexions:
int listen(int sockfd, int backlog)
- sockfd → le fichier descripteur de la socket;
- backlog → taille maximum de la file d'attente de la socket après laquelle le système répond avec ECONNREFUSED;
- le code retour varie entre 0 et -1 en cas d'erreur (errno est positionnée)
On peut maintenant attendre une connexion:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd → le fichier descripteur de la socket;
- addr → l'adresse du client;
- addrlen → la taille de la structure addr;
- le code retour varie entre un entier positif qui correspond au descripteur de la socket cliente et -1 en cas d'erreur (errno est positionnée)
Côté client
Après avoir créé la socket avec socket on peut se connecter à la partie serveur:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd → le fichier descripteur de la socket;
- addr → une structure symbolisant l'adresse (cf. ci-dessus)
- addrlen → la taille de l'objet addr;
- le code retour varie entre 0 et -1 en cas d'erreur (errno est positionnée)
Lecture / écriture
Fonctions génériques
Tout d'abord, on peut dire que la socket est un fichier et se manipule donc comme un fichier !
Lecture
Pour lire dans une socket on peut utiliser read:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères où mettre le message reçu;
- count → le nombre de caractères à recevoir;
- le code retour correspond au nombre de caractères reçus ou -1 si une erreur survient (errno est positionné);
Écriture
Pour écrire dans une socket ou peut utiliser write:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères contenant le message à envoyer;
- count → le nombre de caractères à envoyer;
- le code retour correspond au nombre de caractères envoyés ou -1 si une erreur survient (errno est positionné);
Modification
Pour modifier un descripteur fichier de socket on utilise fcntl:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int args */ );
- sockfd → le fichier descripteur de la socket;
- cmd → la commande de modification (descripteur, drapeaux, ...);
- args → les arguments correspondant à la commande
Cette commande va nous permettre de modifier le drapeau du descripteur de la socket (F_SETFL) pour le rendre non bloquant (O_NONBLOCK) !
Fonctions spécifiques
Il existe des fonctions spécifiques pour manipuler les sockets. Ces fonctions ressemblent aux précédentes mais acceptent des drapeaux en plus pour pouvoir modifier le descripteur à la volée.
Lecture
Pour lire dans une socket on peut utiliser recv:
#include <unistd.h>
ssize_t recv(int fd, void *buf, size_t count, int flags);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères où mettre le message reçu;
- count → le nombre de caractères à recevoir;
- flags → liste de drapeaux (eg. O_NONBLOCK);
- le code retour correspond au nombre de caractères reçus ou -1 si une erreur survient (errno est positionné);
Écriture
Pour écrire dans une socket ou peut utiliser send:
#include <unistd.h>
ssize_t send(int fd, const void *buf, size_t count, int flags);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères contenant le message à envoyer;
- count → le nombre de caractères à envoyer;
- flags → liste de drapeaux (eg. MSG_CONFIRM, MSG_DONTWAIT , ...);
- le code retour correspond au nombre de caractères envoyés ou -1 si une erreur survient (errno est positionné);
Cas d'utilisation
Création
Pour commencer, il faut créer le descripteur de fichier:
int fdsocket;
if ((fdsocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("Echéc de la création: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Paramétrage des options
Il faut maintenant configurer la réutilisation de l'adresse et du port
int opt = 1;
if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) != 0) {
printf("Echéc de paramètrage: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Attachement
On peut maintenant attacher la socket à un port et une adresse:
struct sockaddr_in adresse;
adresse.sin_family = AF_INET;
// Ecoute sur toutes les adresses (INADDR_ANY <=> 0.0.0.0)
adresse.sin_addr.s_addr = INADDR_ANY;
// Conversion du port en valeur réseaux (Host TO Network Short)
adresse.sin_port = htons(8080);
if (bind(fdsocket, (struct sockaddr *) &adresse, sizeof(adresse)) != 0) {
printf("Echéc d'attachement: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Mise en écoute
La socket est prête à passer à l'écoute des connexions des clients:
// Taille de la file d'attente
#define BACKLOG 3
if (listen(fdsocket, BACKLOG) != 0) {
printf("Echéc de la mise en écoute: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Acceptation des connexions
Il est possible d'accepter les nouvelles connexions:
int clientSocket;
// Structure contenant l'adresse du client
struct sockaddr_in clientAdresse;
unsigned int addrLen = sizeof(clientAdresse);
if ((clientSocket = accept(fdsocket, (struct sockaddr *) &clientAdresse, &addrLen)) != -1) {
// Convertion de l'IP en texte
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN);
printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port);
}
Il est possible d'utiliser fcntl pour passer le descripteur de la socket en non bloquant, avant l'appel à la fonction accept. Si le descripteur reste bloquant, la fonction accept ne rendra pas la main tant qu'un client ne se sera connecté, ayant pour résultat le blocage complet du programme !
// Passage en mode non bloquant
fcntl(fdsocket, F_SETFL, O_NONBLOCK);
On peut remarquer, au passage, l'utilisation de inet_ntop pour convertir l'adresse du client du format binaire au format text.
Lecture / Écriture
Mode connecté (TCP)
On peut maintenant utiliser le descripteur de la socket du client pour lire et écrire.
- avec les fonctions génériques:
#define BUFFER_LEN 200
// Descripteur de la socket du client
int clientSocket;
char buffer[BUFFER_LEN];
// Passage en mode non bloquant, sinon read attend
fcntl(clientSocket, F_SETFL, O_NONBLOCK);
int len = read(clientSocket, buffer, BUFFER_LEN);
write(clientSocket, "Coucou\n", strlen("Coucou\n"));
- avec les fonctions spécifiques:
#define BUFFER_LEN 200
// Descripteur de la socket du client
int clientSocket;
char buffer[BUFFER_LEN];
int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);
send(clientSocket, "Coucou\n", strlen("Coucou\n"), MSG_DONTWAIT);
Analysons le retour de la fonction read ou recv lorsque l'on utilise un descripteur non bloquant d'après la documentation :
NAME recv, recvfrom, recvmsg - receive a message from a socket [...] MSG_DONTWAIT (since Linux 2.2) Enables nonblocking operation; if the operation would block, the call fails with the error EAGAIN or EWOULDBLOCK (this can also be enabled using the O_NONBLOCK flag with the F_SETFL fcntl(2)). [...] RETURN VALUE [...] These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. When a stream socket peer has performed an orderly shutdown, the return value will be 0 (the traditional "end-of-file" return). [...]
Le retour est :
- lorsque le descripteur est vide (aucune donnée de la part du client) -1 et errno sera positionné à la valeur EAGAIN, il ne faut donc pas fermer la socket si le retour est -1 ! Il faut d'abord vérifier que errno n'a pas la valeur EAGAIN;
- lorsque le retour est 0, cela signifie que l'extrémité est fermée, il faut donc fermer le descripteur local;
- lorsque la valeur est supérieure à 0 elle correspond au nombre d'octets reçus.
On peut donc en déduire le morceau de code suivant pour gérer le retour de read ou recv:
int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);
if (len == -1 && errno != EAGAIN) {
// Une erreur est survenue
} else if (len == 0) {
// Le client s'est déconnecté (extrémité de la socket fermée)
} else if (len > 0) {
// Le client à envoyé des données
}
Mode datagramme (UDP)
Lecture
Pour lire dans une socket on peut utiliser recvfrom:
#include <unistd.h>
ssize_t recvfrom(int fd, void *buf, size_t count, int flags, const struct sockaddr *addr, socklen_t addrlen);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères où mettre le message reçu;
- count → le nombre de caractères à recevoir;
- flags → liste de drapeaux (eg. O_NONBLOCK);
- addr → une structure symbolisant l'adresse du client (cf. ci-dessus)
- addrlen → la taille de l'objet addr;
- le code retour correspond au nombre de caractères reçus ou -1 si une erreur survient (errno est positionné);
Écriture
Pour écrire dans une socket ou peut utiliser sendto:
#include <unistd.h>
ssize_t sendto(int fd, const void *buf, size_t count, const struct sockaddr *addr, socklen_t addrlen);
- sockfd → le fichier descripteur de la socket;
- buf → le tableau de caractères contenant le message à envoyer;
- count → le nombre de caractères à envoyer;
- flags → liste de drapeaux (eg. MSG_DONTWAIT , ...);
- addr → une structure symbolisant l'adresse du client (cf. ci-dessus)
- addrlen → la taille de l'objet addr;
- le code retour correspond au nombre de caractères envoyer ou -1 si une erreur survient (errno est positionné);
Exemples
Serveur TCP
Voici un exemple de serveur echo qui renvoie le message au client. Les connexions utilisent TCP et un simple client comme telnet suffit pour l'utiliser.
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <fcntl.h>
// Port d'écoute de la socket
#define PORT 8080
// Adresse d'écoute (toutes les adresses)
#define IP INADDR_ANY
// Taille de la file d'attente
#define BACKLOG 3
// Nombre de connexions clients
#define NB_CLIENTS 2
// Taille du tampon de lecture des messages
#define BUFFER_LEN 200
// Commande pour arrêter le serveur
#define EXIT_WORD "exit"
void initAdresse(struct sockaddr_in * adresse);
int initSocket(struct sockaddr_in * adresse);
int waitForClient(int * serverSocket);
void addClientToTab(int clientSocket, int clients[]);
void manageClient(int clients[]);
int main(void) {
// Création et initialisation du tableau contenant les descripteurs des sockets clients
int clients[NB_CLIENTS];
for (int i = 0; i < NB_CLIENTS; i++) {
clients[i] = -1;
}
// Structure contenant l'adresse
struct sockaddr_in adresse;
initAdresse(&adresse);
// Descripteur de la socket du serveur
int serverSocket = initSocket(&adresse);
int clientSocket;
while (1) {
// Descripteur de la socket du client, on attend une connexion
if ((clientSocket = waitForClient(&serverSocket)) != -1) {
// On ajoute le nouveau client au tableau des descripteurs
addClientToTab(clientSocket, clients);
}
manageClient(clients);
}
return EXIT_SUCCESS;
}
// Initialisation de la structure sockaddr_in
void initAdresse(struct sockaddr_in * adresse) {
(*adresse).sin_family = AF_INET;
(*adresse).sin_addr.s_addr = IP;
(*adresse).sin_port = htons( PORT);
}
// Démarrage de la socket serveur
int initSocket(struct sockaddr_in * adresse) {
// Descripteur de socket
int fdsocket;
// Nombre d'options
int opt = 1;
// Création de la socket en TCP
if ((fdsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
printf("Echéc de la création: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Création de la socket\n");
// Paramètrage de la socket
if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
sizeof(opt)) != 0) {
printf("Echéc de paramètrage: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Paramètrage de la socket\n");
// Attachement de la socket sur le port et l'adresse IP
if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
printf("Echéc d'attachement: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Attachement de la socket sur le port %i\n", PORT);
// Passage en écoute de la socket
if (listen(fdsocket, BACKLOG) != 0) {
printf("Echéc de la mise en écoute: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// Passage en mode non bloquant
fcntl(fdsocket, F_SETFL, O_NONBLOCK);
printf("Mise en écoute de la socket\n");
return fdsocket;
}
// Attente de connexion d'un client
int waitForClient(int * serverSocket) {
int clientSocket;
// Structure contenant l'adresse du client
struct sockaddr_in clientAdresse;
int addrLen = sizeof(clientAdresse);
if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse,
(socklen_t*) &addrLen)) != -1) {
// Convertion de l'IP en texte
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN);
printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port);
// Passage en mode non bloquant
int opt = 1;
setsockopt(clientSocket, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(1));
}
return clientSocket;
}
// Ajoute les clients au tableau
void addClientToTab(int clientSocket, int clients[]) {
// On vérifie si on à de la place de libre
int found = -1;
for (int i = 0; i < NB_CLIENTS; i++) {
// On cherche de la place
if (clients[i] == -1) {
// On ajoute le client
printf("Ajout du client à l'index %i\n", i);
clients[i] = clientSocket;
// Nouvelle connexion, envoie du message de bienvenu
send(clientSocket, "Entrez 'exit' pour quitter\n", strlen("Entrez 'exit' pour quitter\n"),
MSG_DONTWAIT);
found = 0;
break;
}
}
if (found == -1) {
// On a plus de place de libre
puts("Plus de place, désolé");
close(clientSocket);
}
}
// On traite l'input des clients
void manageClient(int clients[]) {
// Création d'un tampon pour stocker les messages des clients dans la heap
static char buffer[BUFFER_LEN + 1];
for (int i = 0; i < NB_CLIENTS; i++) {
// On vérifie les messages
int clientSocket = clients[i];
if (clientSocket == -1) {
continue;
}
int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);
// Booléen pour suivre l'état de la socket
int isClosed = 0;
if (len == -1 && errno != EAGAIN) {
// Une erreur est survenue
printf("Errno [%i] : %s\n", errno, strerror(errno));
isClosed = 1;
} else if (len == 0) {
// Le client s'est déconnecté (extrémité de la socket fermée)
isClosed = 1;
} else if (len > 0) {
// Ajout du terminateur de chaîne
buffer[len] = '\0';
if (strncmp(buffer, EXIT_WORD, 4) == 0) {
// Le client veut se déconnecter
send(clientSocket, "Bye\n", strlen("Bye\n"), 0);
isClosed = 1;
} else {
// On renvoie le texte au client dans un buffer assez grand
int len = strlen("Vous avez dit : ") + strlen(buffer) + 1;
char response[len];
strcpy(response, "Vous avez dit : ");
strcat(response, buffer);
// Un seul envoie permet de ne pas surcharger le réseau
send(clientSocket, response, strlen(response), 0);
}
}
if (isClosed == 1) {
// La socket est fermé ou le client veut quitter le serveur !
printf("Fermeture de la connexion avec le client\n");
// Fermeture de la socket
close(clientSocket);
// On fait de la place dans le tableau
clients[i] = -1;
}
}
}
On peut interroger notre serveur avec telnet (yum -y install telnet):
# telnet 127.0.0.1 8080 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. Entrez 'exit' pour quitter coucou Vous avez dit : coucou
Serveur UDP
Si on essaie de reproduire le même type de serveur mais cette fois-ci avec une socket en UDP, cela devient beaucoup plus simple. En effet, en UDP, plus besoin de tenir un tableau avec les descripteurs des sockets des clients car... on est en mode déconnecté (datagramme) ! Entendez par là que les clients envoient un message sans attendre de réponse et sans se soucier de savoir si le message est arrivé au serveur.
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <fcntl.h>
// Port d'écoute de la socket
#define PORT 8080
// Adresse d'écoute (toutes les adresses)
#define IP INADDR_ANY
// Taille de la file d'attente
#define BACKLOG 3
// Taille du tampon de lecture des messages
#define BUFFER_LEN 200
void initAdresse(struct sockaddr_in * adresse);
int initSocket(struct sockaddr_in * adresse);
void manageClient(int serverSocket);
int main(void) {
// Structure contenant l'adresse
struct sockaddr_in adresse;
initAdresse(&adresse);
// Descripteur de la socket du serveur
int serverSocket = initSocket(&adresse);
while (1) {
// Descripteur de la socket du client, on attend une connexion
manageClient(serverSocket);
}
return EXIT_SUCCESS;
}
// Initialisation de la structure sockaddr_in
void initAdresse(struct sockaddr_in * adresse) {
(*adresse).sin_family = AF_INET;
(*adresse).sin_addr.s_addr = IP;
(*adresse).sin_port = htons( PORT);
}
// Démarrage de la socket serveur
int initSocket(struct sockaddr_in * adresse) {
// Descripteur de socket
int fdsocket;
// Nombre d'options
int opt = 1;
// Création de la socket en UDP
if ((fdsocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) {
printf("Echéc de la création: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Création de la socket\n");
// Paramètrage de la socket
if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
sizeof(opt)) != 0) {
printf("Echéc de paramètrage: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Paramètrage de la socket\n");
// Attachement de la socket sur le port et l'adresse IP
if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
printf("Echéc d'attachement: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Attachement de la socket sur le port %i\n", PORT);
return fdsocket;
}
// On traite l'input des clients
void manageClient(int serverSocket) {
// Création d'un tampon pour stocker les messages des clients
static char buffer[BUFFER_LEN + 1];
// Structure contenant l'adresse du client
struct sockaddr_in clientAdresse;
unsigned int addrLen = sizeof(clientAdresse);
// On vérifie si on a reçu un message
int len = recvfrom(serverSocket, buffer, BUFFER_LEN, MSG_DONTWAIT,
(struct sockaddr*) &clientAdresse, &addrLen);
if (len == -1 && errno != EAGAIN) {
printf("Problème de socket %s\n", strerror(errno));
exit(EXIT_FAILURE);
} else if (len > 0) {
printf("Message reçu de %s:%i\n", inet_ntoa(clientAdresse.sin_addr),
ntohs(clientAdresse.sin_port));
// Ajout du terminateur de chaîne
buffer[len] = '\0';
// On renvoie le texte au client dans un buffer assez grand
len = strlen("Vous avez dit : ") + strlen(buffer) + 1;
char response[len];
strcpy(response, "Vous avez dit : ");
strcat(response, buffer);
// Un seul envoie permet de ne pas surcharger le réseau
sendto(serverSocket, response, strlen(response), MSG_DONTWAIT,
(struct sockaddr*) &clientAdresse, addrLen);
}
}
On peut interroger notre serveur avec netcat (yum -y install nc):
# nc -u 127.0.0.1 8080 coucou Vous avez dit : coucou
Cas du multicast
Il est intéressant parfois de s'abonner à un groupe multicast pour recevoir des messages.
Pour cela il suffit d'utiliser la structure mreq:
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(ip); // ip correspond à l'adresse multicast eg. "235.0.0.1"
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // cette ip correspond à l'adresse locale
Il faut ensuite positionner l'option sur le descripteur de fichier de la socket concernée:
if (setsockopt(fdsocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*) &mreq, sizeof(mreq)) < 0) {
printf("Echéc de paramètrage: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Notification évènementielle avec epoll
Depuis la version 2.5 du noyau Linux il est possible d'utiliser une série de fonctions, appartenant à l'espace utilisateur (user-space), permettant de suivre l'état d'un descripteur de fichier. La différence avec read, recv ou encore recvfrom c'est que l'on va pouvoir surveiller plusieurs descripteurs de fichiers en même temps, laissant le CPU libre pour effectuer autre chose !
Utilisation
Tout d'abord il faut créer un descripteur de fichier de type epoll:
#include <sys/epoll.h>
int epoll_create1(int flags);
- flags → EPOLL_CLOEXEC pour éviter les situations de compétition en environnement parallélisé;
- le code retour correspond au descripteur epoll ou -1 en cas d'erreur (errno est positionnée).
Une fois le descripteur epoll créé, il faut lui associer un événement:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd → le descripteur epoll à modifier;
- op → une opération parmi:
- EPOLL_CTL_ADD → pour ajouter un événement sur un descripteur de fichier;
- EPOLL_CTL_MOD → pour modifier un événement sur un descripteur de fichier;
- EPOLL_CTL_DEL → pour supprimer un événement sur un descripteur de fichier;
- fd → un descripteur de fichier à surveiller;
- event → la structure contenant l'événement à surveiller (EPOLLIN, EPOLLOUT, EPOLLRDHUP, ...);
- le code retour varie entre 0 en cas de succès ou -1 en cas d'erreur (errno est positionnée).
Voici la structure epoll_event:
#include <sys/epoll.h>
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
Enfin, il ne reste plus qu'à attendre des événements:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- epfd → le descripteur epoll à modifier;
- events → un tableau de epoll_event contenant les événements;
- maxevents → le nombre maximum d'événements à traiter (selon la taille du tableau);
- timeout → le temps, en millisecondes que la fonction attend:
- -1 → la fonction devient bloquante;
- 0 → retourne immédiatement;
- > 0 → attend le nombre de millisecondes spécifiées;
- le code retour correspond au nombre d'événements ou -1 en cas d'erreur (errno est positionnée).
Améliorations architecturales
Gestion des utilisateurs
Tout d'abord, le fait de laisser epoll gérer les descripteurs de socket nous permet de supprimer le tableau clients ainsi que la variable NB_CLIENTS. En effet, la référence vers le descripteur est maintenue par epoll et nous pouvons gérer maintenant un nombre illimité de clients.
Performances
La boucle while de notre serveur consomme énormément de CPU, environ 100% de CPU par client ! On aurait pu utiliser un temporisateur avec nanosleep pour baisser la consommation CPU entre 5% et 10%... Le fait de basculer sur une fonction bloquante comme epoll_wait qui utilise des signaux (interruptions) permet de réduire considérablement la consommation CPU. Gardez à l'esprit qu'il n'y a rien de mieux que les interruptions, la preuve, lorsque l'on fait un top, notre programme a complètement disparu de la liste !
Modification du serveur
Modifions le code du serveur TCP pour y intégrer epoll:
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
// Port d'écoute de la socket
#define PORT 8080
// Adresse d'écoute (toutes les adresses)
#define IP INADDR_ANY
// Taille de la file d'attente
#define BACKLOG 3
// Nombre de connexions clients
#define NB_EVENTS 2
// Taille du tampon de lecture des messages
#define BUFFER_LEN 200
// Commande pour arrêter le serveur
#define EXIT_WORD "exit"
void initAdresse(struct sockaddr_in * adresse);
int initSocket(struct sockaddr_in * adresse);
int waitForClient(int * serverSocket);
void addClientToTab(int * epfd, int * clientSocket);
void manageClient(int * epfd, int * clientSocket);
void addEpollEvent(int * epfd, int * socket);
void delEpollEvent(int * epfd, int * socket);
int main(void) {
// Création du descripteur EPOLL
int epfd = epoll_create1(0);
// Structure contenant les événements EPOLL
struct epoll_event events[NB_EVENTS];
// Structure contenant l'adresse
struct sockaddr_in adresse;
initAdresse(&adresse);
// Descripteur de la socket du serveur
int serverSocket = initSocket(&adresse);
addEpollEvent(&epfd, &serverSocket);
int clientSocket;
while (1) {
int nfds = epoll_wait(epfd, events, NB_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == serverSocket) {
if ((clientSocket = waitForClient(&serverSocket)) != -1) {
// On ajoute le nouveau client au tableau des descripteurs
addClientToTab(&epfd, &clientSocket);
}
} else if (events[i].events & EPOLLIN) {
// On a reçu quelque chose
manageClient(&epfd, &events[i].data.fd);
}
}
}
close(serverSocket);
return EXIT_SUCCESS;
}
// Initialisation de la structure sockaddr_in
void initAdresse(struct sockaddr_in * adresse) {
(*adresse).sin_family = AF_INET;
(*adresse).sin_addr.s_addr = IP;
(*adresse).sin_port = htons( PORT);
}
// Démarrage de la socket serveur
int initSocket(struct sockaddr_in * adresse) {
// Descripteur de socket
int fdsocket;
// Nombre d'options
int opt = 1;
// Création de la socket en TCP
if ((fdsocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("Echéc de la création: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Création de la socket\n");
// Paramètrage de la socket
if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
sizeof(opt)) != 0) {
printf("Echéc de paramètrage: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Paramètrage de la socket\n");
// Attachement de la socket sur le port et l'adresse IP
if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
printf("Echéc d'attachement: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Attachement de la socket sur le port %i\n", PORT);
// Passage en écoute de la socket
if (listen(fdsocket, BACKLOG) != 0) {
printf("Echéc de la mise en écoute: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Mise en écoute de la socket\n");
return fdsocket;
}
// Attente de connexion d'un client
int waitForClient(int * serverSocket) {
int clientSocket;
// Structure contenant l'adresse du client
struct sockaddr_in clientAdresse;
int addrLen = sizeof(clientAdresse);
if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse,
(socklen_t*) &addrLen)) != -1) {
// Convertion de l'IP en texte
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN);
printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port);
// Passage en mode non bloquant
int opt = 1;
setsockopt(clientSocket, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(1));
}
return clientSocket;
}
// Ajoute les clients au tableau
void addClientToTab(int * epfd, int * clientSocket) {
addEpollEvent(epfd, clientSocket);
// Nouvelle connexion, envoie du message de bienvenu
send(*clientSocket, "Entrez 'exit' pour quitter\n",
strlen("Entrez 'exit' pour quitter\n"), MSG_DONTWAIT);
}
// On traite l'input des clients
void manageClient(int * epfd, int * clientSocket) {
// Création d'un tampon pour stocker les messages des clients dans la heap
static char buffer[BUFFER_LEN + 1];
int len = recv(*clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);
// Booléen pour suivre l'état de la socket
int isClosed = 0;
if (len == -1 && errno != EAGAIN) {
// Une erreur est survenue
printf("Errno [%i] : %s\n", errno, strerror(errno));
isClosed = 1;
} else if (len == 0) {
// Le client s'est déconnecté (extrémité de la socket fermée)
isClosed = 1;
} else if (len > 0) {
// Ajout du terminateur de chaîne
buffer[len] = '\0';
if (strncmp(buffer, EXIT_WORD, 4) == 0) {
// Le client veut se déconnecter
send(*clientSocket, "Bye\n", strlen("Bye\n"), 0);
isClosed = 1;
} else {
// On renvoie le texte au client dans un buffer assez grand
int len = strlen("Vous avez dit : ") + strlen(buffer) + 1;
char response[len];
strcpy(response, "Vous avez dit : ");
strcat(response, buffer);
// Un seul envoie permet de ne pas surcharger le réseau
send(*clientSocket, response, strlen(response), 0);
}
}
if (isClosed == 1) {
// On enlève la socket des événements EPOLL
delEpollEvent(epfd, clientSocket);
// La socket est fermé ou le client veut quitter le serveur !
printf("Fermeture de la connexion avec le client\n");
// Fermeture de la socket
close(*clientSocket);
}
}
// Ajoute un descripteur de socket aux événements EPOLL
void addEpollEvent(int * epfd, int * socket) {
printf("Ajout de la socket %i dans les événements EPOLL\n", *socket);
// Structure contenant les événements EPOLL
struct epoll_event event;
// Initialisation de la structure
memset(&event, 0, sizeof(struct epoll_event));
// On enregistre la socket du serveur comme descripteur EPOLL
event.data.fd = *socket;
// On surveille les message entrant, la fermeture de la socket
event.events = EPOLLIN | EPOLLRDHUP;
epoll_ctl(*epfd, EPOLL_CTL_ADD, *socket, &event);
}
// Efface un descripteur de socket des événements EPOLL
void delEpollEvent(int * epfd, int * socket) {
printf("Suppression de la socket %i des événements EPOLL\n", *socket);
// Structure contenant les événements EPOLL
struct epoll_event event;
// Initialisation de la structure
memset(&event, 0, sizeof(struct epoll_event));
// On enregistre la socket du serveur comme descripteur EPOLL
event.data.fd = *socket;
// On surveille les message entrant, la fermeture de la socket
event.events = EPOLLIN | EPOLLRDHUP;
epoll_ctl(*epfd, EPOLL_CTL_DEL, *socket, &event);
}
Voici ce que l'on peut observer dans la console:
Création de la socket Paramètrage de la socket Attachement de la socket sur le port 8080 Mise en écoute de la socket Ajout de la socket 4 dans les événements EPOLL Connexion de 127.0.0.1:17607 Ajout de la socket 5 dans les événements EPOLL Suppression de la socket 5 des événements EPOLL Fermeture de la connexion avec le client Connexion de 127.0.0.1:18119 Ajout de la socket 5 dans les événements EPOLL Connexion de 127.0.0.1:18631 Ajout de la socket 6 dans les événements EPOLL Suppression de la socket 5 des événements EPOLL Fermeture de la connexion avec le client