Différences entre versions de « C socket »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
Ligne 227 : Ligne 227 :
 
// Passage en mode non bloquant
 
// Passage en mode non bloquant
 
fcntl(fdsocket, F_SETFL, O_NONBLOCK);
 
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.
 
</source>
 
</source>
 +
 
== Lecture / Écriture ==
 
== Lecture / Écriture ==
 
On peut maintenant utiliser le descripteur de la socket du client pour lire et écrire.
 
On peut maintenant utiliser le descripteur de la socket du client pour lire et écrire.

Version du 28 octobre 2018 à 11:48

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

On peut remarquer, au passage, l'utilisation de ''inet_ntop'' pour convertir l'adresse du client du format binaire au format text.

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