基于线程的TCP Echo Server命令行无输出及LIST/GET命令异常停滞问题求助
我仔细检查了你的TCP Echo Server代码,找出了几个导致服务器无输出、处理GET命令时停滞的关键问题,下面逐个分析并给出修复方案:
1. 线程参数传递的竞态问题
在main函数的循环里,你把栈上的client变量地址传给了pthread_create,但主线程会立刻进入下一次循环,覆盖client的值。线程启动后读取的*(int *)arg会拿到错误的文件描述符,导致后续所有操作失效,这也是服务器无输出的核心原因之一。
修复方式:
动态分配内存存储client的值,避免主线程覆盖问题:
// 在main的accept之后 int *client_ptr = malloc(sizeof(int)); *client_ptr = client; if (pthread_create(&child, NULL, Child, client_ptr) != 0) { perror("Thread creation"); free(client_ptr); // 创建失败要释放内存 } else { pthread_detach(child); }
然后在Child函数里记得释放内存:
int client = *(int *)arg; free(arg); // 释放动态分配的内存
2. LIST命令中的无意义死循环
你的LIST命令里写了while(1),这会导致线程无限重复遍历目录,永远无法退出这个循环,既无法处理后续客户端命令,也会让服务器看起来陷入停滞。
修复方式:
直接去掉死循环,遍历一次目录就完成LIST响应,处理完后跳过后续的echo逻辑:
if (strcmp(line,"LIST\n") ==0) { DIR *d = opendir("."); if (d) { struct dirent *dir; while ((dir = readdir(d)) != NULL) { send(client, dir->d_name, strlen(dir->d_name), 0); send(client, "\n", 1, 0); } closedir(d); } else { send(client, "Failed to open directory\n", strlen("Failed to open directory\n"), 0); } continue; // 跳过echo逻辑,等待下一个命令 }
3. 字符串处理缺少终止符
recv函数不会自动给接收的缓冲区添加\0,但strcmp、StartsWith、printf("%s")这些函数都依赖字符串的\0结尾。这会导致字符串比较失败(比如strcmp(line,"LIST\n")永远不成立),服务器无法识别LIST命令,也会让printf输出乱码或者无输出。
修复方式:
每次recv成功后,立刻给缓冲区添加终止符:
bytes_read = recv(client, line, sizeof(line)-1, 0); // 留一个位置给'\0' if (bytes_read > 0) { line[bytes_read] = '\0'; // 添加字符串终止符 printf("Received: %s", line); // 现在printf能正常输出了 // 后续的命令判断逻辑... }
4. GET命令未实现核心逻辑
当前GET命令只是printf(line),没有读取文件并发送给客户端,而且处理完GET后还会执行后续的send(client, line, bytes_read, 0),把GET命令本身回传给客户端,同时因为没有处理文件,客户端会一直等待响应,看起来服务器停滞。
修复方式:
解析GET命令中的文件名,读取文件内容并发送,处理完成后跳过echo逻辑:
if (StartsWith(line,"GET ")) { // 注意加空格,避免匹配到GET开头的其他字符串 char filename[DEFAULT_BUFLEN]; sscanf(line, "GET %s", filename); // 解析文件名 FILE *fp = fopen(filename, "r"); if (fp == NULL) { char err_msg[DEFAULT_BUFLEN]; snprintf(err_msg, sizeof(err_msg), "Error: File %s not found\n", filename); send(client, err_msg, strlen(err_msg), 0); } else { char file_buf[DEFAULT_BUFLEN]; size_t read_bytes; while ((read_bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0) { send(client, file_buf, read_bytes, 0); } fclose(fp); send(client, "\nFile transfer completed\n", strlen("\nFile transfer completed\n"), 0); } continue; // 跳过echo逻辑 }
另外,优化StartsWith函数避免重复计算字符串长度:
bool StartsWith(const char *a, const char *b) { size_t len_b = strlen(b); return strncmp(a, b, len_b) == 0; }
修复后的完整代码
#include <stdlib.h> #include <errno.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #include <sys/socket.h> #include <resolv.h> #include <arpa/inet.h> #include <pthread.h> #include <stdbool.h> #include <dirent.h> #include <stdio.h> /* Definations */ #define DEFAULT_BUFLEN 1024 #define PORT 1888 void PANIC(char* msg); #define PANIC(msg) { perror(msg); exit(-1); } bool StartsWith(const char *a, const char *b) { size_t len_b = strlen(b); return strncmp(a, b, len_b) == 0; } void* Child(void* arg) { char line[DEFAULT_BUFLEN]; ssize_t bytes_read; int client = *(int *)arg; free(arg); // 释放动态分配的内存 send(client, "Welcome to Vepa Server\n", strlen("Welcome to Vepa Server\n"), 0); do { bytes_read = recv(client, line, sizeof(line)-1, 0); // 留一个字节给终止符 if (bytes_read > 0) { line[bytes_read] = '\0'; // 添加字符串终止符 printf("Received from client: %s", line); // 处理LIST命令 if (strcmp(line, "LIST\n") == 0) { DIR *d = opendir("."); if (d) { struct dirent *dir; while ((dir = readdir(d)) != NULL) { send(client, dir->d_name, strlen(dir->d_name), 0); send(client, "\n", 1, 0); } closedir(d); } else { char err_msg[] = "Failed to open current directory\n"; send(client, err_msg, strlen(err_msg), 0); } continue; // 跳过后续的echo逻辑,等待下一个命令 } // 处理GET命令 if (StartsWith(line, "GET ")) { char filename[DEFAULT_BUFLEN]; if (sscanf(line, "GET %s", filename) == 1) { FILE *fp = fopen(filename, "r"); if (fp == NULL) { char err_msg[DEFAULT_BUFLEN]; snprintf(err_msg, sizeof(err_msg), "Error: File '%s' does not exist\n", filename); send(client, err_msg, strlen(err_msg), 0); } else { char file_buf[DEFAULT_BUFLEN]; size_t read_bytes; while ((read_bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0) { send(client, file_buf, read_bytes, 0); } fclose(fp); char success_msg[] = "\nFile transfer completed successfully\n"; send(client, success_msg, strlen(success_msg), 0); } } else { char err_msg[] = "Error: Invalid GET command format. Use 'GET filename'\n"; send(client, err_msg, strlen(err_msg), 0); } continue; // 跳过echo逻辑 } // Echo逻辑:把收到的内容回传给客户端 if (send(client, line, bytes_read, 0) < 0) { printf("Send failed\n"); break; } } else if (bytes_read == 0) { printf("Connection closed by client\n"); break; } else { printf("Recv failed: %s\n", strerror(errno)); break; } } while (bytes_read > 0); close(client); return NULL; } int main(int argc, char *argv[]) { int sd, opt, optval; struct sockaddr_in addr; unsigned short port = 0; while ((opt = getopt(argc, argv, "p:")) != -1) { switch (opt) { case 'p': port = atoi(optarg); break; default: fprintf(stderr, "Usage: %s [-p port]\n", argv[0]); exit(EXIT_FAILURE); } } if ((sd = socket(PF_INET, SOCK_STREAM, 0)) < 0) PANIC("Socket"); addr.sin_family = AF_INET; addr.sin_port = (port > 0) ? htons(port) : htons(PORT); addr.sin_addr.s_addr = INADDR_ANY; // 设置SO_REUSEADDR选项 optval = 1; setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if (bind(sd, (struct sockaddr*)&addr, sizeof(addr)) != 0) PANIC("Bind"); if (listen(sd, SOMAXCONN) != 0) PANIC("Listen"); printf("System ready on port %d\n", ntohs(addr.sin_port)); while (1) { int client; socklen_t addr_size = sizeof(addr); pthread_t child; client = accept(sd, (struct sockaddr*)&addr, &addr_size); if (client < 0) { perror("Accept failed"); continue; } printf("Connected: %s:%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); // 动态分配内存存储client文件描述符,避免竞态问题 int *client_ptr = malloc(sizeof(int)); if (client_ptr == NULL) { perror("Malloc failed"); close(client); continue; } *client_ptr = client; if (pthread_create(&child, NULL, Child, client_ptr) != 0) { perror("Thread creation failed"); free(client_ptr); close(client); } else { pthread_detach(child); /* 与主线程分离,线程结束后自动释放资源 */ } } return 0; }
验证修复效果
- 服务器命令行现在会正常输出客户端连接信息和收到的命令。
- 发送
LIST命令,服务器会返回当前目录的文件列表。 - 发送
GET filename.txt命令,服务器会读取文件内容并发送给客户端,文件不存在时返回错误信息。 - 处理完命令后,服务器不会停滞,能继续接收客户端的下一个命令。
内容的提问来源于stack exchange,提问作者vepa

