伺服器開發之大量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/