Table of Contents
Introduction à la programmation de socket en C
Construire une application de chat à partir de zéro est l'une des meilleures façons de maîtriser les fondamentaux de la programmation réseau. L'utilisation de sockets C vous permet de contrôler directement chaque interaction réseau, de l'établissement de connexion au transfert de données. Ce projet passe par la création d'un serveur de chat multi-clients et d'un programme client qui communiquent en temps réel. En chemin, vous apprendrez à créer des sockets, à les lier aux ports, à écouter les connexions, à accepter les clients, à envoyer et à recevoir des messages et à gérer simultanément plusieurs utilisateurs.
Nous prenons en compte les connaissances de base de la programmation C (variables, boucles, fonctions, pointeurs) et d'un environnement de travail avec un compilateur C (GCC) et un système d'exploitation compatible POSIX (Linux, macOS ou WSL sur Windows).
Comprendre les chaussettes C
Une socket est un paramètre de communication entre deux machines sur un réseau. Dans l'API de socket POSIX, une socket est représentée par un descripteur de fichier entier. Les types les plus courants sont les sockets (SOCK STREAM) utilisant TCP et sockets de datagramme (SOCK DGRAM) utilisant UDP. Pour une application de chat fiable, nous utilisons des sockets de flux TCP.
Les fonctions principales qui composent l'API de socket sont :
- – Crée une nouvelle socket et retourne un descripteur de fichier.
- – Associé à une socket avec une adresse IP et un numéro de port spécifiques.
- – Marque une prise comme passive, prête à accepter les connexions entrantes.
- – Bloque jusqu'à ce qu'un client se connecte, puis retourne une nouvelle socket pour ce client spécifique.
- – Utilisé par un client pour lancer une connexion à un serveur.
- et – Transmettre et recevoir des données sur une prise connectée.
Toutes ces fonctions sont déclarées dans . Des en-têtes supplémentaires comme (pour ) et (pour la conversion de l'adresse IP) sont également nécessaires. La compréhension de ces en-têtes et des structures qu'ils définissent est essentielle. La structure détient la famille d'adresses, le port (dans l'ordre des octets réseau) et l'adresse IP. Vous devez toujours convertir entre l'ordre des octets hôte et réseau en utilisant et (ou leurs contreparties inversées).
Pour une référence plus approfondie, consultez Le Guide de la programmation en réseau de Beej – une ressource classique qui explique chaque fonction avec des exemples clairs.
Configuration du serveur
Le serveur est le centre central de l'application de chat. Il écoute les connexions client, reçoit les messages de n'importe quel client et diffuse ces messages à tous les autres clients connectés.
Création de la prise de courant
La première étape consiste à créer une socket avec la fonction :
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
- AF INET – Famille d'adresses pour IPv4.
- SOCK STREAM – Flux d'octets (TCP) fiable, bidirectionnel et basé sur la connexion.
- 0 – Protocole; par défaut, TCP lorsque SOCK STREAM est utilisé.
Toujours vérifier la valeur de retour; une défaillance de création de socket doit être manipulée gracieusement. La fonction imprime la description d'erreur à stderr.
Reliure la prise
Ensuite, nous lions la socket à une adresse et un port locaux. Les clients se connecteront à cette adresse.
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // Bind to all local interfaces
server_addr.sin_port = htons(8080); // Port number
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
INADDR ANY[ permet au serveur d'accepter les connexions sur n'importe quelle adresse IP assignée à l'hôte. Le numéro de port (8080 dans cet exemple) est choisi pour éviter des services bien connus; les valeurs supérieures à 1024 sont typiques pour les programmes de niveau utilisateur.
Écouter des connexions
Après la liaison, le serveur doit écouter les connexions entrantes. L'appel place la socket dans un mode passif où elle va faire la file des requêtes de connexion entrantes.
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
Le second argument (5) est l'arriéré – la longueur maximale de la file d'attente des connexions en attente. Une valeur plus élevée peut être utilisée pour les serveurs occupés, mais le système impose une limite supérieure (souvent ].
Accepter les connexions client
Pour accepter un client, nous appelons dans une boucle. Il retourne un nouveau descripteur de fichier socket pour ce client spécifique, tandis que la socket serveur d'origine continue à écouter.
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
// In a real server, handle the error without exiting
continue;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("New connection from %s:%d\n", client_ip, ntohs(client_addr.sin_port));
Pour chaque client accepté, nous obtenons l'IP et le port du client. Ces informations sont utiles pour l'enregistrement et le contrôle d'accès potentiel. La prise cliente est alors prête pour le transfert de données.
Traitement des données d'un client
Une fois qu'un client est connecté, le serveur doit pouvoir lire les messages et les diffuser. Pour un simple serveur qui gère un client à la fois, le modèle typique est d'appeler sur la prise client. Cependant, pour vraiment soutenir plusieurs clients, vous avez besoin de la cohérence.
Développement du client
Le client est plus simple : il crée une socket, se connecte au serveur, puis entre dans une boucle où il lit l'entrée de l'utilisateur et l'envoie au serveur, tout en recevant simultanément des messages diffusés par le serveur.
Création et connexion
Tout d'abord, la socket client est créé exactement comme le serveur l'a fait. Ensuite, il se connecte en utilisant l'adresse IP du serveur et le port:
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
// Convert IP address from text to binary
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("invalid address / address not supported");
close(sock);
exit(EXIT_FAILURE);
}
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
close(sock);
exit(EXIT_FAILURE);
}
Ici convertit la chaîne d'adresse IP lisible par l'homme en forme binaire. Pour le code de production, vous pouvez résoudre les noms d'hôte avec .
Boucle de change
Après la connexion, le client doit envoyer des messages tapés par l'utilisateur et recevoir des messages du serveur simultanément. Une approche commune est d'utiliser deux threads : l'un pour la lecture depuis stdin et l'envoi, l'autre pour la réception depuis la socket. Vous pouvez également utiliser des E/S non-bloquants ou l'appel système . Un client minimal qui envoie seulement des messages (et ignore la réception) est facile, mais pour une vraie conversation, nous avons besoin d'une communication bidirectionnelle.
Le squelette suivant utilise pour créer un processus enfant qui gère les messages entrants pendant que le parent gère l'extrait. Pour le threading, préférez pour la portabilité.
// Fork to handle send and receive separately
if (fork() == 0) {
char buffer[1024];
while (1) {
memset(buffer, 0, sizeof(buffer));
int n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
printf("Server disconnected\n");
break;
}
printf("%s", buffer);
}
close(sock);
exit(0);
} else {
char message[1024];
while (fgets(message, sizeof(message), stdin) != NULL) {
send(sock, message, strlen(message), 0);
memset(message, 0, sizeof(message));
}
close(sock);
}
Ce modèle fonctionne, mais dans une application réelle, vous devriez gérer correctement le cadrage des messages, car TCP est un protocole de flux – les messages peuvent être divisés ou concaténés. Une solution simple est d'utiliser un délimiteur tel que ou d'envoyer la longueur du message en premier.
Gestion de plusieurs clients
Le serveur doit gérer plusieurs clients à la fois. Deux méthodes populaires sont multi-threading (un thread par client) et I/O multiplexing[ (en utilisant ] ou ).
Utilisation de fils (pthread)
Dans le modèle threaded, la boucle principale accepte un client et crée un nouveau thread dédié à ce client. Le thread gère la réception des messages et leur diffusion. Ceci est conceptuellement simple et fonctionne bien pour des dizaines de clients, mais le thread-over peut devenir un problème avec des milliers de connexions.
void *handle_client(void *arg) {
int client_fd = *(int*)arg;
free(arg); // Free the heap‑allocated int
char buffer[1024];
while (1) {
memset(buffer, 0, sizeof(buffer));
int n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
// Client disconnected
break;
}
// Broadcast to all other clients
// (requires a mutex‑protected list of client fds)
}
close(client_fd);
return NULL;
}
while (1) {
int *pclient = malloc(sizeof(int));
*pclient = accept(server_fd, ...);
pthread_t tid;
pthread_create(&tid, NULL, handle_client, pclient);
pthread_detach(tid); // Automatically clean up on exit
}
N'oubliez pas de protéger les données partagées (comme la liste des sockets connectés) avec un mutex. Aussi, attention avec la sécurité du thread de – il est généralement sûr, mais vous devez éviter les écritures simultanées à la même socket.
Utilisation de select()
L'appel système permet à un seul thread de surveiller plusieurs descripteurs de fichiers pour détecter la lisibilité, la lisibilité ou les erreurs. Cela évite le surf de nombreux threads et est efficace pour un nombre modéré de connexions. Le serveur maintient un ensemble de descripteurs de fichiers (serveur socket + toutes les sockets client). Lorsque retourne, nous itérerons à travers le jeu pour trouver quel descripteur est prêt.
fd_set readfds;
int max_fd = server_fd;
while (1) {
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
for (int i = 0; i < client_count; i++) {
FD_SET(clients[i], &readfds);
if (clients[i] > max_fd) max_fd = clients[i];
}
int activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (activity < 0) { perror("select"); break; }
if (FD_ISSET(server_fd, &readfds)) {
// Accept new client
int new_socket = accept(server_fd, ...);
clients[client_count++] = new_socket;
}
for (int i = 0; i < client_count; i++) {
int sd = clients[i];
if (FD_ISSET(sd, &readfds)) {
char buffer[1024];
int n = recv(sd, buffer, sizeof(buffer), 0);
if (n <= 0) {
// Remove client
close(sd);
clients[i] = clients[--client_count];
i--;
} else {
// Broadcast buffer to all other clients
}
}
}
}
Un avantage de est la portabilité, mais elle est limitée par (habituellement 1024). Pour une plus grande évolutivité, utiliser ou (Linux-spécifique). La page de personne select(2) fournit tous les détails.
Mettre en œuvre un protocole de clavardage simple
Pour rendre le chat cohérent, vous avez besoin d'un protocole minimal. Sans protocole, les messages de différents clients peuvent s'interposer sur le fil. Au minimum, décidez d'un format de message. Une approche simple : chaque message est une ligne unique se terminant par . Le serveur prépend le nom de l'expéditeur ou l'ID avant de diffuser. Par exemple :
[Alice] Hello everyone!
[Bob] Hi Alice!
Lorsqu'un client envoie un message, le serveur lit la ligne (en utilisant un délimiteur) et l'envoie ensuite à tous les autres clients, en option avec un préfixe. Le cadrage des messages est critique. Une méthode robuste consiste à envoyer un en-tête de taille fixe qui inclut la longueur du message, suivi de la charge utile.
struct chat_message {
uint32_t length; // in network byte order
char data[1024]; // up to 1024 bytes
};
Le serveur lit exactement 4 octets pour obtenir la longueur, puis lit que de nombreux octets pour les données. Cela garantit que les messages sont reçus en une seule pièce.
Gestion des erreurs et pratiques exemplaires
La programmation en réseau est pleine de cas de pointe. Quelques pratiques essentielles :
- Vérifiez toujours la valeur de retour de , , , , , , ], et fermez les prises en cas de défaillance.
- Utilisez ou pour enregistrer des messages d'erreur significatifs.
- Définissez l'option socket sur la socket du serveur pour éviter les erreurs --Adresse déjà en cours d'utilisation -- lors du redémarrage.
- Poignez des signaux comme (qui peuvent se produire en écrivant à une prise fermée) en les ignorant ou en utilisant .
- Utilisez des E/S non-bloquants ou des timeouts avec pour empêcher le serveur de s'accrocher à un client mort.
- Toujours zéro tampons avant de lire pour vous assurer de ne pas envoyer les données restantes.
- Nettoyer les ressources – fermer les prises et libérer la mémoire allouée – pour éviter les fuites.
Pour une plongée plus profonde dans la manipulation des erreurs et les techniques avancées, lisez la spécification POSIX pour connect() et les fonctions connexes.
Élargir la demande
Une fois que vous avez un chat multiclients en cours de travail, vous pouvez ajouter des fonctionnalités pour le rendre plus utile:
- Authentification de l'utilisateur[ – Les clients fournissent un nom d'utilisateur sur connect; le serveur valide et le stocke.
- Messagerie privée – Autorise - -text -"msg" commande que le serveur n'achemine que vers le client cible.
- Support de la salle / canal – Les clients peuvent rejoindre des salles nommées; les messages ne sont diffusés que dans une salle.
- Transfert de fichiers[ – Envoyer des fichiers binaires en mettant en place un canal de données séparé ou en envoyant des métadonnées de fichiers et des octets bruts sur la même socket.
- Encryptage – Utilisez OpenSSL ou TLS pour sécuriser la communication.
- Interface utilisateur graphique[ – Écrire un client GUI (en utilisant GTK, Qt, ou même un front-end web avec un pont de moteur).
- Stockage continu[ – Enregistrez les messages dans un fichier ou une base de données pour une récupération ultérieure.
Chaque extension enseigne des concepts précieux : conception de protocole, concurrence, sécurité et expérience utilisateur. La fondation que vous construisez maintenant va soutenir n'importe quelle de ces améliorations.
Conclusion
La création d'une application de chat simple à l'aide de sockets C est un excellent projet pratique pour tous ceux qui apprennent à programmer le réseau. Vous avez appris à créer des serveurs et des clients, à gérer plusieurs connexions avec des threads ou , à gérer les données brutes sur TCP et à mettre en place un protocole de chat de base. Les exemples de code fournissent un point de départ que vous pouvez compiler, tester et modifier.
Pour plus de pratique, envisagez de mettre en place un simple serveur HTTP ou un outil de partage de fichiers entre pairs. Les compétences sont directement transférables. Et rappelez-vous – lisez toujours la documentation. La page de manuel socket(7) est votre compagnon constant.