从一台服务器收听多个端口

时间:2013-03-21 23:56:17

标签: c linux sockets

是否可以在一个应用程序中绑定和侦听Linux中的多个端口?

3 个答案:

答案 0 :(得分:14)

对于您想要收听的每个端口,您:

  1. 使用socket创建单独的套接字。
  2. 使用bind将其绑定到相应的端口。
  3. 在套接字上调用listen,使其设置为侦听队列。
  4. 此时,您的程序正在侦听多个套接字。为了接受这些套接字上的连接,您需要知道客户端连接到哪个套接字。这就是select的用武之地。事实上,我的代码就是这样,所以这里是一个完整的测试例子,它等待多个套接字上的连接并返回连接的文件描述符。远程地址以其他参数返回(缓冲区必须由调用者提供,就像接受一样)。

    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 *参数,该参数将获取看到传入连接的文件描述符。

答案 1 :(得分:2)

您只有bind()到一个套接字,然后是listen()accept() - 绑定的套接字是针对服务器的,accept()的fd是针对服务器的客户端。您对后者进行选择,查找输入中有未决数据的任何客户端套接字。

答案 2 :(得分:0)

在这种情况下,您可能会对libevent感兴趣。它将为您完成select()的工作,可能使用更好的界面,例如epoll()

select()的一个巨大缺点是使用FD_...宏将套接字数限制为fd_set变量中的最大位数(从大约100到256)。如果你有一个2或3个连接的小型服务器,你会没事的。如果您打算在更大的服务器上工作,那么fd_set很容易被溢出。

此外,使用select()poll()可以避免服务器中的线程(即你可以poll()你的套接字并知道你是否可以accept()read(),或write()给他们。)

但如果你真的想要像Unix那样做,那么你想在调用fork()之前考虑accept()。在这种情况下,您并不一定需要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到1,000之间。)