6

[我之前问过类似的问题。这是一个更集中的版本。]

什么会导致服务器对 TCP 套接字的 select() 调用始终超时,而不是“看到”客户端的套接字 close()?在客户端,套接字是一个常规的 socket() 创建的阻塞套接字,它成功连接到服务器并成功传输往返事务。在服务器端,套接字通过accept() 调用创建,处于阻塞状态,通过fork() 传递给子服务器进程,被顶级服务器关闭,并被子服务器进程成功使用初始交易。当客户端随后关闭套接字时,子服务器进程的 select() 调用始终超时(1 分钟后),而不是指示套接字上的读取就绪状态。select() 调用仅查找准备就绪的条件:

这是子服务器进程中使用 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 的算法会导致 FIN 数据包在 close() 时不发送吗?

4

3 回答 3

4

最可能的解释是,当您认为自己关闭时,您实际上并没有关闭连接的客户端。可能是因为您有一些其他文件描述符引用了未关闭的客户端套接字。

如果您的客户端程序曾经执行过fork(或相关的分叉调用,例如systemor popen),则分叉的子程序很可能拥有文件描述符的副本,这会导致您看到的行为。

测试/解决问题的一种方法是让客户端在关闭套接字之前执行显式关闭(2):

shutdown(sock, SHUT_RDWR);
close(sock);

如果这导致问题消失,那么这就是问题所在——您在某处有另一个客户端套接字文件描述符副本。

如果问题是由于孩子获得了套接字,最好的解决方法可能是在创建套接字后立即在套接字上设置 close-on-exec 标志:

fcntl(sock, F_SETFD, fcntl(sock, F_GETFD) | FD_CLOEXEC);

或者在某些系统上,将SOCK_CLOEXEC标志用于套接字创建调用。

于 2013-03-08T18:09:33.130 回答
3

谜团已揭开。

@nos 在第一条评论中是正确的:这是防火墙问题。不需要客户端的shutdown();客户端确实关闭了套接字;服务器确实使用了正确的超时时间;并且代码中没有错误。

该问题是由我们的 Linux 虚拟服务器 (LVS) 上的防火墙规则引起的。一个客户端连接到 LVS,连接被传递到几个后端服务器中负载最小的一个。来自客户端的所有数据包都通过 LVS;来自后端服务器的所有数据包都直接发送到客户端。LVS 上的防火墙规则导致来自客户端的 FIN 数据包被丢弃。因此,后端服务器从未看到客户端的 close()。

解决方案是从 LVS 系统的 iptables(8) 规则中删除“-m state --state NEW”选项。这允许将来自客户端的 FIN 数据包转发到后端服务器。这篇文章有更多信息。

感谢所有建议使用wireshark(1) 的人。

于 2013-03-12T22:31:43.863 回答
1

select()Linux 的调用将修改timeout参数的值。从手册页:

在 Linux 上,select() 修改超时以反映未睡眠的时间量

所以你的timeo意志会归零。当它为零select时将立即返回(大多数返回值为零)。

以下更改可能会有所帮助:

for (;;) {
    struct timeval timo = timeo;
    fd_set      readFds = fds;
    int         status = select(sock+1, &readFds, 0, 0, &timo);
于 2013-03-07T23:19:41.403 回答