【问题标题】:Why doesn't client's close() of socket cause server's select() to return为什么客户端的套接字close()不导致服务器的select()返回
【发布时间】:2013-02-23 02:58:58
【问题描述】:

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

什么会导致服务器对 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 的算法会不会导致 FIN 数据包在 close() 时无法发送?

【问题讨论】:

  • 可能有防火墙、nat 网关或其他设备跟踪和超时连接,这将导致您的服务器看不到 close()。如果您可以排除这种情况,并且这种情况始终如一地发生而不是由网络问题引起,那么您的代码在某处存在错误。
  • 你是close()-ing 服务器端父进程中的accept()-ed 套接字吗?
  • @NikolaiNFetissov 是的
  • @nos 我认为 NAT、防火墙或路由器可能会持续删除 FIN 数据包,但不太可能。我已经仔细而详尽地检查了代码(不幸的是,它太大而无法发布)。我认为更有可能是我不理解某些东西。
  • 只是对您最后一个问题的评论,nagel 不会导致不发送 fin(或 rst)。另外(void)close(socket); 是一个相当大的错误,希望这只是一个错字。

标签: c sockets select tcp


【解决方案1】:

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

如果您的客户端程序曾经执行过fork(或派生的相关调用,例如systempopen),则派生的孩子很可能拥有文件描述符的副本,这将导致您的行为'再看。

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

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

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

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

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

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

【讨论】:

  • 您的回答很切题而且很好。不幸的是,客户端套接字肯定是关闭的。我已将相关客户端调用的序列添加到我的问题中。
  • close 之前将shutdown 调用插入客户端会影响问题吗?我从未见过正确关闭的套接字未与服务器通信的情况。
  • 很遗憾,客户端暂时超出了我的控制范围,因此我无法测试 shutdown() 假设。
【解决方案2】:

谜团解开了。

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

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

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

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

【讨论】:

    【解决方案3】:

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

    在 Linux 上,select() 修改超时以反映未完成的时间量 睡着了

    所以你的timeo 将归零。而当它为零时,select 将立即返回(大部分返回值为零)。

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

    for (;;) {
        struct timeval timo = timeo;
        fd_set      readFds = fds;
        int         status = select(sock+1, &readFds, 0, 0, &timo);
    

    【讨论】:

    • 已经完成。 “代码未达到”部分适当地调整超时值。作为一个古怪的老程序员,我喜欢简洁。
    猜你喜欢
    • 1970-01-01
    • 2020-03-12
    • 1970-01-01
    • 2010-10-09
    • 2011-11-15
    • 1970-01-01
    • 2019-12-24
    • 1970-01-01
    • 2012-06-05
    相关资源
    最近更新 更多