windows11下拔掉网线,60秒内插上网线,通信恢复【c++ tcp实现】。
前言
当TCP的Peer A ,Peer B 两端建立了连接之后,如果一端突然拔掉网线或拔掉电源时,怎么检测到拔掉网线或者拔掉电源、链路不通?原因是在需要长连接的网络通信程序中,经常需要心跳检测机制,来实现检测对方是否在线或者维持网络连接的需要。
同时如果在网线被短时间内拔掉后又恢复插入,此时通信是否能够恢复到正常的数据收发(不是指重新连接,重建新的socket)。
必要的条件
要能够在短时间内拔插网线后自动的恢复原来的正常通信状态,那么就必须满足下面的铁律:
1)如果网线断开的时间短暂,在SO_KEEPALIVE设定的探测时间间隔内,并且两端在此期间没有任何针对此长连接的网络操作。当连上网线后此TCP连接可以自动恢复,继续进行正常的网络操作。
2)如果网线断开的时间很长,超出了SO_KEEPALIVE设定的探测时间间隔,或者两端期间在此有了任何针对此长连接的网络操作。当连上网线时就会出现ETIMEDOUT或者ECONNRESET的错误。你必须重新建立一个新的长连接进行网络操作。
关键实现片段:
首先我们来看实现客户端的关键函数或内容:
// 监测以及数据接收线程
DWORD WINAPI monitorThread(LPVOID pM)
{
while(1)
{
char szRecvBuf[10] = {0};
int nRet = recv(sockClient, szRecvBuf, 1, MSG_PEEK); // 注意, 最后一个参数必须是MSG_PEEK, 否则会影响主线程接收信息,这里其实是预读,获取接收数据的缓冲区中是否存在有效数据,因为这里用来判别数据是否存在,因此只需读取1字节
if(nRet <= 0) // 实际上, 等于0表示服务端主动关闭通信socket
{ //判断关闭或者异常的代码区
if(nRet==0) {
std::cout << "server exec close client socket" << std::endl;
}else if(nRet==-1) {
std::cout << "other error nRet is " << nRet << std::endl;
}
closesocket(sockClient);
break;
}
else { //实际的接收数据的地方
char buf[1024] = {0};
int readSize = recv(sockClient, buf, 1024, 0);
std::cout << "recv bytes " << readSize << " , data:" << buf << std::endl;
}
Sleep(200);
}
return 0;
}
为了模拟实际的通信交互,开启一个模拟发送的线程
//该线程模拟数据的发送,以便于测试真实的通信发送
DWORD WINAPI monitorSendThread(LPVOID pM)
{
while(1) {
int s = sockClient ;
auto curTime = system_clock::now();
double xx = std::chrono::duration<double,std::milli>
(curTime-sendPoint).count();
if (xx>=60*1000) {
//这里是为了模拟间隙性的发送
char buf[100] = {0};
memcpy(buf, "1234567890", strlen("1234567890"));
if (sockClient!=-1) {
send(sockClient, buf , 10 , 0) ;
}
sendPoint = system_clock::now();
std::chrono::milliseconds dura(1000);
std::this_thread::sleep_for(dura);
}
}
}
main 函数中的关键代码
//设置接收缓冲区为8M
int optVal = 0;
int optLen = sizeof(optVal);
optVal = 8*1024*1024;
setsockopt(sockClient, SOL_SOCKET, SO_RCVBUF, (char*)&optVal, optLen);
//设置发送缓冲区为2M
int bufferSize = 2* 1024 * 1024; // 设置为1MB
int result = setsockopt(sockClient, SOL_SOCKET, SO_SNDBUF, (char*)&bufferSize, sizeof(bufferSize));
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.58.93");
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);
HANDLE handle1 = CreateThread(NULL, 0, monitorSendThread, NULL, 0, NULL);
接下来我们看看服务端的实现关键片段:
首先我们需要对客户端socket设置开启保活keepalive,因此需要一个保活设置函数
int socket_set_keepalive (int fd)
{
#ifdef WIN32
struct tcp_keepalive kavars;
struct tcp_keepalive alive_out;
kavars.onoff = true;
kavars.keepalivetime = 180*1000; //180秒后还没有数据则会发起保活探测
kavars.keepaliveinterval = 5*1000;
/* Set: use keepalive on fd */
int alive = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &alive,sizeof alive) != 0)
{
return -1;
}
unsigned long ulBytesReturn = 0;
WSAIoctl(fd, SIO_KEEPALIVE_VALS, &kavars, sizeof(kavars),&alive_out, sizeof(alive_out), &ulBytesReturn, NULL, NULL);
#else
int keepAlive = 1; // 非0值,开启keepalive属性
int keepIdle = 60*1000; // 如该连接在120秒内没有任何数据往来,则进行此TCP层的探测
int keepInterval = 5; // 探测发包间隔为5秒
int keepCount = 3; // 尝试探测的最多次数
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(fd, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
#endif
return 0;
}
接下来我们看看main函数中的关键代码
int main() {
// 初始化winsock的环境
...
// 1.创建监听套接字
...
// 2.绑定到ip与端口
// 3.监听套接字
//设置保活
socket_set_keepalive (sListen);
cout << "server start on :" << " xx.xx.xx.xx:8888 " << endl;
mBaseDuation = system_clock::now();
// 4. select开始了
fd_set readSet;//定义一个读(接受消息)的集合
FD_ZERO(&readSet);//初始化集合
FD_SET(sListen, &readSet);
// 不停的select才可以读取套接字的状态改变
while (true) {
fd_set tmpSet; // 定义一个临时的集合
FD_ZERO(&tmpSet); // 初始化集合
tmpSet = readSet; // 每次循环都是所有的套接字
// 设置超时时间
struct timeval timeout;
timeout.tv_sec = 120;
timeout.tv_usec = 0;
// 利用select选择出集合中可以读写的多个套接字,有点像筛选
int ret = select(0, &tmpSet, NULL, NULL, &timeout);//最后一个参数为NULL,一直等待,直到有数据过来.这里select函数实际也执行了改变socket操作
if (ret == SOCKET_ERROR) {
continue;
}
// 成功筛选出来的tmpSet可以发送或者接收的socket
for (int i = 0; i < tmpSet.fd_count; ++i) {
//获取到套接字
SOCKET selectedSocket = tmpSet.fd_array[i];
// 接收到客户端的链接
if (selectedSocket == sListen) {
SOCKET c = accept(selectedSocket, NULL, NULL);
socket_set_keepalive (c) ; //保活
//设置发送缓冲区为8M
int nSendBuf=8*1024*1024;//设置为8M
setsockopt(c,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
//设置接收缓冲区为2M
int nRecvBuf=2*1024*1024;//设置为2M
setsockopt(c,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
// fd_set集合最大值为64
if (readSet.fd_count < FD_SETSIZE) {
//往集合中添加客户端套接字
FD_SET(c, &readSet);
cout << c << " logged in." << endl;
mBaseDuation = system_clock::now();
// 给客户端发送欢迎
char buf[100] = {0};
sprintf(buf, "hello from server", c);
send(c, buf, 100, 0);
} else {
cout << "max 64 clients for now." << endl;
}
} else {
// 接收客户端的数据
char buf[100] = {0};
ret = recv(selectedSocket, buf, 100, 0);
if (ret == SOCKET_ERROR || ret == 0) {
//这种情况下代表 收到了对端断开信号
closesocket(selectedSocket);
FD_CLR(selectedSocket, &readSet);
cout << "client fd = " << selectedSocket << " , logged off." << endl;
}else if(ret<0) { //代表出错
//cout << "selectedSocket net " << " error ! " << endl;
if(errno == EINTR ||errno == EAGAIN ||errno == EWOULDBLOCK) {
cout << "errno == EINTR ||errno == EAGAIN ||errno == EWOULDBLOCK" << endl;
continue;
}
}
else { // >0代表收到数据
mBaseDuation = system_clock::now();
cout << selectedSocket << " recv: " << buf << endl;
}
//实验着发送一些数据到客户端,将收到的数据发送回去
int error;
int lenx = sizeof(error);
//int WSAAPI getsockopt(SOCKET s,int level,int optname,char *optval,int *optlen);
if (getsockopt(selectedSocket, SOL_SOCKET, SO_ERROR, (char *)(&error), &lenx) < 0) {
// getsockopt 调用失败
perror("getsockopt fault !");
//return -1;
continue;
}
if (error == 0) {
if (send(selectedSocket, buf, 100, 0)>0){
mBaseDuation = system_clock::now();
}
cout << "server echo send data to client "<< selectedSocket << " recv data ! " << buf << endl;
}else {
}
}
}
}
// 关闭监听套接字
...
// 清理winsock环境
...
return 0;
}
在编写好客户端与服务端后,我们开始进行测试:
1)首先将两台电脑的网线接入局域网内的交换机上。
2)运行服务端程序。
3)运行客户端程序。
4)在客户端与服务端建立了连接后可以看到服务端收到数据。
5)在服务端收到数据后立刻拔掉客户端的网线,设立秒表查看时间。
6)在秒表到45秒后,快速的将客户端网线插回交换机。
7)在等到约60秒后,可以发现客户端定时发送的数据成功的发送到了服务端,等待几分钟后,发现网络通信交互正常。【可重复4-7的步骤进行测试】
8)重新在服务端收到数据后,拔掉客户端网线,同时设立秒表查看时间。
9)当秒表时间超过60秒后,客户端会显示 other error nRet is -1 .
10) 此时插上客户端的网线到交换机,此时网络已经无法恢复【因为客户端的socket已经被检测到断开了,此时只有建立新的socket重新连接才能继续通信】。
好了,以上内容,希望对你有所帮助。
如果有需要源码的请到 :test-demo