文章目录
  • TCP建立长连接
  • 长连接和短连接
  • 长连接的优势
  • TCP KEEPALIVE
  • 心跳包
  • 心跳检测步骤
  • 断线重连
  • 断线重连函数实现
  • 实例
  • 服务端
  • 客户端
  • 程序功能
  • 演示效果


TCP建立长连接

长连接和短连接
  • 长连接是指不论TCP的客户端和服务器之间是否有数据传输,都保持建立的TCP连接;
  • 短连接则不同,一旦两者之间的数据传输完毕,则立即断开连接,下次需要传输数据时再重新创建连接
长连接的优势

由于TCP建立连接需要进行三次握手,每次建立连接都需要进行资源消耗,对于频繁请求资源的客户端而言,长连接可减少大量开销

TCP KEEPALIVE

TCP中默认包含一个keep-alive机制用于检测连接是否可用,TCP在链路空闲时定时(默认两小时)发送数据给对方,双方处理结果如下:

  • 主机可达,对方收到数据之后响应ACK应答,则连接正常
  • 主机可达,但程序退出,对方则发送RST应答,发送TCP撤销连接
  • 主机可达,但程序崩溃,对方发送FIN消息

对方主机不响应ACK、RST,继续发送消息直到超时(两小时),就撤销连接

虽然TCP已经有了keep-alive机制保证连接的可靠性,但是其仍有一定的局限性

  • keep-alive只能检测连接是否存活,但无法检测其是否可用。例如:socket连接虽然存在,但是无法对该连接进行读写操作,keep-alive机制是无法获知的
  • keep-alive的灵活性不够,其默认间隔为两小时,无法及时获知TCP连接情况
  • keep-alive无法检测到机器断电、网线拔出、防火墙等导致的断线问题
心跳包

心跳包就是探测性的数据包,因为它像心跳一样每隔固定时间发送,以此来通知服务器自己仍处于存活状态,以保持长连接。心跳包的内容基本没有要求,一般是很小的数据包,或者是仅包含包头的空包

如果不主动关闭socket,操作系统是不会将其关闭的,这样socket所在的进程如果没有挂掉,则socket所占用的资源将一直无法回收。不仅如此,防火墙会自动关闭一定时间没有进行数据交互的连接。因此,我们需要自己实现心跳包,用于长连接的保活、断线处理及资源回收等操作。

一般来说,心跳包的频率为30~40秒,如果对实时性要求较高,则可进一步减小时间间隔

心跳检测步骤
  • 客户端定时发送探测包给服务器,同时启动超时定时器
  • 服务器接收到检测包之后就回复一个数据包
  • 客户端如果接收到服务器的返信,则表示服务器正常,定时器结束
  • 如果客户端定时器超时仍没有接收到服务器返信,则表示服务器停止工作
断线重连

长连接断开原因
在长连接通信过程中,双方的所有通信都建立在1条长连接上(1次TCP连接),因此连接需要持续保持双方连接才可使得双方持续通信
长连接会存在断开的情况,而断开原因主要是:

  • 长连接所在进程被杀死 NAT超时
  • 网络状态发生变化
  • 其他不可抗因素(网络状态差、DHCP的租期等等 )

维持长连接的另一个方法是断线重连

如果服务器因为某种故障导致连接无法正常使用,客户端使用心跳检测发现了服务器端掉线,则客户端可进行断线重连操作

我们将之前写的Linux创建tcp连接流程中的程序的客户端进行修改,进而实现服务器断开连接之后,客户端会自动重连

断线重连函数实现
void reconnect()
{
    printf("------Reconnect ...-----\n");
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6666);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  
    int retry = 5;		//掉线后重连五次,期间重连成功直接退出
    while(retry > 0)
    {
        confd = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
        if(confd < 0)
            perror("connect");
        else
            break;
        sleep(3);
        retry--;
    } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

需要注意的是,在判断连接是否断开时,可以判断recv函数是否成功,而非send函数
原因在于,send函数在将数据发送出去就立即返回,不论传输是否成功;
而recv是等数据传输完成之后才会返回,因此使用recv的结果作为判断连接是否成功才更为准确

实例

服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define MAXLINE 4096

int main() {
    int sockfd, connfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    int n;

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("Create socket error: %s (errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf("Bind socket error: %s (errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    if (listen(sockfd, 10) == -1) {
        printf("Listen socket error: %s (errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("====Waiting for client's request=======\n");

    while (1) {
        if ((connfd = accept(sockfd, (struct sockaddr *)NULL, NULL)) == -1) {
            printf("Accept socket error: %s (errno: %d)\n", strerror(errno), errno);
            continue;
        }

        while (1) {
            n = recv(connfd, buff, MAXLINE, 0);
            if (n <= 0) {
                if (n == 0) {
                    printf("Client disconnected\n");
                } else {
                    printf("Recv error: %s (errno: %d)\n", strerror(errno), errno);
                }
                break;
            }
            buff[n] = '\0';
            printf("Recv msg from client: %s\n", buff);

			if(!strcmp(buff, "keepalive"))
				continue;

            memset(buff, 0x00, sizeof(buff));
            printf("send msg to Client: ");
            fgets(buff, MAXLINE, stdin);

            if (send(connfd, buff, strlen(buff), 0) < 0) {
                printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
                break;
            }
        }

        close(connfd);
    }

    close(sockfd);
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
客户端
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 4096

int sockfd = 0;
int confd = 0;
int count = 0;
char ipAddr[24] = "";

int reconnect() {
    close(sockfd);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 0;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6666);
    saddr.sin_addr.s_addr = inet_addr(ipAddr);

    int retry = 5;
    while (retry > 0) {
        confd = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
        if (confd < 0) {
            perror("connect");
        } else {
            return 1;
        }
        sleep(3);
        printf("------Reconnect ...-----\n");

        retry--;
    }
    return 0;
}

void printMes(int signo) {
    char heartData[12] = "keepalive";
    printf("Get a SIGALRM, %d counts!\n", ++count);
    if (send(sockfd, heartData, strlen(heartData), 0) < 0) {
        printf("send msg error: %s(errno :%d)\n", strerror(errno), errno);
        if (0 == reconnect()) {
            printf("The reconnect failed!!!\n");
            return;
        }
    }
}

void initTimer() {
    struct itimerval tick;

    signal(SIGALRM, printMes);
    memset(&tick, 0, sizeof(tick));

    tick.it_value.tv_sec = 10;
    tick.it_value.tv_usec = 0;

    tick.it_interval.tv_sec = 10;
    tick.it_interval.tv_usec = 0;

    if (setitimer(ITIMER_REAL, &tick, NULL) < 0) {
        printf("Set timer failed!\n");
    }
}

int main(int argc, char** argv) {
    int n;
    char recvline[4096], sendline[4096];
    struct sockaddr_in servaddr;

    if (argc != 2) {
        printf("usage: ./client <ipaddress>\n");
        return 0;
    }

    memcpy(ipAddr, argv[1], sizeof(ipAddr));

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s (errno :%d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    if (inet_pton(AF_INET, ipAddr, &servaddr.sin_addr) <= 0) {
        printf("inet_pton error for %s\n", argv[1]);
        return 0;
    }

    if ((confd = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) < 0) {
        printf("connect socket error: %s(errno :%d)\n", strerror(errno), errno);
        return 0;
    }

    initTimer();

    while (1) {
        memset(sendline, 0x00, sizeof(sendline));
        printf("send msg to server:\n");
        fgets(sendline, 4096, stdin);

        if (strstr(sendline, "exit")) {
            printf("The socket is free....\n");
            break;
        }

        if (send(sockfd, sendline, strlen(sendline), 0) < 0) {
            printf("send msg error: %s(errno :%d)\n", strerror(errno), errno);
            if (0 == reconnect()) {
                return 0;
            }
        }

        n = recv(sockfd, recvline, MAXLINE, 0);
        if (n < 0) {
            printf("recv error: %s(errno :%d)\n", strerror(errno), errno);
            if (0 == reconnect()) {
                return 0;
            }
        } else if (n == 0) {
            printf("server closed connection\n");
            if (0 == reconnect()) {
                return 0;
            }
        } else {
            recvline[n] = '\0';
            printf("recv msg from server: %s\n", recvline);
        }
    }

    close(sockfd);
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
程序功能

先启动服务端,等待客户端进行连接。如果客户端发起连接后,每隔10s发送一个心跳包

  • 如果服务端接受到心跳包,则不输入内容,直接等待下一次客户端发送的信息;
  • 如果客户端接受到字符串,则等待服务端输入字符串进行回应;
  • 如果客户端在发送字符串后,没有收到回应信息,则重新发起连接请求;
演示效果

程序手动在服务端执行过程中,输入ctrl+C关闭服务端进程,在客户端重连次数之内再重新打开即可

  • 客户端
  • 服务端