C/C++网络编程中的TCP保活

来源:互联网 发布:淘宝怎么合并付款 编辑:程序博客网 时间:2024/04/30 19:42
以下是我从互联网上收集的资料,整理一了下,取精去粗,更通俗易懂。
本文重点介绍的是使用WIN32 TCP自带的保活设置,至于应用层自定义心跳包的保活方式这里就不做介绍了。
重点是让读者了解为什么要做TCP保活,这对与设计客户端与服务器交互不是很频繁的服务器程序是十分必要的(就是服务器不能确定一段时间内客户端肯定会发信息过来,或者什么时间客户端发数据过来)。

TCP保活的必要性:

TCP的长连接理论上只要连接建立后,就会一直保持着。但有时有一些防火墙之类的软件会自动检查主机的网络连接状况,比如说如果发现某个连接在几分钟之内都没有数据通讯,则会关闭这个连接。有时客户端与服务器需要实时的检测连接状态,就是需要知道对方是否还在线,如果对方不在线了,需要做相应的处理,这是就需要通过发送心跳包的方法监测链路的状态。


导致 TCP 连接断连的因素 :

理想状态下,一个 TCP 连接可以被长期保持。然而,在实际应用中,客户端或服务器端上维持的一个看似正常的 TCP 连接可能已经断连。TCP 连接主要受到两个方面的影响而导致断连:网络中间节点和客户端 / 服务器节点参与通信的两方节点。

在实际网络应用中,两个主机之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等。因此,两个主机之间 TCP 连接的保持同样会受到中间节点的影响,尤其是会受到防火墙(软件或硬件防火墙)的限制。防火墙是一种装置,有多种不同的实现方式(软件实现、硬件设备实现或是软硬件相结合实现),它需要依据一系列规则对进出的信息流进行扫描,并允许安全(符合规则)的信息交互、阻止不安全(违反规则)的信息交互。防火墙的工作特性决定了要维护一个网络连接就需要耗费较多的资源,并且企业防火墙常常位于企业网络的出入口,长时间维护非活跃的 TCP 连接必将导致网络性能的下降。因此,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 TCP 连接断连。类似的,如果中间节点异常导致来自客户端关闭连接的请求无法传递到服务器端,也将导致服务器端的相应连接发生断连。

另一方面,对于一个 TCP 连接两端的主机而言,创建 TCP 连接需要耗费一定的系统资源。如果不再使用某个连接,那么我们总是希望进行通信的两个主机能够主动关闭相应的连接,以便释放所占用的系统资源。然而,如果由于客户端出现异常 ( 例如崩溃或异常重启 ) 而导致连接未能正常关闭,这将导致服务器端的连接断连。

无论是客户端节点或是服务器端节点,断连的 TCP 连接已经不能传递任何信息,因此,维护大量断连的 TCP 连接将导致系统资源的浪费。这种系统资源的浪费可能并不会对客户端节点带来太大问题;然而,对于服务器主机而言,这可能会导致系统资源(尤指内存资源和 socket 资源)被耗尽而拒绝为新的用户请求提供服务。因此在实际应用中,服务器端需要采取相应的方法来探测 TCP 连接是否已经断连。

探测 TCP 连接断连的三种常用方法

探测 TCP 连接是否断连或是工作正常的原理比较简单:定期向连接的远程通信节点发送一定格式的信息并等待远程通信节点的反馈,如果在规定时间内收到来自远程节点的正确的反馈信息,那么该连接就是正常的,否则该连接已经断连。依据该原理,目前常用的探测方法有以下三种。

应用程序的自我探测

应用程序本身附带探测其自身建立的 TCP 连接的功能。这种方法具有极大的灵活性,可以依据应用本身的特点选择相应的探测机制和功能实现。然而,实际应用中,大部分应用程序均没有附带自我探测的功能。

第三方应用程序的探测

此种方法就是在服务节点上安装相应的第三方应用程序来探测该节点上所有的 TCP 连接是否正常或是已经断连。该方法最大的不足就是需要所有支持探测的客户端能够识别来自该探测应用的数据报文,因此,实际应用中比较少见。

TCP 协议层的保活探测

最常用的探测方法就是采用 TCP 协议层提供的保活探测功能即 TCP 连接保活定时器。尽管该功能并不是 RFC 规范的一部分,但是几乎所有的类 Unix 系统均实现了该功能,所以使得该探测方法被广泛使用。

长连接与短连接

TCP短连接

我们模拟一下TCP短连接的情况,client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。为什么呢,一般的server不会回复完client后立即关闭连接的,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在client/server间传递一次读写操作

短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段

TCP长连接

接下来我们再模拟一下长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

首先说一下TCP/IP详解上讲到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务器端检测到这种半开放的连接。

使用WIN32 TCP自带的保活


在默认的情况下,TCP连接是没有保活的心跳的。这就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线,另一端永远也不可能知道。这对于一些服务型的程序来说,将是灾难性的后果。

所以,必须对创建的socket,启用保活心跳,即Keepalive选项。


WIN32环境下的TCP Keepalive参数设置
  而WIN32环境下的参数设置,就要麻烦一些,需要使用另外的一个函数WSAIoctl和一个结构struct tcp_keepalive。

  它们的原型分别为:


[html] view plaincopy #include <winsock2.h>   
#include <mstcpip.h>   
   
int WSAIoctl(   
             SOCKET s,   
             DWORD dwIoControlCode,   
             LPVOID lpvInBuffer,   
             DWORD cbInBuffer,   
             LPVOID lpvOutBuffer,   
             DWORD cbOutBuffer,   
             LPDWORD lpcbBytesReturned,   
             LPWSAOVERLAPPED lpOverlapped,   
             LPWSAOVERLAPPED_COMPLETION lpCompletionRoutine   
);   
   
struct tcp_keepalive {   
    u_long onoff;   
    u_long keepalivetime;   
    u_long keepaliveinterval;   
};   
  在这里,使用WSAIoctl的时候,dwIoControlCode要使用SIO_KEEPALIVE_VALS,lpvOutBuffer用不上,cbOutBuffer必须设置为0。

  struct tcp_keepalive结构的参数意义为:

  onoff,是否开启KEEPALIVE; keepalivetime,多长时间触发Keepalive报文的发送; keepaliveinterval,多长时间没有回应触发下一次发送。

  注意:这里两个时间单位都是毫秒而不是秒。


[html] view plaincopy #include <winsock2.h>   
#include <mstcpip.h>   
   
int   
socket_set_keepalive (int fd)   
{   
  struct tcp_keepalive kavars[1] = {   
      1,   
      10 * 1000,        /* 10 seconds */   
      5 * 1000          /* 5 seconds */   
  };   
   
  /* Set: use keepalive on fd */   
  alive = 1;   
  if (setsockopt   
      (fd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &alive,   
       sizeof alive) != 0)   
    {   
      log_warn ("Set keep alive error: %s.\n", strerror (errno));   
      return -1;   
    }   
   
  if (WSAIoctl   
      (fd, SIO_KEEPALIVE_VALS, kavars, sizeof kavars, NULL, sizeof (int), &ret, NULL,   
       NULL) != 0)   
    {   
      log_warn ("Set keep alive error: %s.\n", strerror (WSAGetLastError ()));   
      return -1;   
    }   
   
  return 0;   
}  


最后说明一下 具体是用TCP协议自身的保活定时器实现心跳,还是通讯双方通过自定义协议实现心跳包来保活,还是通过第三方应用来进行保活,要取决于你的项目需求,网络负荷以及那种实现起来更便捷等多方面的因素,根据自身情况来使用。
原创粉丝点击