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. |
Utilisation
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é)
Quelques fonctions utiles
Les 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) !
Les 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;
}