C socket

De The Linux Craftsman
Aller à la navigation Aller à la recherche

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'envoie 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'entê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é)


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é)

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é)


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é)


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é)

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é)

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 ou mettre le message reçu;
  • count → le nombre de caractères à recevoir;
  • le code retour correspond au nombre de caractères reçu 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 envoyer 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 correspondants à 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 ressemble au précédentes mais acceptent des drapeaux en plus pour pouvoir modifier le descripteur à la volé.

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 ou 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çu 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);
  • 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 envoyer 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(*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);
}

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

Lecture / Écriture

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

send(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érieur à 0 elle correspond au nombre d'octets reçu.

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	
}

Serveur mono-utilisateur

Voici un exemple de serveur echo qui renvoie le message au client.

#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
// Message à envoyer au client
# define WELCOME_MESSAGE "Entrez 'exit' pour quitter\n"
// 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);

int main(void) {
	// Création et initialisation du tableau contenant les clients
	int clients[NB_CLIENTS];
	for (int i = 0; i < NB_CLIENTS; i++) {
		clients[i] = -1;
	}
	int nbClients = 0;
	// Structure contenant l'adresse
	struct sockaddr_in adresse;
	initAdresse(&adresse);
	// Descripteur de la socket du serveur
	int serverSocket = initSocket(&adresse);
	// Création d'un tampon pour stocker les messages des clients
	char buffer[BUFFER_LEN + 1];
	while (1) {
		if (nbClients < NB_CLIENTS) {
			// Descripteur de la socket du client, on attend une connexion
			int clientSocket = waitForClient(&serverSocket);
			if (clientSocket != -1) {
				// On vérifie si on à de la place de libre
				int found = -1;
				for (int i = 0; i < NB_CLIENTS; i++) {
					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
						write(clientSocket, WELCOME_MESSAGE,
								strlen(WELCOME_MESSAGE));
						found = 0;
						nbClients++;
						break;
					}
				}
				if (found == -1) {
					// On a plus de place de libre
					send(clientSocket, "Plus de place, désolé\n",
							strlen("Plus de place, désolé\n"), 0);
					close(clientSocket);
				}
			}
		}
		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);
			// On vérifie que la socket n'est pas fermé
			if (len == -1 && errno != EAGAIN) {
				printf("Errno [%i] : %s\n", errno, strerror(errno));
				// La socket est fermé, on libère de la place
				close(clientSocket);
				clients[i] = -1;
				nbClients--;
			} else if (len > 0) {
				// Ajout du terminateur de chaîne
				buffer[len] = '\0';
				if (strncmp(buffer, EXIT_WORD, 4) == 0) {
					// On ferme la socket
					send(clientSocket, "Bye\n", strlen("Bye\n"), 0);
					printf("Fermeture de la connexion avec le client\n");
					close(clientSocket);
					clients[i] = -1;
					nbClients--;
				} else {
					// On renvoie le texte au client
					write(clientSocket, "Vous avez dit : ",
							strlen("Vous avez dit : "), 0);
					send(clientSocket, buffer, strlen(buffer), 0);
				}
			}
		}
	}
	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);
	}
	// 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));
		//fcntl(clientSocket, F_SETFL, O_NONBLOCK|SO_KEEPALIVE);
	}
	return clientSocket;
}

Serveur multi-utilisateurs