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
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);
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
}
Mode datagramme (UDP)
Lecture
Pour lire dans une socket on peut utiliser recvfrom:
#include <unistd.h>
ssize_t recv(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 ou 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çu ou -1 si une erreur survient (errno est positionné);
Écriture
Pour écrire dans une socket ou peut utiliser sendto:
#include <unistd.h>
ssize_t send(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 essaye 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 la que les clients envoie 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 !