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) :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
//---------------------------------------------------- // 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) :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
//---------------------------------------------------- // 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 :



