为什么客户端的close()套接字不会导致服务器的select()返回

时间:2013-03-07 18:42:24

标签: c sockets select tcp

[之前我曾问过类似的事情。这是一个更有针对性的版本。]

什么可以导致服务器的TCP套接字上的select()调用始终超时而不是“看到”客户端的套接字close()?在客户端,套接字是一个常规的socket() - 创建阻塞套接字,成功连接到服务器并成功传输往返事务。在服务器端,套接字通过accept()调用创建,阻塞,通过fork()传递给子服务器进程,由顶级服务器关闭,并由子服务器进程成功使用最初的交易。当客户端随后关闭套接字时,子服务器的select()调用始终超时(1分钟后),而不是在套接字上指示读取就绪状态。 select()调用仅查找read-ready条件:write-ready和exception参数为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()?

时不被发送

3 个答案:

答案 0 :(得分:4)

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

如果您的客户端程序执行了fork(或者相关的调用,例如systempopen),则分叉的子代可能具有文件描述符的副本,会引起你所看到的行为。

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

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

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

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

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

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

答案 1 :(得分:3)

神秘解决了。

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

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

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

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

答案 2 :(得分: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);