TCP重置報文段及RST常見場景分析


RST表示連接重置,用於關閉那些已經沒有必要繼續存在的連接。一般情況下表示異常關閉連接,區別與四次分手正常關閉連接。

產生RST的三個條件是:

  1. 目的地為某端口的SYN到達,然而在該端口上並沒有正在監聽的服務器;
  2. TCP想取消一個已有連接;
  3. TCP接收到一個根本不存在的連接上的分節。

下面的幾種場景,都會產生RST,以此來說明重置報文段的用途。

一、針對不存在端口的連接請求

客戶端向服務端某端口發起連接請求SYN,但是目的服務端主機不存在該端口,此時向客戶端回應RST,中斷連接請求。

下面通過程序和抓包進行分析。程序源碼如下:

use std::io::prelude::*;
use std::net::TcpStream;
use std::thread;

fn main() {
    let mut stream = TcpStream::connect("192.168.2.229:33333").unwrap();
    let n = stream.write(&[1,2,3,4,5,6,7,8,9]).unwrap();
    println!("send {} bytes to remote node, waiting for end.", n);

    loop{
        thread::sleep_ms(1000);
    }
}

上面程序向目的主機192.168.2.229發起TCP連接,而目的主機並沒有啟動端口為33333的監聽服務。所以當本地主機向目的主機發起TCP連接后,會收到來自目的主機的RST,並斷開連接。(當然也不是所有的都會回復RST,有的主機可能不會進行回復)。抓包如下:

本地主機向目的主機發送TCP連接SYN
在這里插入圖片描述

目的主機向本地主機回復ACK、RST
在這里插入圖片描述

二、終止一條連接

終止一條連接的正常方法是由通信一方發送一個FIN。這種方法也被稱為有序釋放。因為FIN是在之前所有排隊數據都已發送后才被發送出去,通常不會出現丟失數據的情況。然而在任何時刻,我們都可以通過發送一個重置報文段RST替代FIN來終止一條連接。這種方式也被稱為終止釋放。

終止一條連接可以為應用程序提供兩大特性:

  • 任何排隊的數據都將被拋棄,一個重置報文段會被立即發送出去;
  • 重置報文段的接收方會說明通信的另一端采用了終止的方式而不是一次正常關閉。API必須提供一種實現上述終止行為的方式來取代正 常的關閉操作。

套接字API可通過套接字選項SO_LINGER的數值設置為0來實現上述功能。

/* Structure used to manipulate the SO_LINGER option.  */
struct linger
  {
    int l_onoff;		/* Nonzero to linger on close.  */
    int l_linger;		/* Time to linger.  */
  };

SO_LINGER的不同值的含義如下:

  1. l_onoff0l_linger的值被忽略,內核缺省情況,close()調用會立即返回給調用者,TCP模塊負責嘗試發送殘留的緩存區數據。
  2. l_onoff為非零值,l_linger0,則連接立即終止,TCP將丟棄殘留在發送緩沖區中的數據並發送RST給對方,而不是發送FIN,這樣避免了TIME_WAIT狀態,對方read()時將收到Connection reset by peer的錯誤。
  3. l_onoff為非零值,l_linger大於零:如果l_linger時間范圍,TCP模塊成功發送完殘留的緩沖區數據,則正常關閉,如果超時,則向對方發送RST,丟棄殘留在發送緩沖區的數據。

客戶端代碼間附錄代碼1,服務端代碼如下:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>

#define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024

int set_linger(int sock, int l_onoff, int l_linger);
int handle_conn(struct pollfd *nfds, char* buf);
void run();

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

    return 0;
}

void run() {
    // bind socket
    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);

    int ret, i;
    struct pollfd nfds[OPEN_MAX];
    for (i=0;i<OPEN_MAX;++i){
        nfds[i].fd = -1;
    }

    nfds[0].fd = listen_sock;
    nfds[0].events = POLLIN;

    char* buf = (char*)malloc(MAX_BUF);   
    while (1) {
        ret = poll(nfds, OPEN_MAX, NULL);
        if (-1 == ret) {
            perror("poll failure.");
            exit(EXIT_FAILURE);
        }

        /* An event on one of the fds has occurred. */
        if (nfds[0].revents & POLLIN) {
            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_linger(conn_sock, 1, 0);    //設置SO_LINGER option值為0
            for (int k=0;k<OPEN_MAX;++k){
                if (nfds[k].fd < 0){
                    nfds[k].fd = conn_sock;
                    nfds[k].events = POLLIN;
                    break;
                }
                if (k == OPEN_MAX-1){
                    perror("too many clients, nfds size is not enough.");
                    exit(EXIT_FAILURE);
                }
            }
        }

        handle_conn(nfds, buf);
    }

    close(listen_sock);
}

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
            n = read(nfds[i].fd, buf, MAX_BUF);
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLIN;

                close(nfds[i].fd);  //接收數據后就主動關閉連接,用於RST測試          
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    return 0;
}

int set_linger(int sock, int l_onoff, int l_linger) {
    struct linger so_linger;
    so_linger.l_onoff = l_onoff;
    so_linger.l_linger = l_linger;
    int r = setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));

    return r;
}

抓包結果如下:
在這里插入圖片描述
先3次握手,后客戶端向服務度發送了5個字節數據,服務端在接收完5字節數據向客戶端ACK后,表示想中斷連接,此時因設置了SO_LINGER選項值為0close()時,直接向對方發送RST而不是正常的發送FIN,連接立即終止,並且不會有TIME_WAIT狀態,TCP將丟棄殘留在發送緩沖區中的數據,對方read()時將收到Connection reset by peer的錯誤。

三、半開連接

如果在未告知另一端的情況下通信的一端關閉或終止連接,那么就認為該條TCP連接處於半開狀態。

舉個例子,服務器主機被切斷電源后重啟(切斷電源前可將網線斷開,重啟后再接上),此時的客戶端是一個半開的連接。當客戶端再次向服務端發送數據時,服務端對此連接一無所知,會回復一個重置報文段RST后,中斷連接。

再或者如果程序開啟了TCP保活機制,則當監測到對方主機不可達時,發送RST中斷連接。詳細可參考我的另一篇博文TCP保活機制

TCP連接如果長時間沒有數據收發,會使TCP發送保活探測報文,以維持連接或者探測連接是否存在。
在這里插入圖片描述
可以看到如果認為連接不存在了,就會發送RST中斷連接。

四、提前關閉連接

TCP應用程序接收數據是從操作系統中接收的TCP數據,如果數據到達了操作系統但是我應用數據不想繼續接收數據了,此時RST中斷連接。

服務端代碼:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>

#define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024

#define RST_TEST 1

int handle_conn(struct pollfd *nfds, char* buf);
void run();

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

    return 0;
}

void run() {
    // bind socket
    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);

    int ret, i;
    struct pollfd nfds[OPEN_MAX];
    for (i=0;i<OPEN_MAX;++i){
        nfds[i].fd = -1;
    }

    nfds[0].fd = listen_sock;
    nfds[0].events = POLLIN;

    char* buf = (char*)malloc(MAX_BUF);   
    while (1) {
        ret = poll(nfds, OPEN_MAX, NULL);
        if (-1 == ret) {
            perror("poll failure.");
            exit(EXIT_FAILURE);
        }

        /* An event on one of the fds has occurred. */
        if (nfds[0].revents & POLLIN) {
            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));

            for (int k=0;k<OPEN_MAX;++k){
                if (nfds[k].fd < 0){
                    nfds[k].fd = conn_sock;
                    nfds[k].events = POLLIN;
                    break;
                }
                if (k == OPEN_MAX-1){
                    perror("too many clients, nfds size is not enough.");
                    exit(EXIT_FAILURE);
                }
            }
        }

        handle_conn(nfds, buf);
    }

    close(listen_sock);
}

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
#if RST_TEST == 0
            n = read(nfds[i].fd, buf, MAX_BUF);
#else
            n = read(nfds[i].fd, buf, 5);      //只接收部分數據就主動關閉連接,用於RST測試         
#endif
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLOUT;
#if RST_TEST != 0  
                close(nfds[i].fd);  //只接收部分數據就主動關閉連接,用於RST測試
#endif            
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    return 0;
}

客戶端發起連接后發送超過5字節的數據后,因為服務端只接收5個字節數據后不再接收數據(此時服務端操作系統已經收到10個字節數據,但上層read系統調用只接收5個字節),服務端向客戶端發送RST中斷連接。抓包結果如下:
在這里插入圖片描述
先3次握手,握手后客戶端發送了10字節長度的數據,服務端在回應客戶端ACK接收到數據后,發送RST中斷連接。

五、在一個已關閉的TCP連接上收到數據

如果一個已關閉的TCP連接又收到數據,顯然是異常的,此時應RST中斷連接。

服務端其他代碼與上個代碼相同,下面函數替換一下

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
            n = read(nfds[i].fd, buf, MAX_BUF);
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLOUT;

                close(nfds[i].fd);  //接收數據后就主動關閉連接,用於RST測試          
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    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));
        sleep(2);
        write(sock, sendbuf, strlen(sendbuf));		//這里是測試RST用的代碼
        sleep(60);
        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);
}

抓包如下:
在這里插入圖片描述
先3次握手;后客戶端向服務端發送了5字節數據,服務端接收到5字節數據回復ACK;之后向客戶端發送FIN,關閉連接,但此時客戶端還有數據要發送,沒有向服務端發起FIN,此時只進行了2次揮手;之后客戶端又向服務端發送了5個字節數據,但此時服務端該連接已經調用close()關閉,此時再次收到該連接的數據屬於異常,回復RST中斷連接。

六、附錄

測試用的客戶端代碼

#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);

    if (-1 == connect(sock, (struct sockaddr *)&seraddr, sizeof(seraddr))) {
        perror("connect failure");
        exit(EXIT_FAILURE);
    }
    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 (n < 0) {
            perror("read failure.");
            exit(EXIT_FAILURE);
        }
        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);
}

歡迎關注微信公眾號,推送計算機網絡、后端開發、區塊鏈、分布式、Rust、Linux等技術文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM