[我之前也问过类似的问题。这是一个更集中的版本。]
什么会导致服务器对 TCP 套接字的 select() 调用始终超时,而不是“看到”客户端对套接字的 close()?在客户端,套接字是一个常规的 socket() 创建的阻塞套接字,它成功连接到服务器并成功传输往返事务。在服务器端,套接字通过accept()调用创建,处于阻塞状态,通过fork()传递给子服务器进程,被顶级服务器关闭,并被子服务器进程成功使用初始交易。当客户端随后关闭套接字时,子服务器进程的 select() 调用始终会超时(1 分钟后),而不是指示套接字上的读就绪状态。 select() 调用仅查找读就绪条件:写就绪和异常参数为 NULL。
这是在子服务器进程中使用 select() 的简化但逻辑上等效的代码:
int one_svc_run(
const int sock,
const unsigned timeout)
{
struct timeval timeo;
fd_set fds;
timeo.tv_sec = timeout;
timeo.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(sock, &fds);
for (;;) {
fd_set readFds = fds;
int status = select(sock+1, &readFds, 0, 0, &timeo);
if (status < 0)
return errno;
if (status == 0)
return ETIMEDOUT;
/* This code not reached when client closes socket */
/* The time-out structure, "timeo", is appropriately reset here */
...
}
...
}
这是客户端事件序列的逻辑等效项(未显示错误处理):
struct sockaddr_in *raddr = ...;
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
(void)bindresvport(sock, (struct sockaddr_in *)0);
connect(sock, (struct sockaddr *)raddr, sizeof(*raddr));
/* Send a message to the server and receive a reply */
(void)close(sock);
fork()、exec() 和 system() 永远不会被调用。代码比这复杂得多,但这是相关调用的顺序。
Nagel 算法是否会导致 close() 时 FIN 数据包无法发送?
最可能的解释是,当您认为关闭连接的客户端时,您实际上并没有关闭连接的客户端。可能是因为您有一些其他文件描述符引用未关闭的某个地方的客户端套接字。
如果您的客户端程序曾经执行
fork
(或分叉的相关调用,例如 system
或 popen
),分叉的子程序很可能拥有文件描述符的副本,这将导致您所看到的行为.
测试/解决该问题的一种方法是让客户端在关闭套接字之前执行显式 shutdown(2):
shutdown(sock, SHUT_RDWR);
close(sock);
如果这导致问题消失,那么这就是问题所在——您在某个地方挂着另一个客户端套接字文件描述符的副本。
如果问题是由于子进程获取套接字造成的,最好的解决办法可能是在创建套接字后立即在套接字上设置 close-on-exec 标志:
fcntl(sock, F_SETFD, fcntl(sock, F_GETFD) | FD_CLOEXEC);
或者在某些系统上,使用
SOCK_CLOEXEC
标志进行套接字创建调用。
谜团解开了。
@nos 在第一条评论中是正确的:这是防火墙问题。不需要客户端 shutdown();客户端确实关闭了套接字;服务器确实使用了正确的超时;并且代码中没有错误。
问题是由我们的 Linux 虚拟服务器 (LVS) 上的防火墙规则引起的。客户端连接到 LVS,并将连接传递到多个后端服务器中负载最小的一个。所有来自客户端的数据包都会经过LVS;来自后端服务器的所有数据包都直接发送到客户端。 LVS 上的防火墙规则导致来自客户端的 FIN 数据包被丢弃。因此,后端服务器永远不会看到客户端的 close() 操作。
解决方案是从 LVS 系统上的 iptables(8) 规则中删除“-m state --state NEW”选项。这允许来自客户端的 FIN 数据包转发到后端服务器。 这篇文章有更多信息。
感谢所有建议使用wireshark(1)的人。
select()
调用将修改 timeout
参数的值。来自手册页:
在 Linux 上,select() 修改超时以反映未执行的时间量 睡了
所以你的
timeo
将变为零。当它为零时select
将立即返回(大多数返回值为零)。
以下更改可能会有所帮助:
for (;;) {
struct timeval timo = timeo;
fd_set readFds = fds;
int status = select(sock+1, &readFds, 0, 0, &timo);