Différences entre versions de « C socket »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
Ligne 135 : Ligne 135 :
 
Cette commande va nous permettre de modifier le drapeaux du descripteur de la socket (''F_SETFL'') pour le rendre non bloquant (''O_NONBLOCK'') !
 
Cette commande va nous permettre de modifier le drapeaux du descripteur de la socket (''F_SETFL'') pour le rendre non bloquant (''O_NONBLOCK'') !
  
== Les spécifiques ==
+
== 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é''.
 
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===
 
===Lecture===

Version du 27 octobre 2018 à 17:58

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 des sockets

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

Transmission dans une socket

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

Exemples

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>

// 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
// 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) {
	// 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
		int clientSocket = waitForClient(&serverSocket);
		// Envoie du message de bienvenu
		send(clientSocket, WELCOME_MESSAGE, strlen(WELCOME_MESSAGE), 0);
		char buffer[BUFFER_LEN] = "";
		while(strncmp(buffer, EXIT_WORD, 4) != 0){
			int len = read(clientSocket, buffer, BUFFER_LEN);
			// Ajout du terminateur de chaîne
			buffer[len] = '\0';
			// On renvoie le texte au client
			send(clientSocket, "Vous avez dit : ", strlen("Vous avez dit : "), 0);
			send(clientSocket, buffer, strlen(buffer), 0);
		}
		send(clientSocket, "Bye\n", strlen("Bye\n"), 0);
		printf("Fermeture de la connexion avec le client\n");
		close(clientSocket);
	}
	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);
	printf("En attente d'une connexion\n");
	if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse,
			(socklen_t*) &addrLen)) == -1) {
		printf("Echéc de la récupération de la socket du client: %s\n",
				strerror(errno));
		exit(EXIT_FAILURE);
	}
	// 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);
	return clientSocket;
}

Serveur multi-utilisateurs