windows下测试tcp长连接,实现正常接通信后拔掉网线,60秒内插上网线,通信恢复。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值