TCP保活機制

在須要長鏈接的網絡通訊程序中,常常須要心跳檢測機制,來實現檢測對方是否在線或者維持網絡鏈接的須要。這一機制是在應用層實現的,對應的,在TCP協議中,也有相似的機制,就是TCP保活機制。html

1、爲何須要保活機制?

設想這種狀況,TCP鏈接創建後,在一段時間範圍內雙發沒有互相發送任何數據。思考如下兩個問題:後端

  1. 怎麼判斷對方是否還在線。這是由於,TCP對於非正常斷開的鏈接系統並不能偵測到(好比網線斷掉)。
  2. 長時間沒有任何數據發送,鏈接可能會被中斷。這是由於,網絡鏈接中間可能會通過路由器、防火牆等設備,而這些有可能會對長時間沒有活動的鏈接斷掉。

基於上面兩點考慮,須要保活機制。bash

2、TCP保活機制的實現(Linux)

保活機制是由一個保活計時器實現的。當計時器被激發,鏈接一段將發送一個保活探測報文,另外一端接收報文的同時會發送一個ACK做爲響應。微信

一、相關配置

具體實現上有如下幾個相關的配置:網絡

  • 保活時間:默認7200秒(2小時)
  • 保活時間間隔:默認75秒
  • 保活探測數:默認9次

查看Linux系統中TCP保活機制對應的系統配置以下(不一樣系統實現可能不一樣):socket

sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_time 
7200
sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_intvl 
75
sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_probes 
9
複製代碼
二、過程描述

鏈接中啓動保活功能的一端,在保活時間內鏈接處於非活動狀態,則向對方發送一個保活探測報文,若是收到響應,則重置保活計時器,若是沒有收到響應報文,則通過一個保活時間間隔後再次向對方發送一個保活探測報文,若是尚未收到響應報文,則繼續,直到發送次數到達保活探測數,此時,對方主機將被確認爲不可到達,鏈接被中斷。tcp

TCP保活功能工做過程當中,開啓該功能的一端會發現對方處於如下四種狀態之一:分佈式

  1. 對方主機仍在工做,而且能夠到達。此時請求端將保活計時器重置。若是在計時器超時以前應用程序經過該鏈接傳輸數據,計時器再次被設定爲保活時間值。
  2. 對方主機已經崩潰,包括已經關閉或者正在從新啓動。這時對方的TCP將不會響應。請求端不會接收到響應報文,並在通過保活時間間隔指定的時間後超時。超時前,請求端會持續發送探測報文,一共發送保活探測數指定次數的探測報文,若是請求端沒有收到任何探測報文的響應,那麼它將認爲對方主機已經關閉,鏈接也將被斷開。
  3. 客戶主機崩潰而且已重啓。在這種狀況下,請求端會收到一個對其保活探測報文的響應,但這個響應是一個重置報文段RST,請求端將會斷開鏈接。
  4. 對方主機仍在工做,可是因爲某些緣由不能到達請求端(例如網絡沒法傳輸,並且可能使用ICMP通知也可能不通知對方這一事實)。這種狀況與狀態2相同,由於TCP不能區分狀態2與狀態4,結果是都沒有收到探測報文的響應。

3、保活機制的弊端

理解了上面的實現,就能夠發現其存在如下兩點主要弊端:區塊鏈

  1. 在出現短暫的網絡錯誤的時候,保活機制會使一個好的鏈接斷開;
  2. 保活機制會佔用沒必要要的帶寬;

因此,保活機制是存在爭議的,主要爭議之處在因而否應在TCP協議層實現,有兩種主要觀點:其一,保活機制沒必要在TCP協議中提供,而應該有應用層實現;其二,認爲大多數應用都須要保活機制,應該在TCP協議層實現。ui

保活功能在默認狀況下是關閉的。沒有通過應用層的請求,Linux系統不會提供保活功能。

4、保活機制應用代碼示例

一、 服務端代碼
/* server */
#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<netinet/tcp.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
#include <sys/time.h>

#define MAX_EVENTS 1024
#define LISTEN_PORT 33333
#define MAX_BUF 65536

struct echo_data;
int setnonblocking(int sockfd);
int events_handle(int epfd, struct epoll_event ev);
void run();

// 應用TCP保活機制的相關代碼
int set_keepalive(int sockfd, int keepalive_time, int keepalive_intvl, int keepalive_probes) {
    int optval;
    socklen_t optlen = sizeof(optval);
    optval = 1;
    if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen)) {
        perror("setsockopt failure.");
        return -1;
    }

    optval = keepalive_probes;
    if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, optlen)) {
        perror("setsockopt failure.");
        return -1;
    }

    optval = keepalive_intvl;
    if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, optlen)) {
        perror("setsockopt failure.");
        return -1;
    }

    optval = keepalive_time;
    if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, optlen)) {
        perror("setsockopt failure.");
        return -1;
    }
}

int main(int _argc, char* _argv[]) {
    run();

    return 0;
}

void run() {
    int epfd = epoll_create1(0);
    if (-1 == epfd) {
        perror("epoll_create1 failure.");
        exit(EXIT_FAILURE);
    }

    char str[INET_ADDRSTRLEN];
    struct sockaddr_in seraddr, cliaddr;
    socklen_t cliaddr_len = sizeof(cliaddr);
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(LISTEN_PORT);

    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {
        perror("bind server addr failure.");
        exit(EXIT_FAILURE);
    }
    listen(listen_sock, 5);

    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    if (-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev)) {
        perror("epoll_ctl add listen_sock failure.");
        exit(EXIT_FAILURE);
    }

    int nfds = 0;
    while (1) {
        nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (-1 == nfds) {
            perror("epoll_wait failure.");
            exit(EXIT_FAILURE);
        }

        for ( int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == listen_sock) {
                int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);
                if (-1 == conn_sock) {
                    perror("accept failure.");
                    exit(EXIT_FAILURE);
                }
                printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
                set_keepalive(conn_sock, 120, 20, 3);
                setnonblocking(conn_sock);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = conn_sock;
                if (-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev)) {
                    perror("epoll_ctl add conn_sock failure.");
                    exit(EXIT_FAILURE);
                }
            } else {
                events_handle(epfd, events[n]);
            }
        }
    }

    close(listen_sock);
    close(epfd);
}

int setnonblocking(int sockfd){
    if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) {
        return -1;
    }
    return 0;
}

struct echo_data {
    char* data;
    int fd;
};

int events_handle(int epfd, struct epoll_event ev) {
    printf("events_handle, ev.events = %d\n", ev.events);
    int fd = ev.data.fd;
    if (ev.events & EPOLLIN) {
        char* buf = (char*)malloc(MAX_BUF);
        bzero(buf, MAX_BUF);
        int count = 0;
        int n = 0;
        while (1) {
            n = read(fd, (buf + count), 1024);
            printf("step in edge_trigger, read bytes:%d\n", n);
            if (n > 0) {
                count += n;
            } else if (0 == n) {
                break;
            } else if (n < 0 && EAGAIN == errno) {
                perror("errno == EAGAIN, break.");
                break;
            } else {
                perror("read failure.");
                if (ETIMEDOUT == errno) {
                    if (-1 == epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev)) {
                    perror("epoll_ctl del fd failure.");
                    exit(EXIT_FAILURE);
                    }
                    close(fd);
                    return 0;
                }
                exit(EXIT_FAILURE);
            }
        }

        if (0 == count) {
            if (-1 == epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev)) {
                perror("epoll_ctl del fd failure.");
                exit(EXIT_FAILURE);
            }
            close(fd);

            return 0;
        }

        printf("recv from client: %s\n", buf);
        struct echo_data* ed = (struct echo_data*)malloc(sizeof(struct echo_data));
        ed->data = buf;
        ed->fd = fd;
        ev.data.ptr = ed;
        ev.events = EPOLLOUT | EPOLLET;
        if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev)) {
            perror("epoll_ctl modify fd failure.");
            exit(EXIT_FAILURE);
        }

        return 0;
    } else if (ev.events & EPOLLOUT) {
        struct echo_data* data = (struct echo_data*)ev.data.ptr;
        printf("write data to client: %s", data->data);
        int ret = 0;
        int send_pos = 0;
        const int total = strlen(data->data);
        char* send_buf = data->data;
        while(1) {
            ret = write(data->fd, (send_buf + send_pos), total - send_pos);
            if (ret < 0) {
                if (EAGAIN == errno) {
                    sched_yield();
                    continue;
                }
                perror("write failure.");
                exit(EXIT_FAILURE);
            }
            send_pos += ret;
            if (total == send_pos) {
                break;
            }
        }

        ev.data.fd = data->fd;
        ev.events = EPOLLIN | EPOLLET;
        if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, data->fd, &ev)) {
            perror("epoll_ctl modify fd failure.");
            exit(EXIT_FAILURE);
        }

        free(data->data);
        free(data);
    }

    return 0;
}
複製代碼
二、客戶端代碼
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include <time.h>
#include <sys/time.h>
#include<stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define SERVER_PORT 33333
#define MAXLEN 65535

void client_handle(int sock);

int main(int argc, char* argv[]) {
    for (int i = 1; i < argc; ++i) {
        printf("input args %d: %s\n", i, argv[i]);
    }
    struct sockaddr_in seraddr;
    int server_port = SERVER_PORT;
    if (2 == argc) {
        server_port = atoi(argv[1]);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr);
    seraddr.sin_port = htons(server_port);

    connect(sock, (struct sockaddr *)&seraddr, sizeof(seraddr));
    client_handle(sock);

    return 0;
}

void client_handle(int sock) {
    char sendbuf[MAXLEN], recvbuf[MAXLEN];
    bzero(sendbuf, MAXLEN);
    bzero(recvbuf, MAXLEN);
    int n = 0;

    while (1) {
        if (NULL == fgets(sendbuf, MAXLEN, stdin)) {
            break;
        }
        // 按`#`號退出
        if ('#' == sendbuf[0]) {
            break;
        }
        struct timeval start, end;
        gettimeofday(&start, NULL);
        write(sock, sendbuf, strlen(sendbuf));
        n = read(sock, recvbuf, MAXLEN);
        if (0 == n) {
            break;
        }
        write(STDOUT_FILENO, recvbuf, n);
        gettimeofday(&end, NULL);
        printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));
    }

    close(sock);
}
複製代碼

只要鏈接後,不輸入數據,鏈接就沒有數據發送,經過抓包能夠發現每120秒就會有保活探測報文發出:

保活機制
若是一直沒有收到回覆,會中斷鏈接( RST)。
在這裏插入圖片描述

參考文檔:Requirements for Internet Hosts -- Communication Layers

關注微信公衆號,推送計算機網絡、後端開發、Linux、分佈式、區塊鏈、Rust等技術文章

相關文章
相關標籤/搜索