玩的就是心跳,TCP耍起来呀
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;
// 通信的socket
SOCKET 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进行判断。
先说这么多,希望有兴趣的朋友多实践,不要停留在理解表面的原理。
3、当Synchronized遇到这玩意儿,有个大坑,要注意!
点分享
点点赞
点在看