在调用write()期间SO_KEEPALIVE不起作用?

时间:2011-10-14 14:11:50

标签: c sockets keep-alive

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

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

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

但是,如果我在网络断开连接后(以及在超时结束之前)尝试wrte(),则write()和read()将永久阻塞,没有错误。

这是一个剥离的示例代码,它将stdin / stdout定向到套接字。它侦听端口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。 如果在连接丢失后几秒钟后退出,除非我尝试在终端中写入内容。在这种情况下,它将永远阻止。

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

谢谢!


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

相反,如果我尝试写(),它会停止发送Keep-Alive数据包,并开始发送TCP重传(对我来说似乎没问题)。问题是,每次故障后重传之间的时间越来越大,似乎永远不会放弃并关闭套接字。

有没有办法设置最大重传次数或类似的? 感谢

6 个答案:

答案 0 :(得分:2)

我找到了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。 也许这是某个地方的错误?

答案 1 :(得分:1)

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

对于我们来说,解决方案是简单地掌握控制权,而不是依靠底层操作系统/驱动程序告诉您连接何时死亡。如果您同时控制客户端和服务器端,则可以引入自己的ping消息,这些消息会在客户端和服务器之间跳转。这样你就可以a)控制自己的连接超时,b)轻松保存一条记录,说明连接的健康状况。

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

答案 2 :(得分:1)

TCP Keep Alive在RFC1122中指定。 TCP的Keep Alive功能不是检测短期网络中断,而是清除可能耗尽宝贵资源的TCP控制块/缓冲区。该RFC也是在1989年编写的.RFC明确规定TCP Keep Alives不会每两小时发送一次以上,然后,只有在没有其他流量时才需要。如果更高级别的协议需要检测连接丢失,则更高级别协议的工作就是自己完成。 BGP路由协议在TCP上运行,默认情况下每60秒发送一次自己的Keep Alive消息。如果在最后3 * keep_alive_interval秒内没有看到新的流量,BGP规范表示连接被认为是死的。 OpenSSH以ping和pong的形式实现了它自己的活着。它会重新发送到X ping,它希望在Y时间内响应(pong)或者它会终止连接。 TCP本身在临时网络中断时非常难以提供数据,并且本身无法检测网络中断。

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

答案 3 :(得分:1)

在断开电缆连接之前,您是否从另一侧收到过一个字节或一个ACK?也许这与http://lkml.indiana.edu/hypermail/linux/kernel/0508.2/0757.html

中描述的行为有关

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

保持活动时间检查绝对要求tp-&gt; rcv_tstamp具有有效值,并且在处理ESTABLISHED状态的ACK之前它不会。

如果您通过连接成功发送或成功接收至少一个字节,从而处理至少一个处于ESTABLISHED状态的ACK,我认为您会发现保持活动正常。


这是一个模糊的SO_KEEPALIVE行为。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

这是因为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)设置较低的值,但这并不像我已经说过的那样精确。 此外,目前您无法仅为单个套接字设置这些值。那些是全局内核参数。 有一些尝试为单个套接字(http://patchwork.ozlabs.org/patch/55236/)应用tcp重新传输套接字选项,但我不认为它已应用于内核主线。我在系统头文件中找不到这些选项定义。

作为参考,您可以通过'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超时时,您可以根据您使用的平台遇到不同的返回事件,因此您不能仅通过返回事件来确定死连接状态。 例如,当发生keepalive超时时,HP返回POLLERR事件,AIX仅返回POLLIN事件。 那时你将在recv()调用中遇到ETIMEDOUT错误。

在最近的内核版本(自2.6.37开始)中,您可以使用TCP_USER_TIMEOUT选项将运行良好。此选项可用于单个插槽。