如何在Linux下用键盘输入(如Q/-quit)安全终止C++监听TCP服务器
如何让Linux下的C++ TCP服务器通过输入指令安全终止监听
我明白你的需求:不想用Ctrl+C,而是通过输入Q或者-quit这类指令,让处于监听状态的TCP服务器立刻安全终止,不用等客户端连接过来。核心问题在于默认情况下accept()会阻塞程序,导致它没法处理标准输入的指令。下面给你两种实用的实现方案:
方案一:用select()实现IO多路复用(单线程)
这种方式不用多线程,通过select()同时监听TCP监听套接字和标准输入(stdin),只要其中任意一个有事件发生,程序就会立刻响应,完美解决accept()阻塞的问题。
示例代码
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <sys/select.h> int socketFD; bool isRunning = true; int startSocketConnection() { socketFD = socket(AF_INET, SOCK_STREAM, 0); if (socketFD < 0) { perror("socket creation failed"); return -1; } struct sockaddr_in serverAddr; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; serverAddr.sin_port = htons(8080); if (bind(socketFD, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) { perror("bind failed"); close(socketFD); return -1; } if (listen(socketFD, 5) < 0) { perror("listen failed"); close(socketFD); return -1; } std::cout << "Server listening on port 8080. Enter 'Q' or '-quit' to exit.\n"; return 0; } void runServer() { fd_set readFds; int maxFD = std::max(socketFD, 0); // stdin的文件描述符是0 while (isRunning) { FD_ZERO(&readFds); FD_SET(socketFD, &readFds); FD_SET(0, &readFds); // 把标准输入加入监听集合 // 阻塞等待任一文件描述符可读 int activity = select(maxFD + 1, &readFds, NULL, NULL, NULL); if (activity < 0 && errno != EINTR) { perror("select error"); break; } // 处理用户输入指令 if (FD_ISSET(0, &readFds)) { char buffer[256]; memset(buffer, 0, sizeof(buffer)); if (fgets(buffer, sizeof(buffer), stdin) != NULL) { // 去掉输入末尾的换行符 buffer[strcspn(buffer, "\n")] = '\0'; if (strcmp(buffer, "Q") == 0 || strcmp(buffer, "-quit") == 0) { std::cout << "Quit command received. Shutting down server...\n"; isRunning = false; break; } else { std::cout << "Unknown command! Please enter 'Q' or '-quit' to exit.\n"; } } } // 处理客户端连接请求 if (FD_ISSET(socketFD, &readFds)) { struct sockaddr_in clientAddr; socklen_t clientLen = sizeof(clientAddr); int clientFD = accept(socketFD, (struct sockaddr*)&clientAddr, &clientLen); if (clientFD >= 0) { std::cout << "New client connected\n"; // 这里可以添加客户端请求的处理逻辑,比如创建子线程/进程 close(clientFD); // 示例中直接关闭,实际按需处理 } else { // 如果是被信号中断的accept,忽略错误;否则输出错误信息 if (errno != EINTR) { perror("accept error"); } } } } // 清理资源,关闭监听套接字 close(socketFD); std::cout << "Server stopped successfully.\n"; } int main() { if (startSocketConnection() == 0) { runServer(); } return 0; }
关键说明
- IO多路复用:
select()让程序同时监控多个文件描述符,不会因为accept()阻塞而错过用户输入。 - 指令判断:读取用户输入后,去掉换行符再和指令对比,避免因为换行符导致判断失败。
- 安全终止:退出循环后主动关闭监听套接字,确保系统资源被正确释放。
方案二:多线程实现(主线程处理输入,子线程监听连接)
如果你的服务器逻辑比较复杂,多线程方案会更清晰:主线程专门处理用户输入,子线程负责监听客户端连接,用线程安全的标志控制程序退出。
示例代码
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <thread> #include <atomic> int socketFD; std::atomic<bool> isRunning(true); // 原子变量,保证线程间状态修改安全 int startSocketConnection() { socketFD = socket(AF_INET, SOCK_STREAM, 0); if (socketFD < 0) { perror("socket creation failed"); return -1; } struct sockaddr_in serverAddr; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; serverAddr.sin_port = htons(8080); if (bind(socketFD, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) { perror("bind failed"); close(socketFD); return -1; } if (listen(socketFD, 5) < 0) { perror("listen failed"); close(socketFD); return -1; } std::cout << "Server listening on port 8080. Enter 'Q' or '-quit' to exit.\n"; return 0; } void serverListenerThread() { while (isRunning) { struct sockaddr_in clientAddr; socklen_t clientLen = sizeof(clientAddr); int clientFD = accept(socketFD, (struct sockaddr*)&clientAddr, &clientLen); if (clientFD >= 0) { std::cout << "New client connected\n"; close(clientFD); // 示例处理,实际按需扩展 } else { // 非信号中断的错误才处理 if (errno != EINTR) { perror("accept error"); isRunning = false; } } } close(socketFD); std::cout << "Listener thread stopped.\n"; } int main() { if (startSocketConnection() != 0) { return 1; } // 启动监听线程 std::thread listenerThread(serverListenerThread); // 主线程处理用户输入 char buffer[256]; while (isRunning) { memset(buffer, 0, sizeof(buffer)); if (fgets(buffer, sizeof(buffer), stdin) != NULL) { buffer[strcspn(buffer, "\n")] = '\0'; if (strcmp(buffer, "Q") == 0 || strcmp(buffer, "-quit") == 0) { std::cout << "Quit command received. Shutting down server...\n"; isRunning = false; // 关闭监听套接字,唤醒阻塞的accept() shutdown(socketFD, SHUT_RDWR); } else { std::cout << "Unknown command! Please enter 'Q' or '-quit' to exit.\n"; } } } // 等待监听线程结束 listenerThread.join(); std::cout << "Server stopped successfully.\n"; return 0; }
关键说明
- 线程分工:主线程专注处理输入,子线程专注监听连接,逻辑更清晰。
- 线程安全:用
std::atomic<bool>作为终止标志,避免多线程下的竞态条件。 - 唤醒阻塞的accept:调用
shutdown()可以让阻塞在accept()的子线程立刻返回,从而顺利退出循环。
额外注意事项
- 资源清理:不管用哪种方案,终止时都要确保关闭所有打开的套接字(包括已连接的客户端套接字),防止资源泄漏。
- 信号兼容:如果你的服务器需要处理其他信号(比如SIGINT),可以额外添加信号处理函数,确保程序能安全退出。
- 高并发场景:如果需要处理大量客户端,推荐用
epoll代替select(),epoll没有文件描述符数量限制,性能也更好。
内容的提问来源于stack exchange,提问作者BugShotGG
相关产品推荐
相关产品推荐

