查看原文
其他

玩的就是心跳,TCP耍起来呀

IT服务圈儿 2023-02-06

The following article is from 涛歌依旧 Author 点击关注👉👉

来源丨经授权转自 涛歌依旧(ID:ai_taogeyijiu_2021)

作者丨涛歌依旧


大家好,我是涛哥。

今天,我们来聊TCP心跳相关的问题,并用实际网络程序来验证,这个网络程序有一定技巧,希望大家掌握思路。先来看一个有趣的问题,大家可以自己思考一下:

客户端A和服务端B都连接在交换机上,并且建立了TCP连接,两端保持静默状态,不发数据。此时,交换机突然断电,那么,客户端和服务端的连接状态会变化吗?

先说答案:客户端和服务端的连接状态当然不会变化,因为没有任何通知让它们变化啊。然而,在有些场景下,客户端和服务端需要快速感知断开的操作,咋办呢?

可以考虑在应用层发送消息来进行探测,也可以使用TCP协议中自带的心跳机制(keepalive),那怎么写程序测试呢?咱们一起来玩一下,实战起来,挺有意思的!

涛哥手绘(心跳图)


一. 服务端程序

先来看典型的服务端程序吧,很简单:

#include <stdio.h>#include <winsock2.h> // winsock接口#pragma comment(lib, "ws2_32.lib") // winsock实现 int main(){ WORD wVersionRequested; // 双字节,winsock库的版本 WSADATA wsaData; // winsock库版本的相关信息 wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257   // 加载winsock库并确定winsock版本,系统会把数据填入wsaData中 WSAStartup( wVersionRequested, &wsaData );   // AF_INET 表示采用TCP/IP协议族 // SOCK_STREAM 表示采用TCP协议 // 0是通常的默认情况 unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_family = AF_INET; // TCP/IP协议族 addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket对应的IP地址 addrSrv.sin_port = htons(8888); // socket对应的端口 // 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程) bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));  listen(sockSrv, 5);  // sockSrv为监听状态下的socket // &addrClient是缓冲区地址,保存了客户端的IP和端口等信息 // len是包含地址信息的长度 // 如果客户端没有启动,那么程序一直停留在该函数处 SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len); while(1); // 卡住 closesocket(sockConn); closesocket(sockSrv); WSACleanup(); return 0;}编译并运行程序,开启服务端。


二. 客户端程序

接下来,我们仅考虑在客户端程序中增加心跳检测的部分。要说明的是,所谓的心跳,其实就是默认的网络包,简称心跳包。客户端程序如下:
#include <winsock2.h>#include <stdio.h>#pragma comment(lib, "ws2_32.lib") #define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR, 4) // tcp keepalive结构体typedef struct tcp_keepalive { u_long onoff; u_long keepalivetime; u_long keepaliveinterval; }TCP_KEEPALIVE; // 通信的socketSOCKET sockClient = 0; // 监测线程DWORD WINAPI monitorThread(LPVOID pM) { while(1) { char szRecvBuf[10] = {0};    int nRet = recv(sockClient, szRecvBuf, 1, MSG_PEEK);  // 注意,最后一个参数必须是MSG_PEEK,否则会影响主线程接收信息    if(nRet <= 0) // 实际上,等于0表示服务端主动关闭通信socket { printf("监测到啦: nRet is %d\n", nRet); closesocket(sockClient); break; } Sleep(200); } return 0;} int main(){ WORD wVersionRequested; WSADATA wsaData; wVersionRequested = MAKEWORD(1, 1); WSAStartup( wVersionRequested, &wsaData ); sockClient = socket(AF_INET, SOCK_STREAM, 0);  // 启用tcp keepalive机制 #if 1 // 设置SO_KEEPALIVE int iKeepAlive = 1; int iOptLen = sizeof(iKeepAlive); setsockopt(sockClient, SOL_SOCKET, SO_KEEPALIVE, (char *)&iKeepAlive, iOptLen); TCP_KEEPALIVE inKeepAlive = {0, 0, 0}; unsigned long ulInLen = sizeof(TCP_KEEPALIVE); TCP_KEEPALIVE outKeepAlive = {0, 0, 0}; unsigned long ulOutLen = sizeof(TCP_KEEPALIVE); unsigned long ulBytesReturn = 0; // 设置心跳参数 inKeepAlive.onoff = 1; // 是否启用    inKeepAlive.keepalivetime = 1000;       // 在tcp通道空闲1000毫秒后,开始发送心跳包检测 inKeepAlive.keepaliveinterval = 500; // 心跳包的间隔时间是500毫秒 /* 补充上面的"设置心跳参数": 当没有接收到服务器反馈后,对于不同的Windows版本,客户端的心跳尝试次数是不同的,      比如,对于Win XP/2003而言, 最大尝试次数是5次,其它的Windows版本也各不相同。      当然啦,如果是在Linux上,那么这个最大尝试此时其实是可以在程序中设置的。 */       // 调用接口,启用心跳机制 WSAIoctl(sockClient, SIO_KEEPALIVE_VALS, &inKeepAlive, ulInLen, &outKeepAlive, ulOutLen, &ulBytesReturn, NULL, NULL); #endif SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.102"); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(8888); connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 开启监测线程 HANDLE handle = CreateThread(NULL, 0, monitorThread, NULL, 0, NULL); while(1); // 卡住 CloseHandle(handle); closesocket(sockClient); WSACleanup(); return 0;}编译并运行程序,开启客户端。


三. 开始实验咯

如上的客户端和服务端建立了TCP连接,在两侧分别用netstat可以看到,它们都是established的状态。

然后,直接让交换机断电,过会儿,在服务端执行netstat命令,发现还是established状态,符合预期。

但是,在客户端执行netstat命令后,发现TCP的状态变成了closed状态,而且,客户端打印的log如下:

监测到啦: nRet is -1那么,为什么会这样呢?

当客户端将心跳发给服务端后,眼巴巴地期望得到服务端的反馈,如果没有收到反馈,协议栈自然有理由认为客户端是死连接了(于是, 客户端会发RST包重置链接,也就是说,这链接时无效的了),则之后客户端的任何I/O操作或待处理的I/O操作都将失败。

所以,可用recv函数去偷窥接收的内核缓冲区中的数据,如果反馈-1, 那就表明通信断了。此处recv函数的目的不是为了去获取数据,也不是为了去探测什么数据,而是简单地执行一个io操作,一旦启动心跳机制,网络异常后,io操作就会自然失败。

之所以选择recv并把最后一个参数置为MSG_PEEK,  是因为我们要找到一个不影响主线程通信的io操作函数 )。顺便说一句,如果服务端主动关闭通信的socket, 客户端的recv函数会返回0, 所以,综合起来说,为了检测出连接的异常,用<=0进行判断。

先说这么多,希望有兴趣的朋友多实践,不要停留在理解表面的原理。


1、消息队列经典十连问

2、Python 绘制惊艳的桑基图

3、当Synchronized遇到这玩意儿,有个大坑,要注意!

4、InnoDB原理篇:聊聊数据页变成索引这件事

5、图解 | 你管这破玩意叫网络?

点分享

点点赞

点在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存