.. _sockets: Partie 6 - Sockets ================== .. raw:: html Support présentation Introduction ------------ Un **socket** est un point de communication bidirectionnel entre deux processus. Il peut servir à communiquer **localement** (``AF_UNIX``) ou via le **réseau** (``AF_INET`` pour IPv4, ``AF_INET6`` pour IPv6). Les sockets sont manipulées comme des **descripteurs de fichiers** (entiers), utilisables avec ``read``/``write`` ou ``send``/``recv``. .. figure:: _images/socket_a_b.excalidraw.png :alt: Deux processus qui utilisent des sockets pour communiquer via le réseau IP. :align: center Modes de transport TCP/UDP ~~~~~~~~~~~~~~~~~~~~~~~~~~ - **TCP** (*Transmission Control Protocol*) : ``SOCK_STREAM``, mode *connecté*, fiable, ordonné (handshake, accusés, retransmissions, contrôle de flux et de congestion). Idéal pour HTTP/HTTPS, SSH, SMTP/IMAP/POP3, bases de données. - **UDP** (*User Datagram Protocol*) : ``SOCK_DGRAM``, mode *non connecté*, non fiable (best-effort), non ordonné, sans retransmission intégrée. Idéal lorsque la **latence** prime (streaming temps réel, VoIP, jeux). .. figure:: _images/TCP_UDP.jpg :alt: Présentation de TCP avec les handshake et de UDP avec des envois de données directs. :align: center `Source de l'image `__. TCP fournit une **connexion** et une **fiabilité**, UDP fournit une **boîte aux lettres** rapide sans garantie. Analogie avec boire de l'eau, avec TCP on est sûr de boire l'eau, avec UDP, on "espère" boire l'eau. .. figure:: https://i.redd.it/duv11av99nm11.png :align: center :alt: Image montrant d'un côté "TCP" avec une personne buvant correctement à une bouteille d'eau, de l'autre "UDP" avec une personne versant le contenu d'une bouteille d'eau sur le visage. `Source de l'image `__ Modèle OSI et pile TCP/IP ------------------------- Les sockets se situent au-dessus des protocoles de transport (TCP/UDP) et exposent une API de programmation. .. figure:: _images/OSI.png :alt: Modèle OSI avec les 7 couches. :align: center `Source de l'image `__. Principe d'un échange d'une communication en mode connecté (TCP) ---------------------------------------------------------------- Une communication TCP met en jeu deux applications distinctes : - le **serveur** : attend et accepte les connexions entrantes - le **client** : initie la connexion vers le serveur 1. Le **serveur** doit être lancé avant le client. Il crée une socket (``socket``), l'associe à une adresse IP et un numéro de port (``bind``), puis la place en écoute (``listen``). Quand un client se présente, l'appel ``accept`` crée une **nouvelle socket connectée** dédiée à ce client. 2. Le **client** crée aussi une socket (``socket``) puis tente de se connecter au serveur via ``connect`` en précisant l'adresse IP et le port du serveur. 3. Une fois la connexion établie, les deux côtés peuvent échanger des données avec ``send``/``recv`` (ou ``write``/``read``). 4. Pour terminer, la connexion TCP est fermée proprement avec ``shutdown`` et ``close``, ce qui libère les ressources. .. admonition:: À retenir :class: note - ``listen`` met simplement la socket serveur en attente, mais **ne crée pas la connexion**. - C'est ``accept`` qui génère une **nouvelle socket connectée** pour dialoguer avec un client, tandis que la socket d'écoute continue à attendre d'autres connexions. .. figure:: _images/TCP.jpg :alt: Présentation de TCP. :align: center `Source de l'image `__. Principe d'un échange d'une communication en mode non connecté (UDP) -------------------------------------------------------------------- Une communication UDP implique également deux applications : - le **serveur** : attend les datagrammes sur un port donné - le **client** : envoie des datagrammes vers l'adresse et le port du serveur Contrairement à TCP, **aucune connexion persistante n'est établie** : chaque envoi est indépendant et peut être perdu ou reçu dans un ordre différent. 1. Le **serveur** crée une socket (``socket``) de type ``SOCK_DGRAM`` puis l'associe à une adresse IP et un port avec ``bind``. Il utilise ensuite ``recvfrom`` pour attendre la réception d'un datagramme, et peut répondre avec ``sendto``. 2. Le **client** crée aussi une socket (``socket``), puis envoie un datagramme au serveur avec ``sendto`` en indiquant explicitement l'adresse IP et le port de destination. Pour recevoir la réponse, il utilise ``recvfrom``. 3. Chaque envoi ou réception est indépendant : il n'y a pas de notion de session ou de « connexion » comme en TCP. 4. Les sockets UDP se ferment simplement avec ``close``. .. admonition:: À retenir :class: note - UDP ne garantit **ni la livraison**, **ni l'ordre**, **ni l'absence de doublons**. - C'est le programme qui doit gérer ces aspects si nécessaire (accusés de réception, numéros de séquence, retransmission, etc.). - Ce manque de fiabilité en contrepartie d'une faible latence en fait un choix adapté au streaming, à la voix sur IP (VoIP) ou aux jeux en ligne. .. figure:: _images/UDP.jpg :alt: Présentation de UDP. :align: center `Source de l'image `__. Création d'un socket -------------------- Prototype C : .. code-block:: c int socket(int domain, int type, int protocol); - ``domain`` : ``AF_INET`` (IPv4), ``AF_INET6`` (IPv6), ``AF_UNIX`` (local) - ``type`` : ``SOCK_STREAM`` (TCP), ``SOCK_DGRAM`` (UDP) - ``protocol`` : souvent ``0`` (choix par défaut selon ``type``) Retourne ``-1`` si erreur (``errno`` contient le code d'erreur). Exemple : .. code-block:: c // connexion TCP en mode connecté int fd_tcp = socket(AF_INET, SOCK_STREAM, 0); if (fd_tcp == -1) { perror("socket TCP"); exit(1); } // connexion UDP en mode non connecté int fd_udp = socket(AF_INET, SOCK_DGRAM, 0); if (fd_udp == -1) { perror("socket UDP"); exit(1); } Définition des interfaces avec ``bind`` --------------------------------------- ``bind`` associe un socket à une adresse locale (IP + port pour ``AF_INET``/``AF_INET6`` ou chemin pour ``AF_UNIX``). .. code-block:: c int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - ``sockfd`` : descripteur de socket - ``addr`` : pointeur vers la structure d'adresse (cast en ``struct sockaddr*``) - ``addrlen`` : taille de la structure Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur). .. note:: En pratique, on **n'utilise pas directement** ``struct sockaddr``. On remplit une structure spécialisée (``sockaddr_in``, ``sockaddr_in6``, ``sockaddr_un``), puis on la **cast** en ``struct sockaddr*`` pour l'appel système. .. note:: La fonction ``htons`` (*Host TO Network Short*, ``htonl`` pour *Long*) convertit un entier 16 bits de l'ordre d'octets de la machine (**host order**) vers l'ordre d'octets du réseau (**network order**, big-endian). Elle est définie dans ````. Utilisation typique : convertir un **numéro de port** avant de l'inscrire dans une structure d'adresse ``sockaddr_in``. Sans cette conversion, le programme pourrait fonctionner par hasard sur une machine **little-endian** (x86) mais échouer sur une architecture **big-endian** (ordre mémoire différent). - Adresse IPv4 (``AF_INET`` - ````) .. code-block:: c struct sockaddr_in { sa_family_t sin_family; // AF_INET in_port_t sin_port; // port en ordre réseau (htons) struct in_addr sin_addr; // adresse IPv4 (htonl ou inet_pton) unsigned char sin_zero[8]; // padding pour compatibilité }; struct in_addr { uint32_t s_addr; // adresse IPv4 en ordre réseau }; Exemple : .. code-block:: c struct sockaddr_in addr = {0}; // met tous les champs de addr à 0 addr.sin_family = AF_INET; addr.sin_port = htons(8080); // port 8080 → ordre réseau addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 → écouter sur toutes les interfaces IPv4 - Adresse IPv6 (``AF_INET6`` - ````) .. code-block:: c struct sockaddr_in6 { sa_family_t sin6_family; // AF_INET6 in_port_t sin6_port; // port en ordre réseau (htons) uint32_t sin6_flowinfo; // infos QoS/flux struct in6_addr sin6_addr; // adresse IPv6 (inet_pton) uint32_t sin6_scope_id; // identifiant de zone (interfaces locales) }; struct in6_addr { unsigned char s6_addr[16]; // adresse IPv6 en binaire }; Exemple : .. code-block:: c struct sockaddr_in6 addr6 = {0}; addr6.sin6_family = AF_INET6; addr6.sin6_port = htons(8080); // port 8080 → ordre réseau addr6.sin6_addr = in6addr_any; // :: → écouter sur toutes les interfaces IPv6 - Adresse locale (``AF_UNIX`` - ````) .. code-block:: c struct sockaddr_un { sa_family_t sun_family; // AF_UNIX char sun_path[108]; // chemin du fichier-socket }; Exemple : .. code-block:: c struct sockaddr_un addr = {0}; addr.sun_family = AF_UNIX; strcpy(addr.sun_path, "/tmp/mysocket"); // chemin du fichier-socket Attendre des connexions avec ``listen`` --------------------------------------- Pour les sockets TCP, le serveur utilise ``listen`` pour indiquer qu'il accepte des connexions entrantes : .. code-block:: c int listen(int sockfd, int backlog); - ``sockfd`` : socket créée et bindée - ``backlog`` : taille maximale de la file d'attente des connexions Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur). Si plusieurs demandes de connexion ont lieu en même temps, elles sont stockées dans une file d'attente. Exemple : .. code-block:: c int s = socket(AF_INET, SOCK_STREAM, 0); // ... bind(s, ...) if (listen(s, 16) == -1) { perror("listen"); exit(1); } // la socket s est maintenant en écoute Accepter des connexions avec ``accept`` --------------------------------------- Un serveur TCP appelle ``accept`` pour accepter une connexion ce qui crée une **nouvelle socket** connectée pour le client. .. code-block:: c int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - ``sockfd`` : descripteur de la socket d'écoute - ``addr`` : pointeur vers une structure qui contiendra l'adresse du client connecté - ``addrlen`` : pointeur vers la taille de cette structure Retourne un nouveau descripteur, distinct de la socket d'écoute si succès, ``-1`` si erreur (``errno`` contient le code d'erreur). Exemple : .. code-block:: c struct sockaddr_in cli; socklen_t len = sizeof cli; int c = accept(s, (struct sockaddr*)&cli, &len); if (c == -1) { perror("accept"); // gérer l'erreur } else { // c est la socket connectée au client } Connexion à un socket avec ``connect`` -------------------------------------- Le client appelle ``connect`` pour établir la connexion : .. code-block:: c int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - ``sockfd`` : descripteur de la socket du client - ``addr`` : adresse du serveur - ``addrlen`` : taille de la structure Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur). Exemple (IPv4 localhost:8080) : .. code-block:: c int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv = {0}; serv.sin_family = AF_INET; serv.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr); if (connect(sock, (struct sockaddr*)&serv, sizeof serv) == -1) { perror("connect"); exit(1); } Envoyer et recevoir des données avec ``send``/``recv`` ou ``sendto``/``recvfrom`` --------------------------------------------------------------------------------- Ces fonctions permettent d'échanger des données. En TCP, la socket est **connectée** et l'adresse du pair est implicite. En UDP, on utilise généralement ``sendto``/``recvfrom`` pour préciser l'adresse à chaque envoi/réception (sauf si la socket UDP a été ``connect``-ée pour figer le pair). En TCP : .. code-block:: c int send(int sockfd, const void *buf, size_t len, int flags); int recv(int sockfd, void *buf, size_t len, int flags); - ``sockfd`` : descripteur de socket connectée - ``buf`` : tampon de données - ``len`` : taille du tampon - ``flags`` : options (souvent 0) Retourne le nombre d'octets envoyés/reçus ou ``-1`` si erreur. Exemple (écho simple) : .. code-block:: c const char *msg = "Hello\n"; if (send(sock, msg, strlen(msg), 0) == -1) perror("send"); char buf[1024]; ssize_t n = recv(sock, buf, sizeof buf - 1, 0); if (n > 0) { buf[n] = 0; printf("%s", buf); } En UDP (avec adresse) : .. code-block:: c int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); - ``sockfd`` : descripteur de socket UDP - ``buf`` : tampon de données - ``len`` : taille du tampon - ``flags`` : options (souvent 0) - ``dest_addr`` / ``src_addr`` : adresse de destination/source - ``addrlen`` : taille de la structure d'adresse Retourne le nombre d'octets envoyés/reçus ou ``-1`` si erreur. Exemple (ping UDP localhost:8080) : .. code-block:: c int u = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in dst = {0}; dst.sin_family = AF_INET; dst.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &dst.sin_addr); const char *p = "Ping"; if (sendto(u, p, strlen(p), 0, (struct sockaddr*)&dst, sizeof dst) == -1) perror("sendto"); // côté serveur UDP : recvfrom(...), puis sendto(...) pour répondre Fermeture d'une socket ---------------------- En fin de communication, il faut **libérer proprement** la socket pour éviter les fuites de ressources et signaler correctement la fin d'échange au pair. TCP : ``shutdown`` puis ``close`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ En TCP, la fermeture peut être **demi-duplex** (on arrête l'envoi mais on continue à recevoir) ou **complète**. L'API propose : - ``shutdown(fd, SHUT_WR)`` // on n'envoie plus, on peut encore recevoir (half-close) - ``shutdown(fd, SHUT_RD)`` // on ne reçoit plus - ``shutdown(fd, SHUT_RDWR)`` // on n'envoie ni ne reçoit (fermeture logique des deux sens) - ``close(fd)`` // libère le descripteur au niveau du processus **Patron de fermeture conseillé (côté client ou serveur) :** 1. Terminer les envois : ``shutdown(fd, SHUT_WR)`` 2. Lire jusqu'à **EOF** (``recv`` qui retourne 0) pour consommer les données restantes. 3. Appeler ``close(fd)`` pour libérer la ressource. Exemple : fermeture TCP propre ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: c // fin des envois vers le pair shutdown(fd, SHUT_WR); // signal FIN envoyé // lire jusqu'à EOF pour s'assurer de recevoir la fin distante char buf[4096]; for (;;) { ssize_t n = recv(fd, buf, sizeof buf, 0); if (n > 0) { // traiter éventuellement les derniers octets continue; } if (n == 0) { // EOF : le pair a fermé sa moitié d'écriture break; } if (errno == EINTR) continue; // interruption signal → reprendre perror("recv"); break; } // libère le descripteur local close(fd); UDP : ``close`` suffit ~~~~~~~~~~~~~~~~~~~~~~ UDP n'a pas de connexion persistante ni de handshake de fermeture. Il suffit de fermer le descripteur : .. code-block:: c close(fd_udp); // libère la socket UDP Sockets locales (AF_UNIX) ------------------------- Pour la communication **locale** entre processus, ``AF_UNIX`` est très performant (pas de pile IP). Au lieu d'une IP/port, on utilise un **chemin de fichier**. .. code-block:: c :linenos: #include #include #include int fd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr = {0}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, "/tmp/mysock", sizeof addr.sun_path - 1); bind(fd, (struct sockaddr*)&addr, sizeof addr); Avec ``AF_UNIX``, il faut **fermer** puis **supprimer le fichier-socket** (côté serveur) : .. code-block:: c close(fd_unix); // ferme le descripteur unlink("/tmp/mysocket"); // supprime le chemin de la socket (sinon fichier persiste)