C socket

De The Linux Craftsman
Aller à : navigation, rechercher

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.

Socket workflow.png

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;
int addrLen = sizeof(clientAdresse);
if ((clientSocket = accept(fdsocket, &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 dans la heap
	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

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;
	// 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;
	// 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