C socket
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. |
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
// 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 * clientSocket);
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, 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));
}
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;
}
}
}