1. 程式人生 > >伺服器開發之大量time_wait 和 close_wait現象

伺服器開發之大量time_wait 和 close_wait現象

一.tcp狀態轉換圖

因為time_wait和close_wait狀態都是在tcp四次揮手狀態下觸發的,所以小夥伴們直接看下圖



狀態變化的解釋過程:

從客戶端來看:

1.客戶端主動斷開連線時,會先發送FIN包,客戶端此時進入FIN_WAIT_1狀態;

2.客戶端收到伺服器的ACK包(對步驟1中FIN包的應答)後,客戶端進入FIN_WAIT_2狀態;

3.客戶端接收到伺服器的FIN包並回復ACK包給服務端,然後客戶端進入TIME_WAIT狀態,此時會等待2個MSL的時間,

確保傳送的ACK包是否達到了對端。

4.客戶端在等待了2個MSL的時間沒有收到伺服器重傳的FIN包,就預設ACK資料包已經抵達了對端。

從服務端來看:

1.伺服器收到客戶端傳送的FIN資料包後,回覆ACK包給客戶端,此時伺服器進入CLOSE_WAIT狀態

2.等待伺服器將剩餘的資料全部發送給客戶端時,然後執行斷開操作,(老夫把該做的事都做了,然後再給這小子傳送FIN包來結束,哈哈,薑還是老的辣!)

伺服器向客戶端傳送出FIN包後,伺服器端進入LAST_ACK狀態,等待最後一個ACK確認包。

3.服務端收到客戶端傳送的ACK包後,從LAST_ACK狀態轉為CLOSED狀態,伺服器正式關閉了

二、close_wait產生原因實驗剖析

CLOSE_WAIT狀態:

被動斷開連線的一方在傳送完ACK分節之後就會進入CLOSE_WAIT狀態.

它需要伺服器在傳送完剩餘資料之後,就呼叫close來關閉連線.此時伺服器從CLOSE_WAIT狀態變為LAST_ACK狀態.

小夥伴我們先來看下示例程式碼

client端程式碼如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define MAXLINE 80
#define SERV_PORT 8000


int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char str[MAXLINE] = "test ";
    int sockfd, n;


while(1)
{
sockfd = socket(AF_INET, SOCK_STREAM, 0);


    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.254.26", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);


connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));


close(sockfd);
sleep(2);
}


    return 0;
}

server端程式碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>


#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */


#include <unistd.h>
#include <arpa/inet.h>


using namespace std;


#define LENGTH 128
#include "netinet/in.h"
#define MAXLINE 80
#define SERV_PORT 8000


int main(int argc,char** argv)
{
struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    //int i, n;
    int  n;


//建立socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);


//設定埠重用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));


    //fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);


    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;


inet_pton(AF_INET,"192.168.254.26",&(servaddr.sin_addr.s_addr));
    //servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);


    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));


    listen(listenfd, 20);


    printf("Accepting connections ...\n");
    while (1)
{
        cliaddr_len = sizeof(cliaddr);
        int connfd = accept(listenfd,
                (struct sockaddr *)&cliaddr, &cliaddr_len);


//while(1)
{
        n = recv(connfd, buf, MAXLINE,0);
        if (n == 0)
{
//對端主動關閉
            printf("the other side has been closed.\n");
            //break;
        }
        printf("received from %s at PORT %d len = %d\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
               ntohs(cliaddr.sin_port),n);
}
        //測試:模擬CLOSE_WAIT狀態時,將close(connfd);這句程式碼註釋

        close(connfd);
    }


 return 0;
}

測試程式碼中,當recv的返回值為0時(對端主動關閉連線),會跳出while(1)迴圈,此時正確做法是呼叫close關閉tcp連線

此處我們為了測試,故意將close(connfd)這句程式碼註釋掉,註釋後伺服器對於客戶端傳送的FIN包不會做迴應,一直保持close_wait狀態。

執行截圖


伺服器端出現CLOSE_WAIT狀態。

三、time_wait存在是否必要?

程式執行時的截圖如下:

3.1 該狀態用來防止最後一個ACK的丟失.

如果主動關閉連線的一端傳送的最後一個ACK,在網路中延遲或丟失,被動關閉那麼伺服器將會重複傳送FIN資料包,如果客戶端不保留TIME_WAIT狀態的話,客戶端在傳送完ack包後會進入closed狀態,此時的狀態再收到被動關閉連線一方的fin包,主動關閉方將傳送一個RST分節,但是伺服器將該分節解釋為一個錯誤.

3.2 防止上一次連線中的分段延遲到達後影響新連線。

TCP連線由五元組(協議,源IP,源埠,目的IP,目的埠)唯一標識。假設沒有TIME_WAIT狀態,一個連線關閉後,可能使用相同的五元組的新連線被建立,這時若前一個原連線上的TCP分段因為網路延時剛剛到達,且它的序列號剛好在新連線的接收視窗,則會令新連線接收的資料混亂。儘管每次建立連線使用的序列號都是隨機產生的,但是序列號的長度只有32位,在高速網路上可能很快出現序列號迴圈。TIME_WAIT狀態持續2MSL後,原連線的資料包都已經在網路上消失,不會再幹擾新連線。

如果伺服器或客戶端存在大量的TIME_WAIT狀態,這是一種可能是正常的情況,主動斷開連線的一方會進入TIME_WAIT狀態.

主動連線端會佔用本地埠,大量的TIME_WAIT狀態的socket,會佔用大量的本地埠,當本地埠不足時,tcp連線不能建立成功。可以通過以下兩種方式來解決上述問題

1.調整引數net.ipv4.ip_local_port_range來增加本地埠的選擇範圍,但這樣效果有限。

2.啟用net.ipv4.tcp_tw_reuse引數來重用TIME_WAIT狀態的socket。

3.linux api設定socket套接字的”埠重用“屬性

通常情況下,客戶端的埠資源比較充足,應該讓客戶端主動斷開連線,但在某些場景下,如tcp連線長時間沒有IO操作,應該將此空閒tcp連線踢除,否則空閒tcp會佔有系統各個資源卻不幹事,太浪費了

參考資料

1.TCP網路關閉的狀態變換時序圖

https://coolshell.cn/articles/1484.html

2.tcp狀態實驗分析

http://www.just4coding.com/blog/2017/11/09/timewait/