天天看點

基礎回顧----listen函數的backlog參數的本質

在初學網絡程式設計這塊時,對listen函數的第二個參數(backlog)簡單了解為伺服器允許同時連接配接的用戶端個數,但後面總是感覺對這個參數的意義很模糊,在查閱資料後才發現這個參數遠遠沒有表面上那麼簡單,是以寫篇部落格總結、記錄、鞏固一下。

先提出兩個概念半連接配接狀态隊列(syns queue)和完全連接配接狀态隊列(accept queue),通過下圖可以看到這兩個隊列的作用:

基礎回顧----listen函數的backlog參數的本質

圖檔來源:關于TCP 半連接配接隊列和全連接配接隊列

可以看到linux會将處于SYN_RCVD狀态的socket放在半連接配接隊列中,将已經建議好連接配接處于ESTABLISHED狀态的socket從半連接配接隊列拿出放在全連接配接隊列中,accept系統調用傳回的socket是從全連接配接隊列中擷取的。

現在我們可以讨論backlog的作用了,在核心版本2.2之前,backlog參數是指半連接配接隊列和全連接配接隊列的大小。在2.2版本之後,backlog參數隻表示全連接配接隊列的大小(accept隊列有一個上限值,由系統參數somaxconn指定,也就是全連接配接隊列的大小等于min(somaxconn, backlog)),半連接配接隊列的大小為max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。backlog的經典值為5。

實驗:

上述理論我們可以通過代碼來驗證:

#include<stdio.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<arpa/inet.h>
int main(int argc,const char *argv[])
{
	if(argc < 3)
	{
		printf("usage:%s ip_address port_number\n",argv[0]);
		exit(1);
	}

	const char* ip = argv[1];
	int port = atoi(argv[2]);

	struct sockaddr_in listenaddr,connaddr;
	socklen_t len = sizeof(connaddr);
	listenaddr.sin_family = AF_INET;
	listenaddr.sin_port = htons(port);
	inet_pton(AF_INET,ip,&listenaddr.sin_addr);

	int listenfd = socket(AF_INET,SOCK_STREAM,0);
	
	bind(listenfd,(struct sockaddr*)&listenaddr,sizeof(listenaddr));

	listen(listenfd,3);

	sleep(30);//睡眠20秒,以便積累多個用戶端連接配接
	while(true)
	{
		//接受用戶端連接配接,從全連接配接隊列中取出一個socket
		int connfd = accept(listenfd,(struct sockaddr*)&connaddr,&len);
		printf("hava a accept\n");
		sleep(10);//每10秒從全連接配接隊列中取出一個socket
	}
	return 0;
}
           

上述代碼在伺服器調用listen後,先不accept,這樣多個用戶端連接配接過來,我們便可以通過netstat指令清楚地看到可以有多少個ESTABLISHED狀态的連接配接即全連接配接隊列的長度。

當有七個連接配接請求到達時會出現下面這種情況。

基礎回顧----listen函數的backlog參數的本質

我們可以看到對于服務端來說總共有4個處于ESTABLISHED狀态的連接配接,說明全連接配接隊列的長度是我們設定的backlog參數+1(不同的系統會有所不同,不過全連接配接隊列的大小通常會比backlog值略大),但是server端并沒有其他3個連接配接應有的SYN_RCVD狀态,client端的狀态停留在SYN_SENT狀态,也就是說server并沒有對這3個client發送SYN+ACK,這很怪異,因為server端理應發送SYN+ACK,并将這3個client的連接配接放到半連接配接隊列中。通過tcpdump抓包可以看到,這3個client一直向server重發SYN請求,在多次重發SYN得不到回應時,會傳回Connection timed out。

基礎回顧----listen函數的backlog參數的本質
基礎回顧----listen函數的backlog參數的本質

上述代碼,每隔10s就會調用一次accept函數,也就是每隔10s就會從全連接配接隊列中取出一個socket,這時全連接配接隊列就會空出一個位置,這時server才會對到來的SYN回應建立連接配接,效果如下圖:

基礎回顧----listen函數的backlog參數的本質
基礎回顧----listen函數的backlog參數的本質

貌似在全連接配接隊列滿後,server不會對再來的SYN做出響應,當全連接配接隊列有空位時,server才會對到來的SYN響應建立連接配接,整個過程沒看到server的SYN_RCVD狀态,沒有看到半連接配接隊列的身影。這個結果并不符合理論。

對于上述問題,我檢視了很多文檔,也沒有找到原因,如果有大佬知道,可以寫在評論區,謝謝!

若全連接配接隊列已經滿了,此時服務端收到了一個用戶端的ACK應答會發生什麼(或者說有連接配接需要從SYN隊列轉移到accept隊列)?

這個時候系統會根據/proc/sys/net/ipv4/tcp_abort_on_overflow核心參數的值來做出應對。

  1. 若tcp_abort_on_overflow值為0,則server會扔掉client發來的ACK應答,當定時器逾時時,服務端會重傳SYN+ACK給client(重新走三次握手的第二步),如果重傳次數超過synack重傳的閥值(/proc/sys/net/ipv4/tcp_synack_retries),則會把該連接配接從半連接配接隊列中删除。
  2. 若tcp_abort_on_overflow值為1,server會發送rst給client,并删除掉這個連接配接。

client發送完ACK後就會進入ESTABLISHED狀态,也就是connect函數傳回,但是服務端因為全連接配接隊列滿了,是以對應連接配接實際沒有準備好,這個時候如果client發資料給server,server會怎麼處理呢?

我們看下面這個圖檔:

基礎回顧----listen函數的backlog參數的本質

圖檔來源Linux協定棧accept和syn隊列問題

150166号包是三次握手中的第三步client發送ack給server,然後150167号包中client發送了一個長度為816的包給server,因為在這個時候client認為連接配接建立成功,但是server上這個連接配接實際沒有ready,是以server沒有回複,一段時間後client認為丢包了然後重傳這816個位元組的包,一直到逾時,client主動發fin包斷開該連接配接。

是以通常情況下,應當把 tcp_abort_on_overflow 設定為 0,因為這樣更有利于應對突發流量。因為client每次發送資料都會帶一個對接受到server最近一次資料包的ACK應答,在這裡就是server發送的SYN+ACK,是以若在client重傳期間,server的全連接配接隊列出現空位,則這條連接配接就會建立成功

參考:

阿裡中間件團隊部落格:關于TCP 半連接配接隊列和全連接配接隊列

tcp的半連接配接與完全連接配接隊列

https://www.cnblogs.com/xiaolincoding/p/13067971.html

Linux協定棧accept和syn隊列問題

深入探索 Linux listen() 函數 backlog 的含義

http://blog.csdn.net/yangbodong22011/article/details/60468820

繼續閱讀