在网络通信中,一种很常见架构:C/S架构。

如果在server端开一条线程专门处理socket连接,这就涉及到一个问题,如果socket连接断开(不论是正常断开还是异常掉线),怎么才能知道客服端的连接情况呢,server端这边是绝对被动的,sever端不能主动断开连接。也没有连接链路维持包之类的,而且client端发送数据的时间也是不定的。而在socket连接断开后, server要能够知道连接已经断开并释放资源。

下面讲一个有个小方法:
当使用 select()函数测试一个socket是否可读时,如果select()函数返回值为1,且使用recv()函数读取的数据长度为0 时,就说明该socket已经断开。
为了更好的判定socket是否断开,可以判断当recv()返回值小于等于0时,则socket连接断开。但是还需要判断 errno是否等于 EINTR 。如果errno == EINTR 则说明recv函数是由于程序接收到信号后返回的,socket连接还是正常的,不应close掉socket连接。

注意:对于堵塞socket的recv()函数会在以下三种情况下返回:
(1)recv接到数据时,会返回。
(2)在整个程序接收到信号时,返回-1。errno = EINTR。(在程序的起始阶段,屏蔽掉信号的除外。部分信号还是屏蔽不掉的)
(3)socket出现问题时,返回-1.具体错误码看 man recv()
(4)一定要看 man 说明,很详细,很有帮助

提问:

(1)TCP会自动断开连接吗?

(2)已经建立了TCP连接,并可能互通信息。但是如果长时间不进行信息的传递。这个TCP连接会自动断开吗?

(3)如果能自动断开的话,这个时间大约是多少呢?

答:TCP有一个保活定时器,这个保活定时器能够保证TCP连接一直保持,但是TCP的保活定时器不是每个TCP/IP协议栈都实现了,因为RFC并不要求TCP保活定时器一定要实现。

理由:保活并不是T C P规范中的一部分。Host Requirements RFC提供了3个不使用保活定时器的理由:

(1) 在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉;

(2)它们耗费不必要的带宽;

(3)在按分组计费的情况下会在互联网上花掉更多的钱。然而,许多实现提供了保活定时器。

保活定时器的介绍:

在一个空闲的(idle)TCP连接上,没有任何的数据流。也就是说,如果TCP连接两端没有任何一个进程在向对方发送数据,那么在这两个TCP模块之间没有任何的数据交换。你可能在其它的网络协议中发现有轮询(polling),但在TCP中它不存在。言外之意就是我们只要启动一个客户端进程,同服务器建立了TCP连接,不管你离开几小时,几天,几星期或是几个月,连接依旧存在。中间的路由器可能崩溃或者重启,电话线可能go down或者back up,只要连接两端的主机没有重启,连接依旧保持建立。

这就可以认为不管是客户端的还是服务器端的应用程序都没有应用程序级(application-level)的定时器来探测连接的不活动状态(inactivity),从而引起任何一个应用程序的终止。像BGP每隔30秒就向对方发送一个应用程序探测。这是一个应用程序定时器(application timer),与TCP存活定时器不同。

然而有的时候,服务器需要知道客户端主机是否已崩溃并且关闭,或者崩溃但重启。许多实现提供了存活定时器来完成这个任务。

存活(keepalive)

存活并不是TCP规范的一部分。在Host Requirements RFC罗列有不使用它的三个理由:

(1)在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)

(2)它们消费了不必要的宽带

(3)在以数据包计费的互联网上它们(额外)花费金钱。

然而,在许多的实现中提供了存活定时器。

存活定时器是一个包含争议的东西。许多人认为,即使需要它,这种对c和s双方的轮询也应该由应用程序来完成,而不是由TCP中实现。

如果两个终端系统之间的某个中间网络上有连接的暂时中断,那么存活选项(option)就能够引起两个进程间一个良好连接的终止。例如,如果正好在某个中间路由器崩溃、重启的时候发送存活探测,TCP就将会认为客户端主机已经崩溃,但事实并非如此。

一些服务器应用程序可能代表客户端占用资源,它们需要知道客户端主机是否崩溃。存活定时器可以为这些应用程序提供探测服务。Telnet服务器和Rlogin服务器的许多版本都默认提供存活选项。

在个人计算机用户使用TCP/IP协议通过Telnet登录一台主机,这是能够说明需要使用存活定时器的一个常用例子。如果某个用户在使用结束时只是关掉了电源,而没有注销连接(log off),那么他就留下了一个半打开(half-open)的连接。在一个半打开的连接上,通过发送数据,可以得到一个复位(reset)返回,但那是在客户端,是由客户端发送的数据。如果客户端消失,留给了服务器端半打开的连接,并且服务器又在等待客户端的数据,那么等待将永远持续下去。存活特征的目的就是在服务器端检测这种半打开连接。

目前主要有三种方法来实现用户掉线检测:SO_KEEPALIVE ,SIO_KEEPALIVE_VALS 和Heart-Beat线程。

下面我就上面的三种方法来做一下介绍。

(1)SO_KEEPALIVE 机制

这是socket库提供的功能,设置接口是setsockopt API:

BOOL bSet=TRUE;

setsockopt(hSocket,SOL_SOCKET,SO_KEEPALIVE,(const char*)&bSet,sizeof(BOOL));

        根据MSDN的文档,如果为socket设置了KEEPALIVE选项,TCP/IP栈在检测到对方掉线后,任何在该socket上进行的调用(发送/接受调用)就会立刻返回,错误号是WSAENETRESET,同时,此后的任何在该socket句柄的调用会立刻失败,并返回WSAENOTCONN错误。

该机制的缺点也很明显:

        默认设置是空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测!当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口!关联了完成端口的socket可能会忽略掉该套接字选项。

(2)SIO_KEEPALIVE_VALS 机制

设置接口是WSAIoctl API:    

DWORD dwError = 0L ;

tcp_keepalive sKA_Settings = {0}, sReturned = {0} ;

sKA_Settings.onoff = 1 ;

sKA_Settings.keepalivetime = 5500 ; // Keep Alive in 5.5 sec.

sKA_Settings.keepaliveinterval = 3000 ; // Resend if No-Reply

if (WSAIoctl(skNewConnection, SIO_KEEPALIVE_VALS, &sKA_Settings,sizeof(sKA_Settings),                 
     &sReturned, sizeof(sReturned), &dwBytes,NULL, NULL) != 0)
{

     dwError = WSAGetLastError() ;

}

实现时需要添加tcp_keepalive and SIO_KEEPALIVE_VALS的定义文件MSTCPiP.h

该选项不同于SO_KEEPALIVE 机制的就是它是针对单个连接的,对系统其他的套接口并不影响。 针对完成端口的socket,设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次进行RECV或者SEND操作时,马上就会返回错误告诉上层这个连接已经断开了.如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。

该机制的缺点:不通用吧。这个API只能用于Windows

(3)Heart-Beat线程

就是常说的心跳检测。发包给它,收到回复就说明没掉线。

写一个后台线程,实现Heart-Beat包,当客户端收到该包后,立刻返回相应的反馈包。这个方法的好处是通用,但缺点就是会改变现有的通讯协议!

/* Net check Make sure you have not used OUT OF BAND DATA AND YOU CAN use OOB */
int netcheck(int fd) 
{
        int buf_size = 1024;
        char buf[buf_size];
        //clear OOB DATA 
        recv(fd, buf, buf_size);
        if(send(fd, (void *)"\0", 1, MSG_OOB) < 0 )
        {
                fprintf(stderr, "Connection[%d] send OOB failed, %s", fd, strerror(errno));
                return -1;
        }
        return 0;
}

/* Setting SO_TCP KEEPALIVE */
//int keep_alive = 1;//设定KeepAlive
//int keep_idle = 1;//开始首次KeepAlive探测前的TCP空闭时间
//int keep_interval = 1;//两次KeepAlive探测间的时间间隔
//int keep_count = 3;//判定断开前的KeepAlive探测次数
void set_keepalive(int fd, int keep_alive, int keep_idle, int keep_interval, int keep_count)
{
        int opt = 1;
        if(keep_alive)
        {
                if(setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,
                                        (void*)&keep_alive, sizeof(keep_alive)) == -1)
                {
                        fprintf(stderr, 
                                "setsockopt SOL_SOCKET::SO_KEEPALIVE failed, %s\n",strerror(errno));
                }
                if(setsockopt(fd, SOL_TCP, TCP_KEEPIDLE,
                                        (void *)&keep_idle,sizeof(keep_idle)) == -1)
                {
                        fprintf(stderr,
                                "setsockopt SOL_TCP::TCP_KEEPIDLE failed, %s\n", strerror(errno));
                }
                if(setsockopt(fd,SOL_TCP,TCP_KEEPINTVL,
                                        (void *)&keep_interval, sizeof(keep_interval)) == -1)
                {
                        fprintf(stderr,
                                 "setsockopt SOL_tcp::TCP_KEEPINTVL failed, %s\n", strerror(errno));
                }
                if(setsockopt(fd,SOL_TCP,TCP_KEEPCNT,
                                        (void *)&keep_count,sizeof(keep_count)) == -1)
                {
                        fprintf(stderr, 
                                "setsockopt SOL_TCP::TCP_KEEPCNT failed, %s\n", strerror(errno));
                }
        }
}

一本不错的书《Effective TCP/IP Programming》。

总结:很多人都知道TCP并不会去主动检测连接的丢失,这意味着,如果双方不产生交互,那么如果网络断了或者有一方机器崩溃,另外一方将永远不知道连接已经不可用了。检测连接是否丢失的方法大致有两种:keepalive和heart-beat。

Keepalive是很多的TCP实现提供的一种机制,它允许连接在空闲的时候双方会发送一些特殊的数据段,并通过响应与否来判断连接是否还存活着(所谓keep~~alive)。其实keepalive在实际的应用中并不常见。这得归结于keepalive设计的初衷。Keepalive适用于清除死亡时间比较长的连接。 

比如这样的场景:一个用户创建tcp连接访问了一个web服务器,当用户完成他执行的操作后,很粗暴的直接拨了网线。这种情况下,这个tcp连接已经断开了,但是web服务器并不知道,它会依然守护着这个连接。如果web server设置了keepalive,那么它就能够在用户断开网线的大概几个小时以后,确认这个连接已经中断,然后丢弃此连接,回收资源。
采用keepalive,它会先要求此连接一定时间没有活动(一般是几个小时),然后发出数据段,经过多次尝试后(每次尝试之间也有时间间隔),如果仍没有响应,则判断连接中断。即整个周期需要很长的时间。

所以,如上面的场景那样,需要一种方法能够清除和回收那些在系统不知情的情况下死去了很久的连接,keepalive是非常好的选择。 

但是,在大部分情况下,特别是分布式环境中,我们需要的是一个能够快速或者实时监控连接状态的机制,这里,heart-beat才是更加合适的方案。 

Heart-beat(心跳),按我的理解,它的原理和keepalive非常类似,都是发送一个信号给对方,如果多次发送都没有响应的话,则判断连接中断。它们的不同点在于,keepalive是tcp实现中内建的机制,是在创建tcp连接时通过设置参数启动keepalive机制;而heart-beat则需要在tcp之上的应用层实现。一个简单的heart-beat实现一般测试连接是否中断采用的时间间隔都比较短,可以很快的决定连接是否中断。并且,由于是在应用层实现,因为可以自行决定当判断连接中断后应该采取的行为,而keepalive在判断连接失败后只会将连接丢弃。

关于heart-beat,一个非常有趣的问题是,应该在传输真正数据的连接中发送“心跳”信号,还是可以专门创建一个发送“心跳”信号的连接。比如说,A,B两台机器之间通过连接m来传输数据,现在为了能够检测A,B之间的连接状态,我们是应该在连接m中传输“心跳”信号,还是创建新的连接n来专门传输“心跳”呢?我个人认为两者皆可。如果担心的是端到端的连接状态,那么就直接在该条连接中实现“心跳”。但很多时候,关注的是网络状况和两台主机间的连接状态,这种情况下, 创建专门的“心跳”连接也未尝不可。