从一个服务器监听多个端口

21

在Linux中,是否可能在一个应用程序中绑定和监听多个端口?


1
是的,这是可能的,但你需要使用 select 或线程。 - perreal
8
这类问题的最佳答案是您编写一个小测试程序并“自己试一试”。随着经验的增长,您会越来越经常编写这些小的“测试程序”来解决问题。 - Jonathon Reinhart
如何使用select实现?我不确定如何为一个套接字做多个绑定。 - user2175831
2
不是一个套接字,而是多个套接字。 - Jonathon Reinhart
1
谢谢Jonathon。你的解释(有点)讲得通。 - tink
3个回答

21

对于每个要监听的端口,您需要执行以下操作:

  1. 使用 socket 创建一个单独的套接字。
  2. 使用 bind 将其绑定到相应的端口。
  3. 在套接字上调用 listen,使其设置为监听队列。

此时,您的程序正在侦听多个套接字。为了在这些套接字上接受连接,您需要知道客户端连接到哪个套接字。这就是 select 的作用。恰好我有代码可以做到这一点,下面是在多个套接字上等待连接并返回连接的文件描述符的完整测试示例。远程地址以其他参数返回(缓冲区必须由调用者提供,就像 accept 一样)。

(这里的 socket_type 在 Linux 系统上是 int 的 typedef,而 INVALID_SOCKET-1。这些是因为此代码已移植到 Windows。)

socket_type
network_accept_any(socket_type fds[], unsigned int count,
                   struct sockaddr *addr, socklen_t *addrlen)
{
    fd_set readfds;
    socket_type maxfd, fd;
    unsigned int i;
    int status;

    FD_ZERO(&readfds);
    maxfd = -1;
    for (i = 0; i < count; i++) {
        FD_SET(fds[i], &readfds);
        if (fds[i] > maxfd)
            maxfd = fds[i];
    }
    status = select(maxfd + 1, &readfds, NULL, NULL, NULL);
    if (status < 0)
        return INVALID_SOCKET;
    fd = INVALID_SOCKET;
    for (i = 0; i < count; i++)
        if (FD_ISSET(fds[i], &readfds)) {
            fd = fds[i];
            break;
        }
    if (fd == INVALID_SOCKET)
        return INVALID_SOCKET;
    else
        return accept(fd, addr, addrlen);
}

这段代码没有告诉调用者客户端连接的端口号,但你可以很容易地添加一个int*参数来获取看到传入连接的文件描述符。


我在想是否可以在没有select/poll的情况下实现,但可能没有其他方法。 - Nick

3
你只需要将一个socket绑定到一个端口,然后进行监听和接受连接。绑定的socket是用于服务器的,从accept()返回的fd是用于客户端的。在后者上执行select操作,查找任何有待输入数据的客户端socket。

顺便问一下,你是删除了我的问题的回答还是被管理员删掉了?如果是这样的话有点疯狂,因为相关的评论对其他人也会有用处。 - Stephen Lin
不是我...我觉得那是一个相当不错的对话。 :-/ - K Scott Piel
是的,这很有帮助,甚至知道哪些方法不起作用...这里的管理员太疯狂了;也许你可以编辑一下,结合我们的讨论,并要求将其恢复?你甚至可以说我们已经尝试了所有这些方法,而使用一个魔法数字是所有方法中最好的替代方案,这样就是一个合理的答案,我可以接受(因为那似乎是真正的答案)。由你决定,我无所谓。反正我可能会自己写个回答,因为迄今给出的回答并没有真正解决我的问题。 - Stephen Lin
如果您无法恢复已删除的内容,您可以编写一个新答案,我会接受它,除非/直到有更好的答案出现...虽然失去评论有点糟糕...但您可能更适合编写答案,因为我已经看不到评论了(忘记了所有内容)。 - Stephen Lin

2
在这种情况下,你可能会对 libevent 感兴趣。它将为您完成 select() 的工作,可能使用更好的接口,如 epoll()select() 的巨大缺点是使用 FD_... 宏,将套接字号限制在 fd_set 变量中最大位数(从约100到256)。如果您拥有一个只有2或3个连接的小型服务器,那么没问题。如果您打算在更大的服务器上工作,那么 fd_set 可能很容易被溢出。
此外,使用 select()poll() 可以避免在服务器中使用线程(即,您可以 poll() 您的套接字,并知道是否可以 accept()read()write())。
但是,如果您真的想像 Unix 那样做,那么在调用 accept() 之前,您需要考虑使用 fork()。在这种情况下,您不绝对需要 select()poll()(除非您正在侦听许多 IP/端口并且希望所有子进程都能够回答任何传入的连接,但是这些都有缺点...内核可能在您已经处理请求时向您发送另一个请求,而仅使用 accept(),内核就知道您是否正在忙碌,如果不在 accept() 调用本身中——好吧,它并不完全像这样工作,但作为用户,这就是对您起作用的方式。)
使用 fork(),您可以在主进程中准备套接字,然后在子进程中调用 handle_request() 来调用 accept() 函数。这样,您可以拥有任意数量的端口,每个端口都有一个或多个子进程来监听。这是在 Linux 下真正快速响应任何传入连接的最佳方法(即,作为用户,只要您的子进程等待客户端,这是瞬间完成的)。
void init_server(int port)
{
    int server_socket = socket();
    bind(server_socket, ...port...);
    listen(server_socket);
    for(int c = 0; c < 10; ++c)
    {
        pid_t child_pid = fork();
        if(child_pid == 0)
        {
            // here we are in a child
            handle_request(server_socket);
        }
    }

    // WARNING: this loop cannot be here, since it is blocking...
    //          you will want to wait and see which child died and
    //          create a new child for the same `server_socket`...
    //          but this loop should get you started
    for(;;)
    {
        // wait on children death (you'll need to do things with SIGCHLD too)
        // and create a new children as they die...
        wait(...);
        pid_t child_pid = fork();
        if(child_pid == 0)
        {
            handle_request(server_socket);
        }
    }
}

void handle_request(int server_socket)
{
    // here child blocks until a connection arrives on 'server_socket'
    int client_socket = accept(server_socket, ...);
    ...handle the request...
    exit(0);
}

int create_servers()
{
    init_server(80);   // create a connection on port 80
    init_server(443);  // create a connection on port 443
}

请注意,在这里显示的handle_request()函数处理一个请求。处理单个请求的优点是可以按照Unix的方式:根据需要分配资源,一旦回答了请求,就使用exit(0)退出。exit(0)将为您调用必要的close()free()等函数。
相比之下,如果要连续处理多个请求,则需要在返回accept()调用之前确保资源得到释放。此外,sbrk()函数几乎永远不会被调用来减少子进程的内存占用。这意味着它会不时地增长一点。这就是为什么像Apache2这样的服务器在开始新的子进程之前设置要响应一定数量的请求(默认情况下是100到1000个)。

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