在Linux中,是否可能在一个应用程序中绑定和监听多个端口?
对于每个要监听的端口,您需要执行以下操作:
socket
创建一个单独的套接字。bind
将其绑定到相应的端口。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()
的工作,可能使用更好的接口,如 epoll()
。
select()
的巨大缺点是使用 FD_...
宏,将套接字号限制在 fd_set
变量中最大位数(从约100到256)。如果您拥有一个只有2或3个连接的小型服务器,那么没问题。如果您打算在更大的服务器上工作,那么 fd_set
可能很容易被溢出。select()
或 poll()
可以避免在服务器中使用线程(即,您可以 poll()
您的套接字,并知道是否可以 accept()
、read()
或 write()
)。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个)。
select
或线程。 - perreal