【问题标题】:SO_KEEPALIVE does not work during a call to write()?SO_KEEPALIVE 在调用 write() 期间不起作用?
【发布时间】:2011-10-14 14:11:49
【问题描述】:

我正在开发一个套接字应用程序,它必须对网络故障具有鲁棒性。

应用程序有 2 个正在运行的线程,一个等待来自套接字的消息(一个 read() 循环),另一个将消息发送到套接字(一个 write() 循环)。

我目前正在尝试使用 SO_KEEPALIVE 来处理网络故障。 如果我只在 read() 上被阻止,它可以正常工作。连接丢失几秒钟后(移除网线),read() 将失败并显示“连接超时”消息。

但是,如果我在网络断开后(超时结束之前)尝试 wrte(),write() 和 read() 都将永远阻塞,不会出错。

这是一个将标准输入/标准输出定向到套接字的示例代码。它在端口 5656 上侦听:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

int socket_fd;

void error(const char *msg) {
    perror(msg);
    exit(1);
}

//Read from stdin and write to socket
void* write_daemon (void* _arg) {
    while (1) {
        char c;
        int ret = scanf("%c", &c);
        if (ret <= 0) error("read from stdin");
        int ret2 = write(socket_fd, &c, sizeof(c));
        if (ret2 <= 0) error("write to socket");
    }
    return NULL;
}

//Read from socket and write to stdout
void* read_daemon (void* _arg) {
    while (1) {
        char c;
        int ret = read(socket_fd, &c, sizeof(c));
        if (ret <= 0) error("read from socket");
        int ret2 = printf("%c", c);
        if (ret2 <= 0) error("write to stdout");
    }
    return NULL;
}


//Enable and configure KEEPALIVE - To detect network problems quickly
void config_socket() {
    int enable_no_delay   = 1;
    int enable_keep_alive = 1;
    int keepalive_idle     =1; //Very short interval. Just for testing
    int keepalive_count    =1;
    int keepalive_interval =1;
    int result;

    //=> http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/#setsockopt
    result = setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, &enable_keep_alive, sizeof(int));
    if (result < 0)
        error("SO_KEEPALIVE");

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPIDLE, &keepalive_idle, sizeof(int));
    if (result < 0) 
        error("TCP_KEEPIDLE");

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPINTVL, &keepalive_interval, sizeof(int));
    if (result < 0) 
        error("TCP_KEEPINTVL");

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPCNT, &keepalive_count, sizeof(int));
    if (result < 0) 
        error("TCP_KEEPCNT");
}

int main(int argc, char *argv[]) {
    //Create Server socket, bound to port 5656
    int listen_socket_fd;
    int tr=1;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t clilen = sizeof(cli_addr);
    pthread_t write_thread, read_thread;

    listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_socket_fd < 0)
        error("socket()");

    if (setsockopt(listen_socket_fd,SOL_SOCKET,SO_REUSEADDR,&tr,sizeof(int)) < 0)
        error("SO_REUSEADDR");

    bzero((char *) &serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(5656);
    if (bind(listen_socket_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
        error("bind()");

    //Wait for client socket
    listen(listen_socket_fd,5);
    socket_fd = accept(listen_socket_fd, (struct sockaddr *) &cli_addr, &clilen);
    config_socket();
    pthread_create(&write_thread, NULL, write_daemon, NULL);
    pthread_create(&read_thread , NULL, read_daemon , NULL);
    close(listen_socket_fd);
    pthread_exit(NULL);
}

要重现错误,请使用 telnet 5656。 If 将在连接丢失几秒后退出,除非我尝试在终端中写一些东西。在这种情况下,它将永远阻塞。

所以,问题是:怎么了?如何解决?还有其他选择吗?

谢谢!


我尝试使用 Wireshark 检查网络连接。如果我不调用 write(),我可以看到 TCP keep-alive 包正在发送,并且连接在几秒钟后关闭。

相反,如果我尝试 write(),它会停止发送 Keep-Alive 数据包,而是开始发送 TCP 重传(我觉得没问题)。问题是,每次失败后重传之间的时间越来越长,而且它似乎永远不会放弃和关闭套接字。

有没有办法设置重传的最大次数,或者类似的东西? 谢谢

【问题讨论】:

    标签: c sockets keep-alive


    【解决方案1】:

    我找到了 TCP_USER_TIMEOUT 套接字选项 (rfc5482),如果发送的数据在指定的时间间隔后没有被确认,它将关闭连接。

    对我来说很好用 =)

    //defined in include/uapi/linux/tcp.h (since Linux 2.6.37)
    #define TCP_USER_TIMEOUT 18
    
    int tcp_timeout        =10000; //10 seconds before aborting a write()
    
    result = setsockopt(socket_fd, SOL_TCP, TCP_USER_TIMEOUT, &tcp_timeout, sizeof(int));
    if (result < 0) 
        error("TCP_USER_TIMEOUT");
    

    然而,我觉得我不应该同时使用 SO_KEEP_ALIVE 和 TCP_USER_TIMEOUT。 也许是某个地方的错误?

    【讨论】:

    • 这不是一个错误,TCP_USER_TIMEOUT 已被实施以解决您上面描述的相同问题,即如果发送保活数据包的一方,如果在该套接字上发送,那么重新传输计时器启动并在 15-20 分钟后学会断开连接!使用TCP_USER_TIMEOUT,硬性限制了数据包可以保持未确认的时间。
    【解决方案2】:

    不确定其他人是否会给你一个更好的选择,但在我参与的几个项目中,我们遇到了非常相似的情况。

    对我们来说,解决方案是让您自己掌握控制权,而不是依赖底层操作系统/驱动程序来告诉您连接何时中断。如果您同时控制客户端和服务器端,则可以引入自己的 ping 消息,这些消息在客户端和服务器之间反弹。通过这种方式,您可以 a) 控制自己的连接超时,并且 b) 轻松保存指示连接健康状况的记录。

    在最新的应用程序中,我们将这些 ping 作为带内控制消息隐藏在通信库本身中,因此就实际的客户端/服务器应用程序代码而言,连接超时正常工作。

    【讨论】:

    • 我喜欢它,但我只实现了现有协议的一侧,没有任何方法强制“ping”。
    【解决方案3】:

    TCP Keep Alive 在RFC1122 中指定。 TCP 的 Keep Alive 功能不是检测短期网络中断,而是清理可能占用宝贵资源的 TCP 控制块/缓冲区。该 RFC 也是在 1989 年编写的。RFC 明确指出 TCP Keep Alives 不能每两小时发送一次以上,然后,只有在没有其他流量时才需要。如果更高级别的协议需要检测连接丢失,则由更高级别的协议自行完成。在 TCP 之上运行的 BGP 路由协议默认每 60 秒发送一次它自己形式的 Keep Alive 消息。 BGP 规范说,如果在最后 3*keep_alive_interval 秒内没有看到新的流量,则认为连接已失效。 OpenSSH 以 ping 和 pong 的形式实现了它自己的 keep alive。它将重试发送最多 X 个 ping,它期望在 Y 时间内得到响应(pong),否则它会终止连接。 TCP 本身在遇到临时网络中断时会非常努力地传递数据,并且它本身并不能用于检测网络中断。

    通常,如果您想实现保持活动状态并希望避免阻塞,则可以切换到非阻塞 I/O 并维护一个计时器,该计时器可用于带有超时的 select()/poll() 调用.另一种选择可能是使用单独的计时器线程,甚至是使用 SIGALARM 的更粗略的方法。我建议使用带有 fcntl() 的 O_NONBLOCK 将套接字设置为非阻塞 I/O。然后,您可以使用 gettimeofday() 记录接收到传入 I/O 的时间并使用 select() 休眠,直到下一个 Keep Alive 到期或 I/O 发生。

    【讨论】:

    • 不像你说的那样。 RFC 1122 规定第一个 keepalive 探测的 DEFAULT 间隔应不少于两个小时。这意味着用户可以将其设置为任何想要的。
    【解决方案4】:

    在断开电缆之前,您是否成功收到了来自另一端的字节或 ACK?也许这与http://lkml.indiana.edu/hypermail/linux/kernel/0508.2/0757.html 中描述的行为有关:


    您的测试用例是有问题的,因为您在已建立的状态下甚至没有收到一个 ACK​​,因此 tp->rcv_tstamp 变量无法初始化。您收到的唯一 ACK 是对连接设置 SYN 的响应,我们不会为该 ACK 初始化 tp->rcv_stamp。

    keepalive 时间检查绝对要求 tp->rcv_tstamp 具有有效值,并且在您处理 ESTABLISHED 状态的 ACK 之前它不会。

    如果您通过连接成功发送或接收至少一个字节,并因此处理至少一个处于 ESTABLISHED 状态的 ACK,我想您会发现 keepalive 行为正常。


    这是一个不起眼的 SO_KEEPALIVE 行为。

    【讨论】:

      【解决方案5】:

      write_daemon() 中,您将write() 的返回值存储到ret2 变量中,然后使用ret 变量检查套接字错误,因此您永远不会真正捕获任何write()错误。

      【讨论】:

      • 谢谢!我已经修好了。不幸的是,它没有解决问题。
      【解决方案6】:

      那是因为 tcp 堆栈在你没有意识的情况下进行了 tcp 重传。 以下是解决方案。

      即使您已经为您的应用程序套接字设置了keepalive选项,您也无法及时检测到套接字的死连接状态,以防您的应用程序一直在套接字上写入。 那是因为内核 tcp 堆栈的 tcp 重传。 tcp_retries1 和 tcp_retries2 是用于配置 tcp 重传超时的内核参数。 重传超时的准确时间很难预测,因为它是通过 RTT 机制计算的。 你可以在 rfc793 中看到这个计算。 (3.7. 数据通信)

      https://www.rfc-editor.org/rfc/rfc793.txt

      每个平台都有用于 tcp 重传的内核配置。

      Linux : tcp_retries1, tcp_retries2 : (exist in /proc/sys/net/ipv4)
      

      http://linux.die.net/man/7/tcp

      HPUX : tcp_ip_notify_interval, tcp_ip_abort_interval
      

      http://www.hpuxtips.es/?q=node/53

      AIX : rto_low, rto_high, rto_length, rto_limit
      

      http://www-903.ibm.com/kr/event/download/200804_324_swma/socket.pdf

      如果您想及早检测到死连接,您应该为 tcp_retries2 设置较低的值(默认为 15),但这不是我已经说过的精确时间。 此外,目前您不能只为单个套接字设置这些值。这些是全局内核参数。 有一些尝试为单套接字应用 tcp 重传套接字选项(http://patchwork.ozlabs.org/patch/55236/),但我认为它没有应用于内核主线。我在系统头文件中找不到这些选项定义。

      作为参考,您可以通过如下所示的“netstat --timers”监控您的 keepalive 套接字选项。 https://stackoverflow.com/questions/34914278

      netstat -c --timer | grep "192.0.0.1:43245             192.0.68.1:49742"
      
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (1.92/0/0)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (0.71/0/0)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (9.46/0/1)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (8.30/0/1)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (7.14/0/1)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (5.98/0/1)
      tcp        0      0 192.0.0.1:43245             192.0.68.1:49742            ESTABLISHED keepalive (4.82/0/1)
      

      另外,当keepalive超时发生时,你可以根据你使用的平台,遇到不同的返回事件,所以你不能仅仅通过返回事件来决定死连接状态。 例如,HP 返回 POLLERR 事件,而 AIX 在发生 keepalive 超时时仅返回 POLLIN 事件。 届时您将在 recv() 调用中遇到 ETIMEDOUT 错误。

      在最近的内核版本(自 2.6.37 起)中,您可以使用 TCP_USER_TIMEOUT 选项会很好地工作。此选项可用于单套接字。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-01-13
        • 2023-03-13
        • 1970-01-01
        • 2020-10-17
        • 2012-10-29
        • 2016-09-14
        • 1970-01-01
        相关资源
        最近更新 更多