一个TCP/IP服务器如何处理多个客户端连接(使用C++)?

5
我希望用C++编写一个TCP/IP服务器(使用bind()accept()等),能够同时处理多个客户端的连接。 我已经阅读了一些相关主题,每个人都建议采用以下方法(下面是简陋的伪代码):
set up server, bind it

while (1) {
    accept connection
    launch new thread to handle it
}

这段代码在拥有多线程的机器上可以正常运行。但是我的目标系统是没有硬件线程的单核心机器。为了进行一个小小的测试,我尝试在该系统上通过 std::thread 启动多个线程,但它们之间却是按顺序执行的,没有并行效果 :(

这使得上述算法无法实现。我是说,我相信它可以完成,但我不知道该如何实现,所以我真的很希望得到任何帮助。


你的目标操作系统是什么? - jxh
2
它可以在Linux上运行,但我真的想避免任何平台相关的代码。我更喜欢解决方案是100%可移植的(因此仅使用标准库或大多数平台都可用的库)。 - notadam
3
你正在寻找select函数。 - Captain Obvlious
1
当你尝试使用线程时,你需要解释一下哪些地方出了问题。如果你正在使用事件多路复用器,我建议使用poll而不是select,因为它的语义更容易理解。但是,你应该使用一些更高级别的接口或库,比如libevent之类的东西。然而,线程应该是可以工作的,并且它是最容易让人理解的,所以请解释一下,附上一些示例代码,说明某些东西没有按照你的预期工作。 - jxh
@adam10603 你有想到解决的办法吗?我试着使用了select函数,但对于并发客户端来说并没有起作用。 - Enjal Parajuli
显示剩余2条评论
2个回答

22
我猜测你的实际代码长这样。
void new_connection (int sock) {
    //...handle new connection
}

void accept_loop (int listen_sock) {
    int new_sock;

    while ((new_sock = accept(listen_sock, 0, 0)) != -1) {
        std::thread t(new_connection, new_sock);
    }
}

这段代码的问题在于当循环重新开始时,线程的析构函数被调用。由于析构函数会检测线程上下文仍然处于活动状态,因此会导致异常抛出。
为避免这个问题,您可以将线程对象从活动上下文中分离。
    while ((new_sock = accept(listen_sock, 0, 0)) != -1) {
        std::thread t(new_connection, new_sock);
        t.detach();
    }

以下是一个基本完整的示例(没有错误检查)。此程序创建一个接受套接字,用于服务器规范(对于特定接口,“主机:端口”,或对于任何接口,“:端口”)。
请注意保留HTML标记。
int make_accept_sock (const char *servspec) {
    const int one = 1;
    struct addrinfo hints = {};
    struct addrinfo *res = 0, *ai = 0, *ai4 = 0;
    char *node = strdup(servspec);
    char *service = strrchr(node, ':');
    int sock;

    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    *service++ = '\0';
    getaddrinfo(*node ? node : "0::0", service, &hints, &res);
    free(node);

    for (ai = res; ai; ai = ai->ai_next) {
        if (ai->ai_family == PF_INET6) break;
        else if (ai->ai_family == PF_INET) ai4 = ai;
    }
    ai = ai ? ai : ai4;

    sock = socket(ai->ai_family, SOCK_STREAM, 0);
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    bind(sock, ai->ai_addr, ai->ai_addrlen);
    listen(sock, 256);
    freeaddrinfo(res);

    return sock;
}

接受循环程序创建监听套接字,然后启动线程来处理每个新的传入连接。

void accept_loop (const char *servspec) {
    int sock = make_accept_sock(servspec);

    for (;;) {
        int new_sock = accept(sock, 0, 0);
        std::thread t(new_connection, new_sock);
        t.detach();
    }
}

新的连接处理程序每秒钟输出一个点(.)和一个换行符。可以在这个问题的答案中找到isclosed()函数。
void new_connection (int sock) {
    ssize_t r;
    while (!isclosed(sock)) {
        r = send(sock, ".\n", 2, 0);
        if (r < 0) break;
        sleep(1);
    }
    close(sock);
}

然后主函数将所有内容结合在一起。

int main (int argc, char *argv[])
{
    const char *server = ":11111";

    signal(SIGPIPE, SIG_IGN);

    if (argc > 1) server = argv[1];

    accept_loop(server);

    return 0;
}

4
你不需要线程,而是需要异步或“事件驱动”编程。如果你想要跨平台,可以使用select(),如果你在Linux上并且想支持数千个客户端,可以使用epoll()。
但你不需要从头开始实现这一切 - 你可以使用Boost ASIO(其中一些可能成为C++17的一部分)或类似libeventlibevlibuv的C库。它们将为您处理许多微妙的细节,并帮助您更快地进入应用程序的真正业务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接