论TCP客户端如何快速判断与服务器断联

TCP客户端如何快速判断与服务器断联

问题产生的背景说明

我曾经对接中国电信的服务器的时候,遇到了该问题。中国电信要求和服务器断联之后(异常或者正常断联,其中包括拔掉网线)能够在30s之内连接上中国电信服务器并且接收服务器下发的无线配置。客户端重启无线,能够用无线扫描工具扫描该无线ssid已经被同步。该需求当时面临如下几个问题:

1.客户端在和服务器连接的时候需要通过一系列的鉴权机制(dh秘钥协商),然后才可以和服务器通信
2. 无线配置到生效(ssid改变),需要花费一定的时间。

  • 为了满足这一系列的动作,需要优化如下几点:
  1. 能够快速判断和服务器已经断开(正常或者异常)
  2. 必须优化无线生效时间,尽可能短
  3. 在拔掉网线再插上网线,必须尽快拿到分配的ip地址,尽快和服务器建立连接

今天我们来讨论如何快速的检测和服务器已经断联。

目前检测客户端和服务器断联的方法

  • epoll(能够检测正常的断开连接,事件触发机制。优点是快速)
  • read方式检测
  • keeplive方式检测
  • 自定义心跳包方式检测
  • getsockopt

下面我们逐个分析上述方式的优缺点

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

相比较select和poll的方式,epoll对于监听的文件描述符没有限制(唯一的限制就是内核本身支持的文件描述符个数-可修改)。

我们在注册epoll的时候主要是监听两类事件(EPOLLIN和EPOLLOUT),这两类事件分别代表着可读和可写。我们可以创建读写回调函数,当检测到对应事件的时候,调用对应的程序。为了检测客户端和服务器已经断开我们需要注册另一类事件(EPOLLRDHUP)。该事件代表的是读关闭,当服务器close的时候,会触发该事件

说明:EPOLLERR 只有采取动作时,才能知道是否对方异常。即对方突然断掉,是不可能有此事件发生的。只有自己采取动作(当然自己此刻也不知道),read,write时,出EPOLLERR错,说明对方已经异常断开。

总结: epoll的事件触发机制不满足上述需求(插拔网线是检测不到的)。但是它的优缺点不言而喻(文件描述符监听不受限制,事件触发)。

read方式检测

但我们不采用epoll的时候(即不采用事件触发机制的时候)。我们可以创建一个单独的读线程,文件描述符设置为非阻塞。那么在读线程中死循环调用read函数。这时候可以根据read的返回值来判断socket连接是否已经出了问题。检测方式如下:
调用了read函数读取socket,如果read的返回值为小于0同时错误码不为errno == EAGAIN || errno == EWOULDBLOCK),则代表和服务器的连接已经断开。

说明:该方式适用于各种异常情况,但是因为需要read循环接收,必须开辟线程。系统创建线程是需要有开销的,所以我不推荐该方式。

自定义心跳包检测方式

该方式对于大家都不陌生,说的直白一点就是定时发送心跳包给服务器。如果发送失败或者服务器没有定时回复,则认为连接已经断开。该方式通用、常见并且有效。在一些实时性要求不高的地方推荐这种方式。(基本现在的cs架构都有心跳机制)。但是该方式不适合我上述需求,因为心跳包是有间隔的,有间隔就代表有延时。同时又不能把心跳包设置的太短,这样会增加服务器负荷。

keeplive机制

关于keeplive机制,我在这里不详细说明。说白了就是利用系统发送心跳包。和上述心跳包检测方式一致

getsockopt机制

在应用程序中可以通过调用如下代码来判断连接是否断开

  struct tcp_info info;
  int len = sizeof(struct tcp_info);
  getsockopt(g_fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);
  if ((info.tcpi_state == TCP_ESTABLISHED)) {
    // myprintf("socket connected\n");
    return 1;
  } else {
    printf("===========socket disconnected");
    // myprintf("socket disconnected\n");
    return 0;
  }

运行结果如下:
在这里插入图片描述

说明:该方法只适用于正常断开服务器(即服务器主动close),不适用于拔掉网线的情况

下面提供几种切换网络,断开网络的方案

arp方案

在客户端开启一个定时器,将定时事件设置很短,比如3秒。然后一直ping网关,设置ping的超时时间为1秒。如果检测到2次未返回(次数可以自己根据实际情况定义),则认为连接已经断开

说明: 该方案有一个弊端,这种方式还是在发送数据包,如果服务器没有直接和客户端连接,那么不会加重服务器的负担。(这一点比超短时间发送心跳包友好)。但是该方式本质上仍然是发送数据包。所以仍然会对直连的网关或者路由设备带来负担

判断有无路由或者路由表时候改变

我们可以通过如下代码来获取默认路由的ip地址


#define BUFSIZE 8192

struct route_info {
  u_int dstAddr;
  u_int srcAddr;
  u_int gateWay;
  char ifName[IF_NAMESIZE];
};
int readNlSock(int sockFd, char *bufPtr, int seqNum, int pId) {
  struct nlmsghdr *nlHdr;
  int readLen = 0, msgLen = 0;
  do {
    //收到内核的应答
    if ((readLen = recv(sockFd, bufPtr, BUFSIZE - msgLen, 0)) < 0) {
      perror("SOCK READ: ");
      return -1;
    }

    nlHdr = (struct nlmsghdr *)bufPtr;
    //检查header是否有效
    if ((NLMSG_OK(nlHdr, readLen) == 0) || (nlHdr->nlmsg_type == NLMSG_ERROR)) {
      perror("Error in recieved packet");
      return -1;
    }

    if (nlHdr->nlmsg_type == NLMSG_DONE) {
      break;
    } else {
      bufPtr += readLen;
      msgLen += readLen;
    }

    if ((nlHdr->nlmsg_flags & NLM_F_MULTI) == 0) {
      break;
    }
  } while ((nlHdr->nlmsg_seq != seqNum) || (nlHdr->nlmsg_pid != pId));
  return msgLen;
}
//分析返回的路由信息
void parseRoutes(struct nlmsghdr *nlHdr, struct route_info *rtInfo,
                 char *gateway) {
  struct rtmsg *rtMsg;
  struct rtattr *rtAttr;
  int rtLen;
  char *tempBuf = NULL;
  struct in_addr dst;
  struct in_addr gate;

  tempBuf = (char *)malloc(100);
  rtMsg = (struct rtmsg *)NLMSG_DATA(nlHdr);
  // If the route is not for AF_INET or does not belong to main routing table
  // then return.
  if ((rtMsg->rtm_family != AF_INET) || (rtMsg->rtm_table != RT_TABLE_MAIN)) {
    FREE(tempBuf);
    return;
  }

  rtAttr = (struct rtattr *)RTM_RTA(rtMsg);
  rtLen = RTM_PAYLOAD(nlHdr);
  for (; RTA_OK(rtAttr, rtLen); rtAttr = RTA_NEXT(rtAttr, rtLen)) {
    switch (rtAttr->rta_type) {
      case RTA_OIF:
        if_indextoname(*(int *)RTA_DATA(rtAttr), rtInfo->ifName);
        break;
      case RTA_GATEWAY:
        rtInfo->gateWay = *(u_int *)RTA_DATA(rtAttr);
        break;
      case RTA_PREFSRC:
        rtInfo->srcAddr = *(u_int *)RTA_DATA(rtAttr);
        break;
      case RTA_DST:
        rtInfo->dstAddr = *(u_int *)RTA_DATA(rtAttr);
        break;
    }
  }
  dst.s_addr = rtInfo->dstAddr;
  if (strstr((char *)inet_ntoa(dst), "0.0.0.0")) {
    gate.s_addr = rtInfo->gateWay;
    sprintf(gateway, (char *)inet_ntoa(gate));
    gate.s_addr = rtInfo->srcAddr;
    gate.s_addr = rtInfo->dstAddr;
  }
  FREE(tempBuf);
  return;
}

int get_gateway_ip(char *gateway) {
  struct nlmsghdr *nlMsg;
  struct rtmsg *rtMsg;
  struct route_info *rtInfo;
  char msgBuf[BUFSIZE];

  int sock, len, msgSeq = 0;

  if ((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0) {
    perror("Socket Creation: ");
    return -1;
  }

  memset(msgBuf, 0, BUFSIZE);

  nlMsg = (struct nlmsghdr *)msgBuf;
  rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);

  nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));  // Length of message.
  nlMsg->nlmsg_type =
      RTM_GETROUTE;  // Get the routes from kernel routing table .

  nlMsg->nlmsg_flags =
      NLM_F_DUMP | NLM_F_REQUEST;  // The message is a request for dump.
  nlMsg->nlmsg_seq = msgSeq++;     // Sequence of the message packet.
  nlMsg->nlmsg_pid = getpid();     // PID of process sending the request.

  if (send(sock, nlMsg, nlMsg->nlmsg_len, 0) < 0) {
    printf("Write To Socket Failed…\n");
    close(sock);
    return -1;
  }

  if ((len = readNlSock(sock, msgBuf, msgSeq, getpid())) < 0) {
    printf("Read From Socket Failed…\n");
    close(sock);
    return -1;
  }

  rtInfo = (struct route_info *)malloc(sizeof(struct route_info));
  for (; NLMSG_OK(nlMsg, len); nlMsg = NLMSG_NEXT(nlMsg, len)) {
    memset(rtInfo, 0, sizeof(struct route_info));
    parseRoutes(nlMsg, rtInfo, gateway);
  }
  free(rtInfo);
  close(sock);
  return 0;
}

如果路由表改变或者消失,那么则认为断开连接。该方式需要开启一个定时器,循环检测。但是相比于ping的方式,该方式不会对对外的设备带来负担。所以这一点是比ping的方式友好

总结:目前我只想到了这两种比较好的方式,在我的实际项目中我采用的是判断路由表的方式(定时器设置为1秒,检测异常断联)结合epoll的事件触发方式(检测正常断联)以及心跳报文超时的方式。能够快速的检测。如果大家有 更好的方式欢迎留言,最好是异步通知的方式。该博客不定时更新(如果本人想到了新的方案)

欢迎加入QQ群(610849576),大家一起交流

Boost C++库中的Asio库提供了一种便捷的方式来处理TCP服务器客户端连接管理。当客户端断开连接时,Asio会自动检测到网络层的问题,比如关闭的套接字、超时或者其他错误。通常,你可以通过以下步骤来判断客户端是否已断开: 1. **设置接收事件**: 当Asio注册一个异步读取操作时,如果客户端不再发送数据或网络连接异常,它会触发`read`操作的结果。 ```cpp boost::asio::ip::tcp::socket socket(io_service); // ...其他初始化... socket.async_read_some(boost::asio::buffer(data), read_handler); ``` 在`read_handler`中检查结果: ```cpp void read_handler(const boost::system::error_code& error, std::size_t bytes_transferred) { if (!error) { // 客户端还在连接上,处理数据 } else { // error说明连接有问题,可能是断开 if (error == boost::asio::error::eof) { // EOF意味着远程端已经关闭连接 handle_client_disconnect(); } else { // 其他类型的错误,如网络中断等 handle_network_error(error); } } } ``` 2. **设置超时**: 使用`deadline_timer`设置一个超时时间,如果在这个时间内没有任何数据到达,也可能视为客户端断开。 ```cpp boost::asio::deadline_timer timeout(io_service, deadline); timeout.expires_from_now(boost::posix_time::seconds(5)); auto connect_handler = [&, timeout](const boost::system::error_code& error) mutable { if (!error) { // 连接成功,开始监听数据 } else { // 错误,可能是超时或连接失败 timeout.cancel(); handle_connect_error(error); } }; timeout.async_wait(connect_handler); ``` 在上述代码中,如果`timeout`到期而未收到新的数据,那么`connect_handler`会被调用,这时可以判断客户端断开了。 **相关问题--:** 1. 如何处理Asio的超时错误? 2. Boost Asio还有哪些方法可以检测网络连接状态变化? 3. 在实际应用中,如何优雅地处理客户端突然断开连接的情况?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值