TCP客户端连接后立即断开求助:卡牌游戏C/S架构移植故障
问题描述
将原有独立卡牌游戏(加载与保存功能异常但不影响连接)移植到TCP服务器/客户端架构中。已将游戏函数复制到cardgamefunctions.h文件,新增模仿原游戏主函数的gameLoop函数,实现了Server.c、cardgamefunctions.h和Client.c三个文件。但客户端连接后会立即断开,多次调试均无法获取反馈。注释掉服务器和客户端中的gameLoop调用及头文件引用后,连接可正常维持,怀疑问题出在gameLoop函数。
服务器文件代码(Server.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> //#include "cardgamefunctions.h" #include <signal.h> #include <sys/wait.h> #define PORT 12345 #define MAX_CLIENTS 10 void handle_client(int client_socket) { char menu[] ="\n=== Card Game Menu ===\n1. Create Account\n2. Login\n3. Start New Game\n4. Save Game\n5. Load Game\n6. Show Score\n7. Exit\nEnter your choice: "; if (send(client_socket, menu, strlen(menu), 0) <= 0) { printf("Failed to send menu to client\n"); close(client_socket); return; } //gameLoop(client_socket); close(client_socket); } void sigchld_handler(int signo) { while (waitpid(-1, NULL, WNOHANG) > 0); // Prevent zombie processes } int main(){ signal(SIGCHLD, sigchld_handler); int server_socket; struct sockaddr_in server_addr, client_addr; socklen_t addr_size; server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } int opt = 1; if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("Setsockopt failed"); exit(EXIT_FAILURE); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); close(server_socket); exit(EXIT_FAILURE); } if (listen(server_socket, MAX_CLIENTS) < 0) { perror("Listen failed"); close(server_socket); exit(EXIT_FAILURE); } printf("Server is listening on port %d\n", PORT); while (1) { addr_size = sizeof(client_addr); int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_size); if (client_socket < 0) { perror("Accept failed"); continue; } printf("Client connected!\n"); if (fork() == 0) { // new for each client close(server_socket); handle_client(client_socket); close(client_socket); exit(0); } else { // Parent process close(client_socket); // Parent process doesn't need the client socket } } return 0; }
头文件中的gameLoop函数(cardgamefunctions.h)
void gameLoop() { int gameInProgress = 0; GameState game; game.player.id = NULL; int choice; do { displayMenu(); int bytes_received = recv(client_socket, &choice, sizeof(choice), 0); if (bytes_received <= 0) { printf("Client disconnected.\n"); break; } switch (choice) { case 1: createAccount(&game.player); break; case 2: login(&game.player); break; case 3: if (game.player.id == NULL) { const char *message = "You need to log in or create an account first to start a new game.\n"; send(client_socket, message, strlen(message), 0); break; } { int answer; const char *newGamePrompt = "Do you want to start a new game?\n1.Yes\n2. No\n"; send(client_socket, newGamePrompt, strlen(newGamePrompt), 0); if (recv(client_socket, &answer, sizeof(answer), 0) <= 0) { printf("Client disconnected.\n"); break; } if(answer != 1 && answer != 2) { send(client_socket, "Invalid input :( Please choose either 1 or 2.\n", 35, 0); fflush(stdin); // Clear input buffer } else if (answer == 1) { DeckSize = DECK_SIZE; freeGame(&game); startNewGame(&game); gameInProgress = 1; send(client_socket, "A new game has started ^.^\n", 25, 0);} else if (answer == 2) { send(client_socket,"Okay, returning to menu! \n", 20, 0); } break; } case 4: if (game.player.id == NULL){ send(client_socket, "You need to log in or create an account to play & save a game!\n", 30, 0); break;} saveGameState(&game, "savefile.txt"); send(client_socket, "Game saved successfully.\n", 25, 0); break; case 5: freeGame(&game); //diwxnw ta prohgoumenws loaded dedomena loadGameState(&game, "savefile.txt"); gameInProgress= 1; send(client_socket, "Game loaded successfully.\n", 25, 0); break; case 6: if (game.player.id == NULL) { send(client_socket, "You need to log in or create an account first.\n", 48, 0); } else { char scoreMessage[100]; snprintf(scoreMessage, sizeof(scoreMessage), "Player Score: %d\n", game.player.score); send(client_socket, scoreMessage, strlen(scoreMessage), 0); } break; case 7: send(client_socket, "Exiting game... Bye bye! ^w^\n", 25, 0); freeGame(&game); gameInProgress = 0; return; default: send(client_socket, "Invalid choice. Please enter a number between 1 and 7.\n", 26, 0); break; } if (gameInProgress == 1){ while (game.player.deckSize > 0 && game.computer.deckSize > 0) { count_round(&game); // Player's turn to play a card Card p1Card = playCard(game.player.deck, &game.player.deckSize, game.player.deckSize); // Computer's turn to play a card Card p2Card = playCard2(game.computer.deck, &game.computer.deckSize); // Compare the cards and update the scores compareRanks(&game, p1Card, p2Card); send (client_socket, "Do you want to continue? (1. Yes / 2. Save and Exit):", 39, 0); int cont; if (recv(client_socket, &cont, sizeof(cont), 0) <= 0) { printf("Client disconnected.\n"); break; } if (cont == 1){continue;} else if (cont == 2) { saveGameState(&game, "savefile.txt"); send(client_socket, "Game saved. Returning to menu...\n", 31, 0); gameInProgress = 0; // Stop the game break; } else { const char *invalidInput = "Invalid choice. Please enter 1 or 2.\n"; send(client_socket, invalidInput, strlen(invalidInput), 0); } } if (game.player.deckSize == 0 || game.computer.deckSize == 0) { char gameOverMessage[256]; snprintf(gameOverMessage, sizeof(gameOverMessage), "Game over! Final Score - You: %d, Computer: %d\n", game.player.score, game.computer.score); send(client_socket, gameOverMessage, strlen(gameOverMessage), 0); gameInProgress = 0; } } } while (choice != 7); freeGame(&game); close(client_socket); }
客户端文件代码(Client.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> //#include "cardgamefunctions.h" #define PORT 12345 int main() { int client_socket; struct sockaddr_in server_addr; char buffer[1024]; int choice; client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = INADDR_ANY; if (connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Connection to server failed"); close(client_socket); exit(EXIT_FAILURE); } printf("Connected to server!\n"); client_socket = client_socket; while(1) { memset(buffer, 0, sizeof(buffer)); int bytes_received = recv(client_socket, buffer, sizeof(buffer), 0); if (bytes_received <= 0) { printf("Error receiving data from server. bytes_received: %d\n", bytes_received); close(client_socket); break; } if (bytes_received == 0) { printf("Server closed connection.\n"); break; } printf("%s", buffer); scanf("%d", &choice); if (send(client_socket, &choice, sizeof(choice), 0) <= 0) { printf("Failed to send choice.\n"); break; } memset(buffer, 0, sizeof(buffer)); bytes_received = recv(client_socket, buffer, sizeof(buffer), 0); if (bytes_received <= 0) { printf("Server disconnected :c.\n"); break; } printf("Server: %s\n", buffer); if (choice == 7 || strncmp(buffer, "exit", 4) == 0) { printf("Exiting...\n"); break; // Exit the loop and close the connection } } close(client_socket); return 0; }
问题根源及修复方案
核心问题点
gameLoop函数未接收客户端套接字参数:当前gameLoop()无参数,但调用时需要传入client_socket,函数内部直接使用未定义的client_socket变量,触发未定义行为导致程序崩溃,进而关闭客户端连接。- 重复发送菜单:
handle_client已发送菜单,gameLoop内部又调用displayMenu()(推测该函数也会发送菜单),导致客户端接收重复数据,协议逻辑混乱。 - 通信协议不匹配:客户端每次发送选择后都会等待服务器响应,但服务器部分操作(如创建账号、登录)未发送任何反馈,导致客户端阻塞在
recv操作,最终超时断开。 - 无效操作:服务器端调用
fflush(stdin),但服务器进程无终端输入,该操作无效且可能引发未定义行为。 - 结构体未初始化:
GameState game局部变量未完整初始化,仅设置game.player.id = NULL可能导致其他字段出现垃圾值。
修复步骤
1. 修正gameLoop函数的参数传递
修改函数定义,接收客户端套接字作为参数,并初始化结构体:
void gameLoop(int client_socket) { int gameInProgress = 0; GameState game; memset(&game, 0, sizeof(GameState)); // 完整初始化游戏状态结构体 game.player.id = NULL; int choice; do { // 移除displayMenu()调用,避免重复发送菜单 int bytes_received = recv(client_socket, &choice, sizeof(choice), 0); if (bytes_received <= 0) { printf("Client disconnected.\n"); break; } switch (choice) { case 1: createAccount(&game.player); // 添加操作结果反馈 send(client_socket, "Account created successfully.\n", 29, 0); break; case 2: if (login(&game.player)) { // 假设login返回登录是否成功 send(client_socket, "Login successful.\n", 18, 0); } else { send(client_socket, "Login failed. Invalid credentials.\n", 34, 0); } break; // 其他case补充缺失的响应,确保客户端每次操作都能收到反馈 case 7: send(client_socket, "Exiting game... Bye bye! ^w^\n", 28, 0); freeGame(&game); gameInProgress = 0; return; // 移除fflush(stdin)调用 } // 游戏进行中的逻辑保持不变 } while (choice != 7); freeGame(&game); close(client_socket); }
2. 修正服务器handle_client函数
恢复gameLoop调用并传递正确参数,移除重复的套接字关闭操作:
void handle_client(int client_socket) { char menu[] ="\n=== Card Game Menu ===\n1. Create Account\n2. Login\n3. Start New Game\n4. Save Game\n5. Load Game\n6. Show Score\n7. Exit\nEnter your choice: "; if (send(client_socket, menu, strlen(menu), 0) <= 0) { printf("Failed to send menu to client\n"); close(client_socket); return; } gameLoop(client_socket); // 恢复调用并传入client_socket // 移除close(client_socket),因为gameLoop最后会关闭套接字 }
3. 修正客户端通信逻辑
调整接收逻辑,匹配服务器的发送节奏,避免不必要的阻塞:
while(1) { memset(buffer, 0, sizeof(buffer)); int bytes_received = recv(client_socket, buffer, sizeof(buffer), 0); if (bytes_received <= 0) { printf("Error receiving data from server. bytes_received: %d\n", bytes_received); close(client_socket); break; } printf("%s", buffer); // 收到退出提示直接退出 if (strstr(buffer, "Exiting game") != NULL) { printf("Exiting...\n"); break; } // 仅在需要输入选择时调用scanf if (strstr(buffer, "Enter your choice") != NULL || strstr(buffer, "Do you want to continue") != NULL || strstr(buffer, "Do you want to start a new game") != NULL) { scanf("%d", &choice); if (send(client_socket, &choice, sizeof(choice), 0) <= 0) { printf("Failed to send choice.\n"); break; } } }
4. 其他注意事项
- 账号数据需存储在服务器端共享存储(文件或数据库),而非本地文件,确保多客户端数据隔离。
- 实现循环收发逻辑,处理
recv/send返回值小于请求长度的情况,保证数据完整传输。 - 检查
createAccount、login等函数的错误处理逻辑,确保异常情况能反馈给客户端。
内容的提问来源于stack exchange,提问作者Ria
相关产品推荐
相关产品推荐

