Ce projet vise à construire un système de messagerie fonctionnel en utilisant des files d'attente de messages et des sockets UDP pour la communication inter-processus, avec une base de données pour le stockage des données utilisateur. Les utilisateurs peuvent s'inscrire, se connecter et échanger des messages.
Architecture du système
Le système se compose d'un client et d'un serveur. Le client initie les requêtes et le serveur y répond. Les fonctionnalités principales du client incluent l'inscription, la connexion et l'envoi/réception de messages. L'inscription et la connexion utilisent une file d'attente de messages pour la communication, tandis que la messagerie instantanée utilise des sockets UDP.
Schéma de la base de données
Les informations utilisateur sont stockées dans une base de données. Voici la structure de la table user :
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '',
`password` varchar(64) NOT NULL DEFAULT '',
`check` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8;
Implémentation du client
Le fichier client.c gère la création des clés de file d'attente de messages et l'envoi de messages basés sur leur type. Il attend ensuite la réponse du serveur.
#include "my_header.h"
Message client_message;
ServerToClientMessage server_response;
static int client_to_server_mq_id;
static int server_to_client_mq_id;
void display_main_menu() {
puts("------- CHAT SYSTEM --------");
puts("| 1: Send Message 2: Receive Message |");
puts("| 3: Exit |");
puts("----------------------------");
}
void display_auth_menu() {
puts("-------- AUTH MENU ---------");
puts("| 1: Register 2: Login |");
puts("| 0: Exit |");
puts("----------------------------");
}
void send_message() {
char buffer[200] = {'\0'};
struct sockaddr_in receiver_addr, self_addr;
int sockfd;
ssize_t bytes_sent;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
self_addr.sin_family = AF_INET;
self_addr.sin_port = htons(5555); // Port for sending/receiving on client side
self_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&self_addr, sizeof(self_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// Simplified receiver address (hardcoded for example)
receiver_addr.sin_family = AF_INET;
receiver_addr.sin_port = htons(8888); // Server's listening port
receiver_addr.sin_addr.s_addr = inet_addr("10.10.3.129"); // Server's IP address
printf("Enter message to send: ");
fgets(buffer, sizeof(buffer), stdin);
bytes_sent = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&receiver_addr, sizeof(receiver_addr));
if (bytes_sent < 0) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Message sent.\n");
close(sockfd);
}
void handle_reply(int sockfd, const struct sockaddr_in *sender_addr) {
char reply_buffer[200] = {'\0'};
ssize_t bytes_sent;
printf("Enter reply message: ");
fgets(reply_buffer, sizeof(reply_buffer), stdin);
bytes_sent = sendto(sockfd, reply_buffer, strlen(reply_buffer), 0, (struct sockaddr *)sender_addr, sizeof(*sender_addr));
if (bytes_sent < 0) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Reply sent.\n");
}
void process_user_choice_reply(char choice, int sockfd, const struct sockaddr_in *sender_addr) {
switch (choice) {
case 'y':
handle_reply(sockfd, sender_addr);
break;
case 'n':
break;
default:
puts("Invalid input!");
break;
}
}
void receive_message() {
struct sockaddr_in sender_addr, self_addr;
socklen_t addr_len = sizeof(sender_addr);
ssize_t bytes_received;
char buffer[200] = {'\0'};
char reply_choice;
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
self_addr.sin_family = AF_INET;
self_addr.sin_port = htons(5555); // Port for receiving messages
self_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&self_addr, sizeof(self_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Waiting for message...\n");
bytes_received = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&sender_addr, &addr_len);
if (bytes_received < 0) {
perror("recvfrom failed");
close(sockfd);
exit(EXIT_FAILURE);
}
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);
printf("Do you want to reply? (y/n): ");
scanf(" %c", &reply_choice);
getchar(); // Consume the newline character
process_user_choice_reply(reply_choice, sockfd, &sender_addr);
close(sockfd);
}
void execute_client_command(int command) {
switch (command) {
case 1:
client_message.type = MSG_TYPE_SEND;
msgsnd(client_to_server_mq_id, &client_message, sizeof(client_message) - sizeof(client_message.type), 0);
send_message();
break;
case 2:
receive_message();
break;
case 3:
exit(EXIT_SUCCESS);
default:
puts("Invalid command!");
break;
}
}
void register_user() {
printf("Enter username: ");
scanf("%s", client_message.username);
printf("Enter password: ");
scanf("%s", client_message.password);
client_message.type = MSG_TYPE_REGISTER;
}
void login_user() {
printf("Enter username: ");
scanf("%s", client_message.username);
printf("Enter password: ");
scanf("%s", client_message.password);
client_message.type = MSG_TYPE_LOGIN;
}
int main(int argc, char *argv[]) {
client_to_server_mq_id = get_client_to_server_mq();
server_to_client_mq_id = get_server_to_client_mq();
int auth_choice;
long response_type = 0;
int operation_type = 0;
while (1) {
display_auth_menu();
scanf("%d", &auth_choice);
getchar();
switch (auth_choice) {
case 0: return EXIT_SUCCESS;
case 1:
register_user();
operation_type = MSG_TYPE_REGISTER_ACK;
break;
case 2:
login_user();
operation_type = MSG_TYPE_LOGIN_ACK;
break;
default:
puts("Invalid choice!");
continue;
}
printf("Sending: User=%s, Pass=%s\n", client_message.username, client_message.password);
msgsnd(client_to_server_mq_id, &client_message, sizeof(client_message) - sizeof(client_message.type), 0);
sleep(1); // Allow server to process
if (msgrcv(server_to_client_mq_id, &server_response, sizeof(server_response) - sizeof(server_response.type), operation_type, IPC_NOWAIT) == -1) {
if (errno == ENOMSG) {
puts("No response from server. Please try again.");
} else {
perror("msgrcv error");
}
continue;
}
if (strcmp(server_response.status, "success") == 0) {
printf("%s\n", server_response.message);
break; // Exit authentication loop on success
} else {
printf("Authentication failed: %s\n", server_response.message);
}
}
// Main chat loop
while (1) {
display_main_menu();
int user_command;
puts("Enter command:");
scanf("%d", &user_command);
getchar();
execute_client_command(user_command);
}
return EXIT_SUCCESS;
}
Note: L'utilisation de clés de file d'attente de messages distinctes et de types de messages différents est cruciale pour séparer les communications.
Implémentation du serveur
Le serveur écoute les requêtes des clients et y répond. Il crée plusieurs processus fils en utilisant vfork et execl pour gérer les différentes tâches : inscription, connexion et messagerie. Un gestionnaire de signaux (SIGINT) est mis en place pour arrêter proprement les processus fils lors de l'interruption du serveur (Ctrl+C).
#include "my_header.h"
pid_t worker_pids[3];
static int client_to_server_mq_id;
static int server_to_client_mq_id;
void signal_handler_sigint(int signum) {
printf("\nShutting down server...\n");
for (int i = 0; i < 3; ++i) {
if (worker_pids[i] > 0) {
kill(worker_pids[i], SIGTERM); // Send SIGTERM first
}
}
// Clean up message queues if necessary
msgctl(client_to_server_mq_id, IPC_RMID, NULL);
msgctl(server_to_client_mq_id, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}
int main(int argc, char *argv[]) {
signal(SIGINT, signal_handler_sigint);
client_to_server_mq_id = get_client_to_server_mq();
server_to_client_mq_id = get_server_to_client_mq();
// Process for handling registrations
worker_pids[0] = vfork();
if (worker_pids[0] == 0) {
execl("./register_handler", "register_handler", NULL);
perror("execl failed for register_handler");
exit(EXIT_FAILURE);
}
// Process for handling logins
worker_pids[1] = vfork();
if (worker_pids[1] == 0) {
execl("./login_handler", "login_handler", NULL);
perror("execl failed for login_handler");
exit(EXIT_FAILURE);
}
// Process for handling chat messages
worker_pids[2] = vfork();
if (worker_pids[2] == 0) {
execl("./chat_handler", "chat_handler", NULL);
perror("execl failed for chat_handler");
exit(EXIT_FAILURE);
}
// Wait for any child process to terminate (e.g., if one crashes)
int status;
while (waitpid(-1, &status, 0) > 0);
return 0;
}
Modules et Interaction avec la Base de Données
L'intercation avec la base de données MySQL est gérée à l'aide de la bibliothèque mysql.h. Il faut s'assurer de lier la bibliothèque dynamique -lmysqlclient lors de la compilation. Pour des ensembles de données plus importants, des optimisations supplémentaires seraient nécessaires.
Gestion de l'inscription
Le processus register_handler reçoit les informations d'inscription (nom d'utilisateur, mot de passe) via une file d'attente de messages. Il insère ensuite ces données dans la table user de la base de données.
#include "my_header.h"
Message reg_request;
ServerToClientMessage reg_response;
static int client_to_server_mq_id;
static int server_to_client_mq_id;
void process_registration() {
MYSQL mysql_conn;
int query_result;
char insert_query[256];
mysql_init(&mysql_conn);
// Parameters for mysql_real_connect need to be configured appropriately
if (mysql_real_connect(&mysql_conn, "localhost", "db_user", "db_password", "chat_db", 0, NULL, 0)) {
printf("Database connection successful.\n");
snprintf(insert_query, sizeof(insert_query),
"INSERT INTO user(name, password) VALUES('%s', '%s')",
reg_request.username, reg_request.password);
printf("Executing SQL: %s\n", insert_query);
query_result = mysql_query(&mysql_conn, insert_query);
if (query_result == 0) {
printf("User '%s' registered successfully.\n", reg_request.username);
strcpy(reg_response.status, "success");
sprintf(reg_response.message, "Welcome, %s!", reg_request.username);
} else {
fprintf(stderr, "Registration failed for user '%s'. Error: %s\n", reg_request.username, mysql_error(&mysql_conn));
strcpy(reg_response.status, "failed");
sprintf(reg_response.message, "Username '%s' already exists or error occurred.", reg_request.username);
}
mysql_close(&mysql_conn);
} else {
fprintf(stderr, "Database connection failed: %s\n", mysql_error(&mysql_conn));
strcpy(reg_response.status, "failed");
strcpy(reg_response.message, "Server error during registration.");
}
}
int main() {
client_to_server_mq_id = get_client_to_server_mq();
server_to_client_mq_id = get_server_to_client_mq();
while (1) {
long message_type = MSG_TYPE_REGISTER;
if (msgrcv(client_to_server_mq_id, ®_request, sizeof(reg_request) - sizeof(reg_request.type), message_type, 0) == -1) {
perror("msgrcv failed in register_handler");
continue;
}
printf("Received registration request: User=%s, Pass=%s\n", reg_request.username, reg_request.password);
sleep(1); // Simulate processing time
process_registration();
reg_response.type = MSG_TYPE_REGISTER_ACK;
msgsnd(server_to_client_mq_id, ®_response, sizeof(reg_response) - sizeof(reg_response.type), 0);
}
return 0;
}
Gestion de la connexion
Le processus login_handler reçoit les informations de connexion. Il interroge la base de données pour vérifier si les identifiants correspondent à un utilisateur existant et renvoie un message de succès ou d'échec.
#include "my_header.h"
Message login_request;
ServerToClientMessage login_response;
static int client_to_server_mq_id;
static int server_to_client_mq_id;
void verify_login() {
MYSQL mysql_conn;
MYSQL_RES *query_result_set;
MYSQL_ROW row;
char select_query[128];
int auth_success = 0;
mysql_init(&mysql_conn);
// Parameters for mysql_real_connect need to be configured appropriately
if (mysql_real_connect(&mysql_conn, "localhost", "db_user", "db_password", "chat_db", 0, NULL, 0)) {
printf("Database connection successful.\n");
snprintf(select_query, sizeof(select_query),
"SELECT name, password FROM user WHERE name='%s'",
login_request.username);
printf("Executing SQL: %s\n", select_query);
if (mysql_query(&mysql_conn, select_query) != 0) {
fprintf(stderr, "Login query failed: %s\n", mysql_error(&mysql_conn));
strcpy(login_response.status, "failed");
strcpy(login_response.message, "Database query error.");
} else {
query_result_set = mysql_store_result(&mysql_conn);
if (query_result_set == NULL) {
fprintf(stderr, "Failed to store result set: %s\n", mysql_error(&mysql_conn));
strcpy(login_response.status, "failed");
strcpy(login_response.message, "Server error processing results.");
} else {
while ((row = mysql_fetch_row(query_result_set)) != NULL) {
printf("Found user: Name=%s, Pass=%s\n", row[0], row[1]);
if ((strcmp(row[0], login_request.username) == 0) && (strcmp(row[1], login_request.password) == 0)) {
auth_success = 1;
break;
}
}
mysql_free_result(query_result_set);
if (auth_success) {
strcpy(login_response.status, "success");
sprintf(login_response.message, "Login successful, %s!", login_request.username);
} else {
strcpy(login_response.status, "failed");
strcpy(login_response.message, "Invalid username or password.");
}
}
}
mysql_close(&mysql_conn);
} else {
fprintf(stderr, "Database connection failed: %s\n", mysql_error(&mysql_conn));
strcpy(login_response.status, "failed");
strcpy(login_response.message, "Server error during login.");
}
}
int main() {
client_to_server_mq_id = get_client_to_server_mq();
server_to_client_mq_id = get_server_to_client_mq();
while (1) {
long message_type = MSG_TYPE_LOGIN;
if (msgrcv(client_to_server_mq_id, &login_request, sizeof(login_request) - sizeof(login_request.type), message_type, 0) == -1) {
perror("msgrcv failed in login_handler");
continue;
}
printf("Received login request: User=%s, Pass=%s\n", login_request.username, login_request.password);
sleep(1); // Simulate processing time
verify_login();
login_response.type = MSG_TYPE_LOGIN_ACK;
msgsnd(server_to_client_mq_id, &login_response, sizeof(login_response) - sizeof(login_response.type), 0);
}
return 0;
}
Gestion de la messagerie
Le processus chat_handler gère les communications par messagerie instantanée en utilisant des sockets UDP. Il reçoit les messages des clients et les relaie aux destinataires appropriés. La logique exacte du routage des messages dépend de l'implémentation spécifique.
Ce projet démontre l'application combinée des files d'attente de messages pour les requêtes de contrôle (authentification) et des sockets pour la communication de données en temps réel (messagerie), offrant une approche robuste pour construire des applications de communication.