You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:40:48