是否可以在一个应用程序中绑定和监听 Linux 中的多个端口?
3 回答
对于您要侦听的每个端口,您:
- 使用 .创建一个单独的套接字
socket
。 - 使用 将其绑定到适当的端口
bind
。 - 调用
listen
套接字,以便为它设置一个监听队列。
此时,您的程序正在侦听多个套接字。为了接受这些套接字上的连接,您需要知道客户端正在连接到哪个套接字。这就是select
进来的地方。碰巧,我有代码可以做这个,所以这里有一个完整的测试示例,等待多个套接字上的连接并返回连接的文件描述符。远程地址在附加参数中返回(缓冲区必须由调用者提供,就像接受一样)。
(socket_type
这里是int
Linux 系统上的 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 *
参数,该参数将获取看到传入连接的文件描述符。
您只bind()
需要一个套接字,然后listen()
-accept()
绑定的套接字用于服务器,来自的 fdaccept()
用于客户端。您在后者上进行选择,以查找任何在输入上有待处理数据的客户端套接字。
在这种情况下,您可能会对libevent感兴趣。它将select()
为您完成工作,可能使用更好的界面,例如epoll()
.
最大的缺点select()
是使用FD_...
宏将套接字数限制为变量中的最大位数fd_set
(从大约 100 到 256)。如果你有一个有 2 或 3 个连接的小型服务器,你会没事的。如果您打算在更大的服务器上工作,那么fd_set
很容易溢出。
此外,使用select()
orpoll()
允许您避免服务器中的线程(即您可以使用poll()
您的套接字并知道您是否可以accept()
、read()
或write()
它们。)
但是如果你真的想像 Unix 那样做,那么你需要fork()
在调用之前考虑 -ing accept()
。在这种情况下,您并不绝对需要select()
or 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 之间。)