Intro :
Pour concevoir un jeu vidéo multijoueur il vous faut utiliser dans le programme C++ des objets qu’on appelle sockets.
Ceci constitue la deuxième partie de l’ensemble des articles concernant les sockets.
Dans cette partie nous allons concevoir un petit chat en entrant du texte dans la console côté client et côté serveur.
Prérequis :
– Savoir un peu lire et écrire du C++
– Être sous Windows.
– Avoir suivi la première partie de ce tutoriel
Explications :
1 – Tout d’abord nous allons écrire un petit programme de chat par écrit à travers les entrées clavier de l’utilisateur dans la console.
2 – En deuxième partie, j’énumérerais l’ordre d’appel des fonctions sockets selon le rôle du programme (client ou serveur).
1 – Petit chat client / serveur
Voici le code du chat côté client (ChatClient.cpp) :
//----------------------------------------------------
// Auteur : Clément Profit
// Nom du fichier : ChatClient.cpp
// Date de création : Avril 2015
// Description : Une implémentation d'un client de chat
//----------------------------------------------------
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define PORT 23
// On lie la librairie des sockets Windows
#pragma comment(lib, "ws2_32.lib")
// Structure utilisée pour envoyer ces deux paramètres à la fonction
// thread ; étant donné que l'on ne peut qu'envoyer qu'un seul
// paramètre aux fonctions thread
struct ThreadParam
{
bool bQuit;
int server_sock;
};
// Message à envoyer au serveur
struct ChatMessage
{
// Soit c'est un message texte, soit c'est une requête pour cloturer / quitter
// la connexion
enum MessageType {CHAT_MESSAGE, CHAT_QUIT};
ChatMessage(MessageType message, char* information)
{
message_type = message;
strcpy_s(sInfo, information);
}
ChatMessage() {};
MessageType message_type;
// On utilise un tableau de caractères à taille fixe
// car on ne peut envoyer un pointeur char* sur une machine distante
// (le pointeur étant lié uniquement à la mémoire de la machine émettrice)
char sInfo[4096];
};
// Fonction pour obtenir une chaîne de caractères tappée
// depuis le clavier de l'utilisateur
char* get_string_input()
{
char* message = new char[4096];
// La fonction _getch() permet de tapper un caractère
// sans l'afficher dans la console
char ch = _getch();
// Si on tappe la touche entrée on ne fait rien
if (ch == 13)
{
return nullptr;
}
unsigned int length = 0;
// On tappe un caractère par tour de boucle
while (ch != 13) // Le caractère 13 est la touche entrée
{
message[length] = ch;
length++;
if (length > 4096)
{
break;
}
ch = _getch();
}
// A la fin du message, on met un indicateur de fin de chaîne de caractère
// pour finaliser la chaîne
message[length] = '\0';
return message;
}
// Thread pour entrer des messages à partir du clavier
DWORD WINAPI get_console_message(LPVOID lpParameter)
{
// On fait un cast pour obtenir notre paramètre
ThreadParam* param = (ThreadParam*)lpParameter;
printf("Saisissez un message.\n");
char* sMessage = nullptr;
// A chaque tour de boucle correspond un message tappé
while(true)
{
// On obtient le message tappé
sMessage = get_string_input();
if (sMessage == nullptr)
{
continue;
}
// Si l'on tappe "quit", le programme s'arrête
if (strcmp(sMessage, "quit") == 0)
{
printf("Au revoir !\n");
system("pause");
// On envoie un ChatMessage de sortie
// pour indiquer d'arrêter le programme de la machine
// distante
ChatMessage quitMessage(ChatMessage::CHAT_QUIT, "");
send(param->server_sock, (char*)&quitMessage, sizeof(quitMessage), 0);
// Nous forçons aussi l'arrêt de notre programme sur la machine locale
param->bQuit = true;
break;
};
// On envoie un ChatMessage de texte corresopodant à celui tappé
ChatMessage chatMessage(ChatMessage::CHAT_MESSAGE, sMessage);
send(param->server_sock, (char*)&chatMessage, sizeof(ChatMessage), 0);
printf("[Vous]# %s\n", sMessage);
}
delete sMessage;
return 0;
}
/** Client **/
int main(void)
{
// Il faut appeler ces deux instructions avant toute
// utilisation de socket
WSADATA WSAData;
int error = WSAStartup(MAKEWORD(2,2), &WSAData);
// Voici nos structures de socket
SOCKET sock;
SOCKADDR_IN server_in;
// Si les sockets Windows fonctionnent
if (!error)
{
// Création de la socket
sock = socket(AF_INET, SOCK_STREAM, 0);
// On passe en mode non bloqué, c'est-à-dire que les fonctions recv() et send()
// ne bloquent pas quand un message n'a pas été envoyé ou reçu
unsigned long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
// Configuration de la connexion
// On peut changer ici l'adresse d'un serveur non local
server_in.sin_addr.s_addr = inet_addr("127.0.0.1");
server_in.sin_family = AF_INET;
server_in.sin_port = htons(PORT);
printf("Tentative de connexion au serveur (tappez exit pour quitter)...\n");
// On se connecte au serveur
error = connect(sock, (SOCKADDR*)&server_in, sizeof(server_in));
if (error != SOCKET_ERROR)
{
printf("Connection à %s sur le port %d\n", inet_ntoa(server_in.sin_addr), htons(server_in.sin_port));
}
// Structure de paramètre pour envoyer
// plus d'une variable à la fonction thread
ThreadParam param;
param.bQuit = false;
param.server_sock = sock;
// Création d'un thread pour effectuer la lecture d'un message indépendamment
// de l'écoute par la fonction recv
CreateThread(nullptr, 0, &get_console_message, (void*)¶m, 0, nullptr);
ChatMessage* message = nullptr;
// Comme les packets réseaux reçus peuvent être de différentes tailles
// et qu'ils sont reçus par segments,
// nous avons besoin d'un buffer secondaires pour lui ajouter
// les segments par segments
int iTotalBytes = sizeof(ChatMessage);
int iCurrentBytes = 0;
char* buffer = new char[iTotalBytes];
char* pRecvBuffer = new char[iTotalBytes];
while (!param.bQuit)
{
// On recoit des données qui peuvent être un segment
int iBytes = recv(sock, buffer, iTotalBytes, 0);
if (iBytes != SOCKET_ERROR)
{
// On rajoute le segment reçu dans le buffer à chaque fois
// que des données ont été reçues plus haut
memcpy(pRecvBuffer + iCurrentBytes, buffer, iBytes);
iCurrentBytes += iBytes;
// Dès qu'on a reçu le message au complet
// c'est-à-dire dès que la taille du buffer a atteint
// la taille du message
if (iCurrentBytes >= iTotalBytes)
{
message = (ChatMessage*) pRecvBuffer;
// Si l'on recoit un message qui nous indique de quitter
if (message->message_type == ChatMessage::CHAT_QUIT)
{
wprintf(L"Le serveur s'est deconnecté.\n");
system("pause");
// On quitte la boucle
break;
}
if (message->sInfo != '\0')
{
// On affiche le message reçu depuis le serveur
printf("[Serveur]# : %s\n", message->sInfo);
}
iCurrentBytes = 0;
}
}
}
// On ferme la socket
closesocket(sock);
// Termine l'utilisation de Winsock 2
WSACleanup();
}
return EXIT_SUCCESS;
}
Voici le code du chat côté serveur (ChatServeur.cpp) :
//----------------------------------------------------
// Auteur : Clément Profit
// Nom du fichier : ChatServeur.cpp
// Date de création : Avril 2015
// Description : Une implémentation d'un serveur de chat
//----------------------------------------------------
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>
#define PORT 23
// On lie la librairie des sockets Windows
#pragma comment(lib, "ws2_32.lib")
// Structure utilisée pour envoyer ces deux paramètres à la fonction
// thread ; étant donné que l'on ne peut qu'envoyer qu'un seul
// paramètre aux fonctions thread
struct ThreadParam
{
bool bQuit;
int client_sock;
};
// Message à envoyer au serveur
struct ChatMessage
{
// Soit c'est un message texte, soit c'est une requête pour cloturer / quitter
// la connexion
enum MessageType {CHAT_MESSAGE, CHAT_QUIT};
ChatMessage(MessageType message, char* information)
{
message_type = message;
strcpy_s(sInfo, information);
}
ChatMessage() {};
MessageType message_type;
// On utilise un tableau de caractères à taille fixe
// car on ne peut envoyer un pointeur char* sur une machine distante
// (le pointeur étant lié uniquement à la mémoire de la machine émettrice)
char sInfo[4096];
};
// Si un erreur a lieu, on affiche la raison
// puis on quitte
void exit_on_fail(int sock_err)
{
if (sock_err == SOCKET_ERROR)
{
printf("Socket Error Code = %i", WSAGetLastError());
system("pause");
exit(EXIT_FAILURE);
}
}
// Fonction pour obtenir une chaîne de caractères tappée
// depuis le clavier de l'utilisateur
char* get_string_input()
{
char* message = new char[4096];
// La fonction _getch() permet de tapper un caractère
// sans l'afficher dans la console
char ch = _getch();
// Si on tappe la touche entrée on ne fait rien
if (ch == 13)
{
return nullptr;
}
unsigned int length = 0;
// On tappe un caractère par tour de boucle
while (ch != 13) // Le caractère 13 est la touche entrée
{
message[length] = ch;
length++;
if (length > 4096)
{
break;
}
ch = _getch();
}
// A la fin du message, on met un indicateur de fin de chaîne de caractère
// pour finaliser la chaîne
message[length] = '\0';
return message;
}
// Thread pour entrer des messages à partir du clavier
DWORD WINAPI get_console_message(LPVOID lpParameter)
{
// On fait un cast pour obtenir notre paramètre
ThreadParam* param = (ThreadParam*)lpParameter;
printf("Saisissez un message.\n");
char* sMessage = nullptr;
// A chaque tour de boucle correspond un message tappé
while(true)
{
// On obtient le message tappé
sMessage = get_string_input();
if (sMessage == nullptr)
{
continue;
}
// Si l'on tappe "exit", le programme s'arrête
if (strcmp(sMessage, "quit") == 0)
{
printf("Au revoir !\n");
system("pause");
// On envoie un ChatMessage de sortie
// pour indiquer d'arrêter le programme de la machine
ChatMessage quitMessage(ChatMessage::CHAT_QUIT, "");
send(param->client_sock, (char*)&quitMessage, sizeof(ChatMessage), 0);
// Nous forçons aussi l'arrêt de notre programme sur la machine locale
param->bQuit = true;
break;
};
// On envoie un ChatMessage de texte correspondant à celui tappé
ChatMessage chatMessage(ChatMessage::CHAT_MESSAGE, sMessage);
send(param->client_sock, (char*)&chatMessage, sizeof(ChatMessage), 0);
printf("[Vous]# %s\n", sMessage);
}
delete sMessage;
return 0;
}
/** Serveur **/
int main(void)
{
// Il faut appeler ces deux instructions avant toute
// utilisation de socket
WSADATA WSAData;
int error = WSAStartup(MAKEWORD(2,2), &WSAData);
SOCKET sock;
SOCKET client_sock;
// Voici nos structures de socket
SOCKADDR_IN server_in;
SOCKADDR_IN client_in;
int recsize = sizeof(client_in);
int sock_err;
// Si les sockets Windows fonctionnent
if (!error)
{
// Création de la socket
sock = socket(AF_INET, SOCK_STREAM, 0);
// On passe en mode non bloqué, c'est-à-dire que les fonctions recv() et send()
// ne bloquent pas quand un message n'a pas été envoyé ou reçu
unsigned long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
// Si la socket est valide
if (sock != INVALID_SOCKET)
{
printf("Bienvenu sur le chat (tappez exit pour quitter) !\n\nLe serveur est maintenant ouvert en mode TCP/IP sur le port %d\n\n", PORT);
// Configuration de la socket
server_in.sin_addr.s_addr = htonl(INADDR_ANY);
server_in.sin_family = AF_INET;
server_in.sin_port = htons(PORT);
// On configure le serveur
sock_err = bind(sock, (SOCKADDR*)&server_in, sizeof(server_in));
exit_on_fail(sock_err);
sock_err = listen(sock, 5);
exit_on_fail(sock_err);
printf("Patientez pendant que le client se connecte...\n");
// On boucle pour attendre la connexion d'un client
while (true)
{
client_sock = accept(sock, (SOCKADDR*)&client_in, &recsize);
// Si un client se connecte
if (client_sock != SOCKET_ERROR)
{
printf("\nUn client se connecte avec la socket %d de %s:%d\n", client_sock, inet_ntoa(client_in.sin_addr), htons(client_in.sin_port));
break;
}
}
// Structure de paramètre pour envoyer
// plus d'une variable à la fonction thread
ThreadParam param;
param.bQuit = false;
param.client_sock = client_sock;
// Création d'un thread pour effectuer la lecture d'un message indépendamment
// de l'écoute de la socket
CreateThread(nullptr, 0, &get_console_message, (void*)¶m, 0, nullptr);
ChatMessage* message = nullptr;
// Comme les packets réseaux reçus peuvent être de différentes tailles
// et qu'ils sont reçus par segments,
// nous avons besoin d'un buffer secondaires pour lui ajouter
// les segments par segments
int iTotalBytes = sizeof(ChatMessage);
int iCurrentBytes = 0;
char* buffer = new char[iTotalBytes];
char* pRecvBuffer = new char[iTotalBytes];
while (!param.bQuit)
{
// On recoit des données qui peuvent être un segment
int iBytes = recv(client_sock, buffer, iTotalBytes, 0);
if (iBytes != SOCKET_ERROR)
{
// On rajoute le segment reçu dans le buffer à chaque fois
// que des données ont été reçues plus haut
memcpy(pRecvBuffer + iCurrentBytes, buffer, iBytes);
iCurrentBytes += iBytes;
// Dès qu'on a reçu le message au complet
// c'est-à-dire dès que la taille du buffer a atteint
// la taille du message
if (iCurrentBytes >= iTotalBytes)
{
message = (ChatMessage*) pRecvBuffer;
// Si l'on recoit un message qui nous indique de quitter
if (message->message_type == ChatMessage::CHAT_QUIT)
{
wprintf(L"Le client s'est deconnecté.\n");
system("pause");
// On quitte la boucle
break;
}
if (message->sInfo != '\0')
{
// On affiche le message reçu depuis le client
printf("[Client]# : %s\n", message->sInfo);
}
iCurrentBytes = 0;
}
}
}
// On ferme la socket
closesocket(sock);
}
WSACleanup();
}
return EXIT_SUCCESS;
}
2 – Ordre d’appel des fonctions socket selon le rôle de l’application (client ou serveur) :
Résumé :
Nous avons construit une application client / serveur pour communiquer en messagerie par le biais de la fenêtre console.
Références :


