- 由Linux中listen()函數談開去
- 一、簡介
- 1.前言
- 2.問題引入
- 二、原理介紹
- 1. Tcp三次握手
- 2. 關于backlog參數的了解
- 三、實驗與分析
- 1. 實驗環境
- 2. 例程介紹
- 3. 分步實驗
- 1. Server阻塞于socket()建立後
- 2. Server阻塞于bind()建立後
- 3. Server阻塞于listen()後
- 4. Server阻塞于accept()一次後
- 5. Server阻塞于accept()多次
- 4. 實驗結果分析
- 四、參考與連結
- 五、文檔資訊
由Linux中listen()函數談開去
一、簡介
1. 前言
本篇博文主要談一談Linux系統中,在使用socket套接字建立Tcp連接配接時,關于listen()函數的backlog參數的了解,同時,借由該問題,并結合具體的實踐,嘗試厘清Tcp連接配接的建立過程。
本篇部落格編寫主要參考借鑒了以下兩篇博文,并在其基礎上,加入了個人的一些思考與探索。
- 深入探索 Linux listen() 函數 backlog 的含義
- How TCP backlog works in Linux
下述内容的讨論,建立在對Tcp/Ip、socket有一定了解的基礎上,相關的知識點,可以參考以下博文:
1.揭開Socket程式設計的面紗
2.Linux Socket程式設計(不限Linux)
2. 問題引入
當我們嘗試在
Linux
上通過
Socket
建立一個伺服器,并接收用戶端的連接配接請求時,伺服器端程式通常需要執行以下流程:
- 使用建立 socket() 建立一個監聽描述符
ListenFd
- 使用 bind() 為
綁定一個本地的位址,以便于其它的ListenFd
(套接字),能夠與其建立連接配接socket
- 使用 listen() 将
設定為被動模式——表明自己樂意接受連接配接請求,并設定【連接配接建立隊列的限制】。ListenFd
- 調用 accept() 以接收具體的連接配接。
- 資料互動。。。
伺服器與用戶端通過調用相關函數建立連接配接的流程如下所示:
圖1:Tcp連接配接實作
注:在上圖中,并未展示斷開連接配接的過程
在平時的工程實踐中,自己也都是照貓畫虎, 知其然而不知其是以然。由于最近嘗試寫一個使用TCP建構的用戶端/伺服器公共架構的架構,在寫作過程中,發現自己對于很多基礎的操作都不明其意,基于此,才有了這邊博文。
文章首先介紹socket的一些基本概念,接下來通過一個具體的執行個體,并結合相應的抓包分析,印證明際與理論是否相符。
二、原理介紹
1. Tcp三次握手
在學習計算機網絡的相關知識時,想必大家對于Tcp三次握手的連接配接過程并不陌生:
圖2: Tcp三次握手
正如上圖所示:
- 用戶端請求建立連接配接,然後進入 SYN-SENT 狀态
- 處于LISTEN狀态的伺服器端在收到連接配接請求後,給用戶端回複應答,同時進入SYN-RCVD狀态
- 用戶端在收到應答後,進入ESTABLISHED狀态,同時再次發消息告知伺服器端
- 伺服器端在收到消息後,亦即進入ESTABLISHED狀态
在完成上述互動過程後,Tcp 連接配接建立,兩者即可進行後續的資料互動。
以上為建立連接配接的邏輯過程,看起來清晰易懂,似乎沒有什麼難以了解的地方,但是,倘若需要我們将三次握手過程與圖一的實作聯系起來呢?
根據自己以往的經驗,具體的實作,與Tcp三次握手的原理圖,應該是如下的對應關系:
圖3: Tcp三次握手實作的錯誤了解
對于用戶端而言:在調用
connect()
後進入SYN-SENT狀态,同時阻塞,以等待伺服器應答;在接收到應答後,則進入ESTABLISHED狀态。
注:調用
socket()
到
connect()
之間的這段狀态,我們暫且将其稱為
INIT
狀态
對于伺服器而言:調用
listen()
後進入LISTEN狀态,在調用
accept()
接收用戶端的連接配接請求後,會短暫的進入SYN-RCVD狀态,随後進入ESTABLISHED狀态。
注:調用
socket()
到
listen()
之間的這段狀态,我們暫且将其稱為
INIT
狀态
圖中的狀态劃分,粗看的話,似乎也說的過去,完整展現了三次握手的過程,但事實是否真的如此呢?在展開具體的說明之前,首先需要對
listen()
函數進行介紹。
2. 關于backlog參數的了解
int listen(int sockfd, int backlog);
listen()
将
sockfd
對應的描述符标記為被動模式,所謂被動模式的意思也就是說,可以用
accept()
來接受收到的連接配接請求。其中,
backlog
參數指定列了挂起連接配接隊列可以增長的最大長度。
正如上文所說的,由于Tcp通過三次握手建立連接配接,一個連接配接在成功建立并進入ESTABLISHED狀态前,會經曆一段短暫的SYN-RCVD狀态,之後就可以被
accept()
系統調用處理,并傳回給應用。這意味着Tcp/Ip協定棧擁有兩種方式來實作處于LISTEN狀态的socket套接字的
backlog
隊列:
- 方式1
The implementation uses a single queue, the size of which is determined by the backlog argument of the listen syscall. When a SYN packet is received, it sends back a SYN/ACK packet and adds the connection to the queue. When the corresponding ACK is received, the connection changes its state to ESTABLISHED and becomes eligible for handover to the application. This means that the queue can contain connections in two different state: SYN RECEIVED and ESTABLISHED. Only connections in the latter state can be returned to the application by the accept syscall.
——《How TCP backlog works in Linux》
實作使用單個隊列,其尺寸由
listen()
的
backlog
參數決定。處于被動模式的
socket
在收到一個
SYN
封包後,它傳回一個
SYN/ACK
封包,并将該連結加入隊列。在收到相應的
ACK
應答封包後,此連接配接将狀态改為ESTABLISHED,此後,才有資格被移交給應用程式(注:才可以用于後續的互動)。以上也就意味着,該隊列可能同時包含處于SYN-RCVD以及ESTABLISHED兩種狀态的連接配接。隻有處于後一種狀态的連接配接,方可以被
accept()
處理,并傳回給使用者。
- 方式2
The implementation uses two queues, a SYN queue (or incomplete connection queue) and an accept queue (or complete connection queue). Connections in state SYN RECEIVED are added to the SYN queue and later moved to the accept queue when their state changes to ESTABLISHED, i.e. when the ACK packet in the 3-way handshake is received. As the name implies, the accept call is then implemented simply to consume connections from the accept queue. In this case, the backlog argument of the listen syscall determines the size of the accept queue.
——《How TCP backlog works in Linux》
此種實作使用兩個隊列:
SYN
隊列(連接配接未完成隊列),以及
accept
隊列(已完成連接配接的隊列)。SYN-RCVD狀态的連接配接被加入
SYN
隊列,當連接配接狀态變為ESTABLISHED後,則被移入到
accept
隊列(例如,在接收到三次握手中的
ACK
封包時)。顧名思義,
accept()
調用,其實作就是為了接受來自
accept
隊列的連接配接(注:所謂接受連接配接,意即該連接配接已經建立,可以通過調用
accept()
被傳回給使用者,并用于後續互動。随後,該連接配接也就被從
accept
隊列中移除)。對于此種方式,
listen()
調用的
backlog
參數,決定了
accept
隊列的大小。
Historically, BSD derived TCP implementations use the first approach. That choice implies that when the maximum backlog is reached, the system will no longer send back SYN/ACK packets in response to SYN packets. Usually the TCP implementation will simply drop the SYN packet (instead of responding with a RST packet) so that the client will retry.
The BSD implementation does use two separate queues, but they behave as a single queue with a fixed maximum size determined by (but not necessary exactly equal to) the backlog argument:
The queue limit applies to the sum of […] the number of entries on the incomplete connection queue […] and […] the number of entries on the completed connection queue […].——《How TCP backlog works in Linux》
從曆史上看,派生自BSD的Tcp實作使用第一種方案。該選擇意味着,當隊列達到
backlog
所定義的最大值時,系統不會發送
SYN/ACK
封包去回應
SYN
封包。通常,Tcp實作隻會丢棄
SYN
封包(而不是應答
RST
封包),以便用戶端可以重試。
BSD實作确實是用兩個隊列,然而它們的行為就如同是由
backlog
參數決定大小的單個隊列。
隊列限制适用于未完成連接配接隊列的條目數與已完成連接配接條目數的總和。
On Linux, things are different, as mentioned in the man page of the listen syscall:The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog.——《How TCP backlog works in Linux》
在Linux上,情況則有所不同,以下是
man page
中關于
listen
系統調用的内容:
Tcp socket的
backlog
參數的行為在Linux2.2中有所改變。它現在指定了等待被接受的連接配接已完成的套接字的隊列(注:即
accept
隊列)的長度,而不是未完成的連接配接請求的個數(注:即
SYN
隊列中所包含的連接配接個數)。 未完成連接配接隊列的最大長度可以被設定為
/proc/sys/net/ipv4/tcp_max_syn_backlog
。
為了驗證明際是否與上述理論是一緻的,我們将在下文中通過具體的例程,并結合具體的抓包資料,對Tcp的連接配接過程進行分析。
三、實驗與分析
1. 實驗環境
處理器名稱:Intel® Core™ i3-4170 CPU @ 3.70GHz
系統版本:CentOS release 6.5 (Final)
編譯器版本:gcc version 4.4.7 20120313
2. 例程介紹
實驗使用的例程包括
Client
與
Server
兩部分,具體位址詳見。
- 用戶端代碼片段
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<strings.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include <stdarg.h>
#include <sys/errno.h>
#include <iostream>
#include <sstream>
#define PORT 17777
#define THREAD_NUM 6 //定義建立的線程數量
#define MAXLINE 1024
struct sockaddr_in stServAddr;
using namespace std;
/**
*@brief 格式化錯誤資訊
*
*
*@param int errnoflag
*@param int error
*@param const char *fmt
*@param va_list ap
*
*@return
*
*
*@author Litost_Cheng
*@date 2019年1月21日
*@note 新生成函數
*/
static void ErrDoit(int errnoflag, int error, const char *fmt, va_list ap)
{
char buf[MAXLINE];
vsnprintf(buf, MAXLINE-1, fmt, ap);
if (errnoflag)
snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": errno[%d] %s",
error, strerror(error));
strcat(buf, "\n");
fflush(stdout); /* in case stdout and stderr are the same */
fputs(buf, stderr);
fflush(NULL); /* flushes all stdio output streams */
}
/**
*@brief 判斷條件,列印errno并退出
*
*
*@param bool bCondition
*@param const char *fmt
*@param ...
*
*@return
*
*
*@author Litost_Cheng
*@date 2019年5月11日
*@note 新生成函數
*
*/
bool CondJudgeExit(bool bCondition, const char *fmt, ...)
{
if (!bCondition)
{
va_list ap;
va_start(ap, fmt);
ErrDoit(1, errno, fmt, ap);
va_end(ap);
exit(1);
}
return bCondition;
}
void *func(void *)
{
int nConnFd;
nConnFd = socket(AF_INET,SOCK_STREAM,0);
printf("nConnFd : %d\n",nConnFd);
///在沒個子線程中,都會嘗試與伺服器連接配接連接配接,并傳回結果
if ((connect(nConnFd,(struct sockaddr *)&stServAddr,sizeof(struct sockaddr_in)) == -1))
{
printf("[nConnFd] Connect failed: [%s]\n", strerror(errno));
return (void *)-1;
}
else
{
printf("Connect succeed!\n");
stringstream strStream;
strStream << "[" << nConnFd << "]" << "Send Message";
printf("strStream is [%s]\n", strStream.str().c_str());
if (-1 == write(nConnFd, strStream.str().c_str(), strStream.str().size()))
{
printf("[nConnFd] Connect failed: [%s]\n", strerror(errno));
return (void *)-1;
}
else
{
printf("[nConnFd] Send succeed!\n", nConnFd);
}
}
while(1) {}
}
int main(int argc,char *argv[])
{
memset(&stServAddr,0,sizeof(struct sockaddr_in));
stServAddr.sin_family = AF_INET;
stServAddr.sin_port = htons(PORT);
inet_aton("127.0.0.1",(struct in_addr *)&stServAddr.sin_addr);
//建立線程并且等待線程完成
pthread_t nPid[THREAD_NUM];
//system("netstat -atn | grep '17777'");
//printf("netstat -atn\n");
for(int i = 0 ; i < THREAD_NUM; ++i)
{
pthread_create(&nPid[i],NULL,&func,NULL);
}
sleep(3);
//system("netstat -atn | grep '17777'");
//printf("netstat -atn\n");
for(int i = 0 ; i < THREAD_NUM; ++i)
{
pthread_join(nPid[i], NULL);
}
return 0;
}
以上,為了搞清
backlog
參數的真實含義,用戶端程序會建立
THREAD_NUM
個線程,來向伺服器端發起連接配接請求,并傳回相應的結果。
- 伺服器端代碼片段
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <string>
#include <stdarg.h>
#include <sys/errno.h>
#include <iostream>
#define PORT 17777 //端口号
#define BACKLOG 2 //BACKLOG大小
#define MAXLINE 1024
using namespace std;
/**
*@brief 格式化錯誤資訊
*
*
*@param int errnoflag
*@param int error
*@param const char *fmt
*@param va_list ap
*
*@return
*
*
*@author Litost_Cheng
*@date 2019年1月21日
*@note 新生成函數
*/
static void ErrDoit(int errnoflag, int error, const char *fmt, va_list ap)
{
char buf[MAXLINE];
vsnprintf(buf, MAXLINE-1, fmt, ap);
if (errnoflag)
snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": errno[%d] %s",
error, strerror(error));
strcat(buf, "\n");
fflush(stdout); /* in case stdout and stderr are the same */
fputs(buf, stderr);
fflush(NULL); /* flushes all stdio output streams */
}
/**
*@brief 判斷條件,列印errno并退出
*
*
*@param bool bCondition
*@param const char *fmt
*@param ...
*
*@return
*
*
*@author Litost_Cheng
*@date 2019年5月11日
*@note 新生成函數
*
*/
bool CondJudgeExit(bool bCondition, const char *fmt, ...)
{
if (!bCondition)
{
va_list ap;
va_start(ap, fmt);
ErrDoit(1, errno, fmt, ap);
va_end(ap);
exit(1);
}
return bCondition;
}
/**
*@brief 展示連接配接資訊
*
*
*@param bool bCondition
*@param const char *fmt
*@param ...
*
*@return void
*
*
*@author Litost_Cheng
*@date 2019年5月11日
*@note 新生成函數
*
*/
void Display()
{
system("netstat -atn | grep '17777' | sort -n -t : -k 2");
printf("netstat -atn | grep '17777' | sort -n -t : -k 2\n");
//system("lsof -nP -iTCP | grep '17777'");
//printf("lsof -nP -iTCP | grep '17777'\n");
}
char *pCmd[5];
int main(int argc,char *argv[])
{
int nConLen;
int nSockFd,nConnFd;
struct sockaddr_in stServAddr,stConnAddr;
int nCmd = 0;
pCmd[0] = "socket";
pCmd[1] = "bind";
pCmd[2] = "listen";
pCmd[3] = "accept_once";
pCmd[4] = "accept_times";
printf("Please input the Cmd: \n");
for(int n=0; n<5; n++)
{
printf("\t[%d]: [%s]\n", n, pCmd[n]);
}
std::cin >> nCmd;
std::string strSysCmd = "tcpdump -i lo -s 0 -w ./Tcpdump_";
strSysCmd += pCmd[nCmd];
strSysCmd += ".cap";
strSysCmd += " &";
system(strSysCmd.c_str());
printf("[%s]\n", strSysCmd.c_str());
do
{
printf("Start:");
Display();
//建立套接字
CondJudgeExit(((nSockFd = socket(AF_INET,SOCK_STREAM,0)) != -1), "Create socket failed!\n");
if (0 == nCmd)
{
break;
}
//為套接字綁定位址,需要注意位元組序
memset(&stServAddr,0,sizeof(struct sockaddr_in));
stServAddr.sin_family = AF_INET;
stServAddr.sin_port = htons(PORT);
stServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
CondJudgeExit((bind(nSockFd,(struct sockaddr *)&stServAddr,sizeof(struct sockaddr_in)) != -1), "bind failed!\n");
if (1 == nCmd)
{
break;
}
//設定為被動模式
CondJudgeExit((listen(nSockFd,BACKLOG) != -1), "listen filed!\n");
if (2 == nCmd)
{
break;
}
//accept once
nConLen = sizeof(struct sockaddr_in);
//sleep(10); //sleep 10s之後接受一個連接配接
//該套接字預設為阻塞模式,是以,倘若沒有接受的一個成功建立的連接配接,則會一直阻塞在這裡
accept(nSockFd,(struct sockaddr *)&stConnAddr,(socklen_t *)&nConLen);
printf("I have accept one Connect: [%s], port[%d] \n", inet_ntoa(stConnAddr.sin_addr), ntohs(stConnAddr.sin_port));
if (3 == nCmd)
{
break;
}
printf("Pending on [%s]\n", pCmd[nCmd]);
while(1)
{
sleep(3); //周期性接受連接配接請求
printf("I will accept one\n");
accept(nSockFd,(struct sockaddr *)&stConnAddr,(socklen_t *)&nConLen);
printf("I have accept one Connect: [%s], port[%d] \n", inet_ntoa(stConnAddr.sin_addr), ntohs(stConnAddr.sin_port));
Display();
}
}
while(0);
while(1)
{
printf("Pending on [%s]\n", pCmd[nCmd]);
Display();
sleep(1);
}
return 0;
}
對于伺服器端程序而言,我們手動将
backlog
設定為
BACKLOG
,以判斷其如何處理過量的連接配接;此外,為了厘清Tcp三次握手與具體實作之間的聯系,該例程會根據使用者輸入的不同的選項,将程式阻塞在不同階段,并結合對應時刻的連接配接狀态以及抓包資料,确定目前連接配接所處的狀态。
在開始具體的實驗前,有以下幾點是需要我們注意的:
- 為了了解
參數的實際含義,實驗過程中,我們要求backlog
程式中的Client
參數應該要大于THREAD_NUM
中的Server
,以認為造成過量的連接配接請求。BACKLOG
- 代碼的編譯使用自動的
檔案模闆MakeFileTemplate,使用者隻需執行makefile
指令,即可生成相應的可執行檔案。make
- 連接配接狀态的擷取使用netstat擷取
- 抓包資料的擷取使用Tcpdump,并配合Wireshark工具對抓包進行分析,關于兩工具的使用,詳見該連結:聊聊 tcpdump 與 Wireshark 抓包分析。
3. 分步實驗
在具體的實驗過程中,我們會将
Server
分别阻塞以下幾個階段,同時會附上相應的程式輸出(
Client
,
Server
),連接配接狀态,以及抓包資料,以友善讀者能夠有一個直覺的認識。
1. Server阻塞于socket()建立後
- 實驗資料
- 用戶端輸出
[[email protected] Client]# ./Client nConnFd : 3 nConnFd : 4 nConnFd : 5 [nConnFd] Connect failed: [Connection refused] [nConnFd] Connect failed: [Connection refused] [nConnFd] Connect failed: [Connection refused] nConnFd : 6 [nConnFd] Connect failed: [Connection refused] nConnFd : 7 [nConnFd] Connect failed: [Connection refused] nConnFd : 8 [nConnFd] Connect failed: [Connection refused] [[email protected] Client]#
- 伺服器輸出
[[email protected] Server]# ./Server Please input the Cmd: [0]: [socket] [1]: [bind] [2]: [listen] [3]: [accept_once] [4]: [accept_times] 0 [tcpdump -i lo -s 0 -w ./Tcpdump_socket.cap &] Start:netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [socket] netstat -atn | grep '17777' | sort -n -t : -k 2 tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes Pending on [socket] netstat -atn | grep '17777' | sort -n -t : -k 2
- 抓包資料
由Linux中listen()函數談開去由Linux中listen()函數談開去
- 用戶端輸出
-
資料分析
從以上實驗資料,我們不難看出,當
嘗試去連接配接阻塞在Client
狀态的socket()
時,連接配接全部失敗,從抓包擷取到的資料來看,針對用戶端的Server
請求,SYN
直接回複了Server
,而從RST
調用Client
accept()
傳回的錯誤結果來看,顯示連接配接被拒絕。
是以在該階段,
應該是進入了短暫的SYN-SENT階段(注:并未從Client
抓取到有關資料,可能是由于連接配接迅速被斷開導緻),随後連接配接即被拒絕,而netstat
則是處于CLOSED狀态。Server
2. Server阻塞于bind()後
- 實驗資料
- 用戶端輸出
[[email protected] Client]# ./Client nConnFd : 3 nConnFd : 4 nConnFd : 5 [nConnFd] Connect failed: [Connection refused] [nConnFd] Connect failed: [Connection refused] [nConnFd] Connect failed: [Connection refused] nConnFd : 6 [nConnFd] Connect failed: [Connection refused] nConnFd : 7 [nConnFd] Connect failed: [Connection refused] nConnFd : 8 [nConnFd] Connect failed: [Connection refused] [[email protected] Client]#
- 伺服器端輸出
[[email protected] Server]# ./Server Please input the Cmd: [0]: [socket] [1]: [bind] [2]: [listen] [3]: [accept_once] [4]: [accept_times] 1 [tcpdump -i lo -s 0 -w ./Tcpdump_bind.cap &] Start:netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [bind] tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [bind] netstat -atn | grep '17777' | sort -n -t : -k 2
- 抓包資料
由Linux中listen()函數談開去由Linux中listen()函數談開去
- 用戶端輸出
-
資料分析
在實際的實驗過程中,對于
而言,其情況與阻塞在Client
socket()
時保持一緻:連接配接全部失敗。
是以在該階段,
應該也是進入了短暫的SYN-SENT階段(注:并未從Client
抓取到有關資料,可能是由于連接配接迅速被斷開導緻),随後連接配接即被拒絕,而netstat
一直處于CLOSED狀态。Server
3. Server阻塞于listen()後
- 實驗資料
- 用戶端輸出
[[email protected] Client]# ./Client nConnFd : 3 nConnFd : 4 nConnFd : 5 Connect succeed! strStream is [[5]Send Message] [nConnFd] Send succeed! Connect succeed! strStream is [[4]Send Message] [nConnFd] Send succeed! Connect succeed! strStream is [[3]Send Message] [nConnFd] Send succeed! nConnFd : 6 Connect succeed! strStream is [[6]Send Message] [nConnFd] Send succeed! nConnFd : 7 Connect succeed! strStream is [[7]Send Message] [nConnFd] Send succeed! nConnFd : 8 Connect succeed! strStream is [[8]Send Message] [nConnFd] Send succeed! ^C [[email protected] Client]#
- 伺服器端輸出
[[email protected] Server]# ./Server Please input the Cmd: [0]: [socket] [1]: [bind] [2]: [listen] [3]: [accept_once] [4]: [accept_times] 2 [tcpdump -i lo -s 0 -w ./Tcpdump_listen.cap &] Start:netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [listen] tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [listen] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [listen] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40355 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40356 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40352 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40353 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40354 ESTABLISHED tcp 0 0 127.0.0.1:40352 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40353 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40354 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40355 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40356 127.0.0.1:17777 ESTABLISHED tcp 0 1 127.0.0.1:40357 127.0.0.1:17777 SYN_SENT netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [listen] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40355 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40356 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40352 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40353 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40354 ESTABLISHED tcp 0 0 127.0.0.1:40352 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40353 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40354 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40355 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40356 127.0.0.1:17777 ESTABLISHED tcp 0 1 127.0.0.1:40357 127.0.0.1:17777 SYN_SENT netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [listen] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40355 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40356 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40357 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40352 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40353 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40354 ESTABLISHED tcp 0 0 127.0.0.1:40352 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40353 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40354 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40355 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40356 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40357 127.0.0.1:17777 ESTABLISHED
- 抓包資料
由Linux中listen()函數談開去由Linux中listen()函數談開去
- 用戶端輸出
-
資料分析
在實際的實驗過程中,對于
而言,從Client
以及connect()
write()
的結果來看,所有的連接配接都已成功建立,且處于ESTABLISHED狀态。
但是,對于
而言,有三個套接字(端口分别為:Server
,40352
,40353
)處于ESTABLISHED狀态,剩餘的三個則仍處于SYN_RECV(但是需要注意的是,我們是将40354
的值設定為2)。同時,我們結合具體的抓包資料進行分析:針對所有連接配接而言,Tcp三次握手過程,都已完成。由此說明,對于處于被動模式的套接字(調用backlog
後),能夠自動處理接收到的連接配接請求,并完成三次握手的互動。同時,會将listen()
數量的連接配接放置于backlog + 1
accept
隊列。
是以在該階段,
都處于ESTABLISHED階段,Client
則有Server
數量的連接配接處于ESTABLISHED,剩餘則是處于SYN_RECV狀态。backlog + 1
4. Server阻塞于accept()一次後
- 實驗資料
- 用戶端輸出
[[email protected] Client]# ./Client nConnFd : 3 nConnFd : 4 Connect succeed! strStream is [[4]Send Message] [nConnFd] Send succeed! Connect succeed! strStream is [[3]Send Message] [nConnFd] Send succeed! nConnFd : 5 Connect succeed! strStream is [[5]Send Message] [nConnFd] Send succeed! nConnFd : 6 Connect succeed! strStream is [[6]Send Message] [nConnFd] Send succeed! nConnFd : 7 Connect succeed! strStream is [[7]Send Message] [nConnFd] Send succeed! nConnFd : 8 Connect succeed! strStream is [[8]Send Message] [nConnFd] Send succeed! ^C [[email protected] Client]#
- 伺服器端輸出
[[email protected] Server]# ./Server Please input the Cmd: [0]: [socket] [1]: [bind] [2]: [listen] [3]: [accept_once] [4]: [accept_times] 3 [tcpdump -i lo -s 0 -w ./Tcpdump_accept_once.cap &] Start:netstat -atn | grep '17777' | sort -n -t : -k 2 tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes I have accept one Connect: [127.0.0.1], port[42653] Pending on [accept_once] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40362 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40363 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40358 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40359 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40360 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40361 ESTABLISHED tcp 0 0 127.0.0.1:40358 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40359 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40360 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40361 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40362 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40363 127.0.0.1:17777 ESTABLISHED netstat -atn | grep '17777' | sort -n -t : -k 2 Pending on [accept_once] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40362 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40363 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40358 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40359 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40360 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40361 ESTABLISHED tcp 0 0 127.0.0.1:40358 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40359 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40360 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40361 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40362 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40363 127.0.0.1:17777 ESTABLISHED
- 抓包資料
由Linux中listen()函數談開去由Linux中listen()函數談開去
- 用戶端輸出
-
資料分析
在實際的實驗過程中,對于
而言,其狀态與阻塞在Client
listen()
時,保持一緻,為ESTABLISHED狀态。
但是,對于
而言,相比于阻塞在Server
時,有四個套接字(端口分别為:listen()
,40358
,40359
,40360
)處于ESTABLISHED狀态,剩餘兩個則仍處于SYN_RECV。結合相應的抓包資料,所有的連接配接都完成了Tcp三次握手的連接配接過程,由于我們調用了一次40361
,是以,相較于阻塞在accept()
時(有3個套接字成功建立),對于listen()
Server
端而言,一共有4個套接字處于ESTABLISHED狀态,而這也與前文所述的方式2一緻。
是以在該階段,
都處于ESTABLISHED階段,Client
則有Server
數量的連接配接處于ESTABLISHED,剩餘則是處于SYN_RECV狀态。backlog + 1 + 1(成功調用了一次accept())
5. Server阻塞于accept()多次
- 實驗資料
- 用戶端輸出
[[email protected] Client]# ./Client nConnFd : 3 nConnFd : 4 nConnFd : 5 Connect succeed! strStream is [[4]Send Message] [nConnFd] Send succeed! Connect succeed! strStream is [[5]Send Message] [nConnFd] Send succeed! Connect succeed! strStream is [[3]Send Message] [nConnFd] Send succeed! nConnFd : 6 Connect succeed! strStream is [[6]Send Message] [nConnFd] Send succeed! nConnFd : 7 Connect succeed! strStream is [[7]Send Message] [nConnFd] Send succeed! nConnFd : 8 Connect succeed! strStream is [[8]Send Message] [nConnFd] Send succeed! ^C [[email protected] Client]#
- 伺服器端輸出
[[email protected] Server]# ./Server Please input the Cmd: [0]: [socket] [1]: [bind] [2]: [listen] [3]: [accept_once] [4]: [accept_times] 4 [tcpdump -i lo -s 0 -w ./Tcpdump_accept_times.cap &] Start:netstat -atn | grep '17777' | sort -n -t : -k 2 tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes I have accept one Connect: [127.0.0.1], port[40382] Pending on [accept_times] I will accept one I have accept one Connect: [127.0.0.1], port[40383] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40386 SYN_RECV tcp 0 0 127.0.0.1:17777 127.0.0.1:40387 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40382 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40383 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40384 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40385 ESTABLISHED tcp 0 0 127.0.0.1:40382 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40383 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40384 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40385 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40386 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40387 127.0.0.1:17777 ESTABLISHED netstat -atn | grep '17777' | sort -n -t : -k 2 I will accept one I have accept one Connect: [127.0.0.1], port[40384] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:17777 127.0.0.1:40387 SYN_RECV tcp 15 0 127.0.0.1:17777 127.0.0.1:40382 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40383 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40384 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40385 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40386 ESTABLISHED tcp 0 0 127.0.0.1:40382 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40383 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40384 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40385 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40386 127.0.0.1:17777 ESTABLISHED tcp 0 15 127.0.0.1:40387 127.0.0.1:17777 ESTABLISHED netstat -atn | grep '17777' | sort -n -t : -k 2 I will accept one I have accept one Connect: [127.0.0.1], port[40385] tcp 0 0 0.0.0.0:17777 0.0.0.0:* LISTEN tcp 15 0 127.0.0.1:17777 127.0.0.1:40382 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40383 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40384 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40385 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40386 ESTABLISHED tcp 15 0 127.0.0.1:17777 127.0.0.1:40387 ESTABLISHED tcp 0 0 127.0.0.1:40382 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40383 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40384 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40385 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40386 127.0.0.1:17777 ESTABLISHED tcp 0 0 127.0.0.1:40387 127.0.0.1:17777 ESTABLISHED
- 抓包資料
由Linux中listen()函數談開去由Linux中listen()函數談開去
- 用戶端輸出
-
資料分析
在實際的實驗過程中,對于
而言,其狀态與阻塞在Client
listen()
時,保持一緻,為ESTABLISHED狀态。
對于
而言,結果正如欲想的一緻,在調用3次Server
accept()
後,所有的連接配接都進入了ESTABLISHED。
是以在該階段,
都處于ESTABLISHED階段,Client
則有Server
數量的連接配接處于ESTABLISHED。backlog + 1 + 3(成功調用了三次accept()後)
4. 實驗結果分析
通過對以上分布實驗的結果的分析,我認為在之前對于
linux
中Tcp三次握手實作的了解是錯誤的。正确的了解應該是如下圖所示:
圖4: 關于Tcp三次握手實作的正确的了解
主要存在以下幾點誤區:
- 在
成功調用Server
後,相應的套接字——我們暫且将其稱為listen()
(注:該套接字唯一作用就是用來接受連接配接請求),将進入被動模式,之後A
其實就一直處于監聽狀态LISTEN。A
-
調用Client
發起連接配接請求,處于監聽狀态的套接字connect()
在收到連接配接請求後,首先會将其存儲在前文提到的A
隊列,并将相應的套接字——我們将其稱為SYN
,設定為SYN-RCVD,并發送應答給B
。在收到Client
後,Client
進入ESTABLISHED狀态。但需要注意的是,此時的B
應該仍位于B
隊列,隻有在判斷SYN
隊列未滿(小于accept
+ 1)時,才會将其轉移到backlog
隊列。accept
由于并未看過系統源碼,以上僅是結合相應實驗的到的結論,僅為個人了解,如有謬誤,還望各位批評指正。此外,針對不同的系統,結果可能仍有不同。
三、參考與連結
- 深入探索 Linux listen() 函數 backlog 的含義:https://blog.csdn.net/yangbodong22011/article/details/60399728
- How TCP backlog works in Linux:http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html
- 使用TCP建構的用戶端/伺服器公共架構:https://github.com/0Litost0/TcpClientServerFramework
- MakeFileTemplate:https://github.com/0Litost0/MakeFileTemplate
- netstat指令:https://www.cnblogs.com/peida/archive/2013/03/08/2949194.html
- 聊聊 tcpdump 與 Wireshark 抓包分析:https://www.jianshu.com/p/8d9accf1d2f1
五、文檔資訊
作者: Litost_Cheng
發表日期:2019年05月20日
更多内容:
- Litost_Cheng的個人部落格
- Litost_Cheng的Github
- Litost_Cheng的部落格