aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Serveur de Jeu

Programmation socket C++ : créer un serveur TCP simple

Que vous écriviez un serveur de jeu, un service de chat ou une simple API, le fondement est toujours le même : deux machines qui se parlent sur le réseau. Côté C++, la couche la plus basse qui réalise ce travail est l'API c++ socket — les sockets Berkeley, standard depuis les années 1980 et qui tournent encore aujourd'hui sous le noyau Linux. Dans cet article, nous allons construire de zéro un serveur TCP simple, en utilisant uniquement des appels système POSIX et sans aucune bibliothèque externe, qui écoute les clients entrants, lit leurs données et renvoie une réponse.

Qu'est-ce qu'un socket et pourquoi TCP est-il important ?

Un socket est un point de terminaison de communication fourni par le système d'exploitation. Du point de vue de votre programme, il se comporte comme un descripteur de fichier : vous lisez dedans et vous y écrivez, et le noyau gère toute la complexité réseau entre les deux. Il existe deux principaux protocoles de transport :

  • TCP (SOCK_STREAM) : orienté connexion, ordonné et fiable. Les octets que vous envoyez arrivent complets et dans le bon ordre. Utilisé partout où tout doit passer — chat, HTTP, connexion à un jeu.
  • UDP (SOCK_DGRAM) : sans connexion, rapide mais sans garantie. Les paquets peuvent être perdus ou réordonnés. Privilégié quand la latence compte plus que la fiabilité, comme les positions en temps réel dans un jeu.

Nous nous concentrons ici sur TCP car il illustre le mieux le schéma que l'on appelle un « serveur simple ». Le flux est toujours le même : socket()bind()listen()accept()recv()/send()close().

Les en-têtes nécessaires

La programmation socket sous Linux requiert quelques en-têtes POSIX. Sous Windows, on utiliserait Winsock (winsock2.h et WSAStartup) ; ici nous supposons Linux/macOS.

#include <iostream>
#include <cstring>      // memset, strlen
#include <unistd.h>     // close, read, write
#include <sys/socket.h> // socket, bind, listen, accept
#include <netinet/in.h> // sockaddr_in, htons
#include <arpa/inet.h>  // inet_ntop

Créer le socket d'écoute

La première étape consiste à créer un point de terminaison avec l'appel socket(). AF_INET sélectionne IPv4 et SOCK_STREAM sélectionne TCP. L'appel renvoie -1 en cas d'échec ; vérifier la valeur de retour de chaque appel système est indispensable en programmation socket.

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
    perror("socket");
    return 1;
}

// Évite l'erreur « Address already in use » au redémarrage du serveur
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

L'option SO_REUSEADDR est petite mais importante : lorsque vous arrêtez le serveur puis le relancez aussitôt, le port peut encore être en TIME_WAIT et bind() échoue. Cette option lève cet obstacle.

Lier, écouter et accepter

Nous lions maintenant le socket à une IP et un port. On remplit l'adresse avec une structure sockaddr_in. Le point le plus critique ici est l'ordre des octets : le numéro de port est attendu en big-endian sur le réseau, on le convertit donc avec htons() (host-to-network short). INADDR_ANY signifie « écouter sur toutes les interfaces réseau de cette machine ».

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;   // toutes les interfaces
addr.sin_port = htons(8080);         // port 8080 en ordre réseau

if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) {
    perror("bind");
    return 1;
}

if (listen(server_fd, SOMAXCONN) == -1) {  // file d'attente
    perror("listen");
    return 1;
}
std::cout << "Serveur a l'ecoute sur le port 8080...\n";

Le deuxième argument de listen() est la taille de la file pour les connexions en attente ; SOMAXCONN utilise la plus grande valeur raisonnable autorisée par le système. Nous pouvons désormais accueillir les clients entrants un par un avec accept(). Cet appel bloque jusqu'à l'arrivée d'une nouvelle connexion et renvoie un descripteur de socket distinct pour chaque connexion.

Lire, écrire et construire la boucle

Une fois la connexion établie, on dialogue avec le client via recv() et send() (de manière équivalente, read()/write() fonctionnent aussi). L'erreur la plus fréquente ici est d'oublier que recv() peut renvoyer non pas autant d'octets que demandé, mais autant qu'il y en a de disponibles à l'instant. La valeur de retour est le nombre d'octets lus ; 0 signifie que le pair a fermé la connexion et -1 une erreur.

while (true) {
    sockaddr_in client{};
    socklen_t len = sizeof(client);
    int client_fd = accept(server_fd, (sockaddr*)&client, &len);
    if (client_fd == -1) { perror("accept"); continue; }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client.sin_addr, ip, sizeof(ip));
    std::cout << "Nouvelle connexion : " << ip << "\n";

    char buffer[1024];
    ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (n > 0) {
        buffer[n] = '\0';
        std::cout << "Recu : " << buffer;
        const char* reply = "Bonjour, message bien recu !\n";
        send(client_fd, reply, strlen(reply), 0);
    }
    close(client_fd);   // termine avec ce client
}
close(server_fd);

Cette boucle sert un client à la fois. Après avoir compilé le programme (g++ -std=c++17 server.cpp -o server) et l'avoir lancé, vous pouvez le tester depuis un autre terminal en vous connectant avec nc localhost 8080 ou telnet localhost 8080 et en tapant un message.

À quoi faire attention en production

Le serveur ci-dessus est parfait comme tutoriel, mais à lui seul il ne suffit pas dans le monde réel. Quelques points à garder à l'esprit :

  • Concurrence : une seule boucle ne gère qu'un client à la fois. Pour de nombreux clients, il faut attribuer un thread à chaque connexion ou — solution plus scalable — construire une boucle d'événements basée sur epoll.
  • Lectures/écritures partielles : comme recv(), send() peut ne pas envoyer tous les octets d'un coup. Vous devez le rappeler dans une boucle jusqu'à ce que tout le tampon soit envoyé.
  • Signaux : écrire sur un socket fermé peut tuer le processus avec SIGPIPE ; utiliser le drapeau MSG_NOSIGNAL dans l'appel send() est une bonne habitude.
  • Limites de message : TCP est un flux d'octets, sans notion de « message ». Dans votre propre protocole, vous devez envoyer la longueur du message en préfixe ou utiliser un délimiteur.

Questions fréquentes

Dois-je utiliser TCP ou UDP ?

Utilisez TCP quand les données doivent arriver complètes et dans l'ordre : connexion/authentification, chat, opérations d'inventaire. Si perdre quelques paquets est acceptable mais que la faible latence est critique (positions des joueurs, mises à jour de mouvement), UDP est mieux adapté. Beaucoup de jeux utilisent les deux ensemble.

Le même code fonctionne-t-il sous Windows ?

La logique est identique, mais Windows nécessite la bibliothèque Winsock : on appelle WSAStartup() au début, le type de socket est SOCKET au lieu de int, et on utilise closesocket() au lieu de close(). Écrire une couche multiplateforme pour abstraire ces différences est une approche courante.

Pourquoi recv() renvoie-t-il moins de données que prévu ?

Parce que TCP est un flux, pas des paquets. Le noyau vous donne ce qui se trouve dans son tampon à cet instant. Pour lire un message complet, vous devez rappeler recv() dans une boucle jusqu'à atteindre le nombre d'octets attendu.

Besoin d'aide en programmation réseau bas niveau ? Si vous souhaitez un accompagnement sur des serveurs de jeu et de chat haute performance en C++, des protocoles sur mesure et des architectures basées sur epoll, contactez-moi — bâtissons ensemble votre projet sur des fondations solides.

Bu kategorideki tüm yazılar →

Devamı için