天天看點

由Linux中listen()函數談開去由Linux中listen()函數談開去

  • 由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連接配接的建立過程。

  本篇部落格編寫主要參考借鑒了以下兩篇博文,并在其基礎上,加入了個人的一些思考與探索。

  1. 深入探索 Linux listen() 函數 backlog 的含義
  2. How TCP backlog works in Linux

  下述内容的讨論,建立在對Tcp/Ip、socket有一定了解的基礎上,相關的知識點,可以參考以下博文:

1.揭開Socket程式設計的面紗

2.Linux Socket程式設計(不限Linux)

2. 問題引入

  當我們嘗試在

Linux

上通過

Socket

建立一個伺服器,并接收用戶端的連接配接請求時,伺服器端程式通常需要執行以下流程:

  1. 使用建立 socket() 建立一個監聽描述符

    ListenFd

  2. 使用 bind() 為

    ListenFd

    綁定一個本地的位址,以便于其它的

    socket

    (套接字),能夠與其建立連接配接
  3. 使用 listen() 将

    ListenFd

    設定為被動模式——表明自己樂意接受連接配接請求,并設定【連接配接建立隊列的限制】。
  4. 調用 accept() 以接收具體的連接配接。
  5. 資料互動。。。

  伺服器與用戶端通過調用相關函數建立連接配接的流程如下所示:

由Linux中listen()函數談開去由Linux中listen()函數談開去

圖1:Tcp連接配接實作

注:在上圖中,并未展示斷開連接配接的過程

  在平時的工程實踐中,自己也都是照貓畫虎, 知其然而不知其是以然。由于最近嘗試寫一個使用TCP建構的用戶端/伺服器公共架構的架構,在寫作過程中,發現自己對于很多基礎的操作都不明其意,基于此,才有了這邊博文。

  文章首先介紹socket的一些基本概念,接下來通過一個具體的執行個體,并結合相應的抓包分析,印證明際與理論是否相符。

二、原理介紹

1. Tcp三次握手

  在學習計算機網絡的相關知識時,想必大家對于Tcp三次握手的連接配接過程并不陌生:

由Linux中listen()函數談開去由Linux中listen()函數談開去

圖2: Tcp三次握手

  正如上圖所示:

  1. 用戶端請求建立連接配接,然後進入 SYN-SENT 狀态
  2. 處于LISTEN狀态的伺服器端在收到連接配接請求後,給用戶端回複應答,同時進入SYN-RCVD狀态
  3. 用戶端在收到應答後,進入ESTABLISHED狀态,同時再次發消息告知伺服器端
  4. 伺服器端在收到消息後,亦即進入ESTABLISHED狀态

  在完成上述互動過程後,Tcp 連接配接建立,兩者即可進行後續的資料互動。

  以上為建立連接配接的邏輯過程,看起來清晰易懂,似乎沒有什麼難以了解的地方,但是,倘若需要我們将三次握手過程與圖一的實作聯系起來呢?

  根據自己以往的經驗,具體的實作,與Tcp三次握手的原理圖,應該是如下的對應關系:

由Linux中listen()函數談開去由Linux中listen()函數談開去

圖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三次握手與具體實作之間的聯系,該例程會根據使用者輸入的不同的選項,将程式阻塞在不同階段,并結合對應時刻的連接配接狀态以及抓包資料,确定目前連接配接所處的狀态。

  在開始具體的實驗前,有以下幾點是需要我們注意的:

  1. 為了了解

    backlog

    參數的實際含義,實驗過程中,我們要求

    Client

    程式中的

    THREAD_NUM

    參數應該要大于

    Server

    中的

    BACKLOG

    ,以認為造成過量的連接配接請求。
  2. 代碼的編譯使用自動的

    makefile

    檔案模闆MakeFileTemplate,使用者隻需執行

    make

    指令,即可生成相應的可執行檔案。
  3. 連接配接狀态的擷取使用netstat擷取
  4. 抓包資料的擷取使用Tcpdump,并配合Wireshark工具對抓包進行分析,關于兩工具的使用,詳見該連結:聊聊 tcpdump 與 Wireshark 抓包分析。

3. 分步實驗

在具體的實驗過程中,我們會将

Server

分别阻塞以下幾個階段,同時會附上相應的程式輸出(

Client

,

Server

),連接配接狀态,以及抓包資料,以友善讀者能夠有一個直覺的認識。

1. Server阻塞于socket()建立後

  • 實驗資料
    1. 用戶端輸出
      [[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]#   
                 
    2. 伺服器輸出
      [[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
                 
    3. 抓包資料
      由Linux中listen()函數談開去由Linux中listen()函數談開去
  • 資料分析

      從以上實驗資料,我們不難看出,當

    Client

    嘗試去連接配接阻塞在

    socket()

    狀态的

    Server

    時,連接配接全部失敗,從抓包擷取到的資料來看,針對用戶端的

    SYN

    請求,

    Server

    直接回複了

    RST

    ,而從

    Client

    調用

    accept()

    傳回的錯誤結果來看,顯示連接配接被拒絕。

      是以在該階段,

    Client

    應該是進入了短暫的SYN-SENT階段(注:并未從

    netstat

    抓取到有關資料,可能是由于連接配接迅速被斷開導緻),随後連接配接即被拒絕,而

    Server

    則是處于CLOSED狀态。

2. Server阻塞于bind()後

  • 實驗資料
    1. 用戶端輸出
      [[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]# 
                 
    2. 伺服器端輸出
      [[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
                 
    3. 抓包資料
      由Linux中listen()函數談開去由Linux中listen()函數談開去
  • 資料分析

      在實際的實驗過程中,對于

    Client

    而言,其情況與阻塞在

    socket()

    時保持一緻:連接配接全部失敗。

      是以在該階段,

    Client

    應該也是進入了短暫的SYN-SENT階段(注:并未從

    netstat

    抓取到有關資料,可能是由于連接配接迅速被斷開導緻),随後連接配接即被拒絕,而

    Server

    一直處于CLOSED狀态。

3. Server阻塞于listen()後

  • 實驗資料
    1. 用戶端輸出
      [[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]# 
                 
    2. 伺服器端輸出
      [[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
                 
    3. 抓包資料
      由Linux中listen()函數談開去由Linux中listen()函數談開去
  • 資料分析

      在實際的實驗過程中,對于

    Client

    而言,從

    connect()

    以及

    write()

    的結果來看,所有的連接配接都已成功建立,且處于ESTABLISHED狀态。

      但是,對于

    Server

    而言,有三個套接字(端口分别為:

    40352

    ,

    40353

    ,

    40354

    )處于ESTABLISHED狀态,剩餘的三個則仍處于SYN_RECV(但是需要注意的是,我們是将

    backlog

    的值設定為2)。同時,我們結合具體的抓包資料進行分析:針對所有連接配接而言,Tcp三次握手過程,都已完成。由此說明,對于處于被動模式的套接字(調用

    listen()

    後),能夠自動處理接收到的連接配接請求,并完成三次握手的互動。同時,會将

    backlog + 1

    數量的連接配接放置于

    accept

    隊列。

      是以在該階段,

    Client

    都處于ESTABLISHED階段,

    Server

    則有

    backlog + 1

    數量的連接配接處于ESTABLISHED,剩餘則是處于SYN_RECV狀态。

4. Server阻塞于accept()一次後

  • 實驗資料
    1. 用戶端輸出
      [[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]# 
                 
    2. 伺服器端輸出
      [[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 
                 
    3. 抓包資料
      由Linux中listen()函數談開去由Linux中listen()函數談開去
  • 資料分析

      在實際的實驗過程中,對于

    Client

    而言,其狀态與阻塞在

    listen()

    時,保持一緻,為ESTABLISHED狀态。

      但是,對于

    Server

    而言,相比于阻塞在

    listen()

    時,有四個套接字(端口分别為:

    40358

    ,

    40359

    ,

    40360

    ,

    40361

    )處于ESTABLISHED狀态,剩餘兩個則仍處于SYN_RECV。結合相應的抓包資料,所有的連接配接都完成了Tcp三次握手的連接配接過程,由于我們調用了一次

    accept()

    ,是以,相較于阻塞在

    listen()

    時(有3個套接字成功建立),對于

    Server

    端而言,一共有4個套接字處于ESTABLISHED狀态,而這也與前文所述的方式2一緻。

      是以在該階段,

    Client

    都處于ESTABLISHED階段,

    Server

    則有

    backlog + 1 + 1(成功調用了一次accept())

    數量的連接配接處于ESTABLISHED,剩餘則是處于SYN_RECV狀态。

5. Server阻塞于accept()多次

  • 實驗資料
    1. 用戶端輸出
      [[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]# 
                 
    2. 伺服器端輸出
      [[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 
                 
    3. 抓包資料
      由Linux中listen()函數談開去由Linux中listen()函數談開去
  • 資料分析

      在實際的實驗過程中,對于

    Client

    而言,其狀态與阻塞在

    listen()

    時,保持一緻,為ESTABLISHED狀态。

      對于

    Server

    而言,結果正如欲想的一緻,在調用3次

    accept()

    後,所有的連接配接都進入了ESTABLISHED。

      是以在該階段,

    Client

    都處于ESTABLISHED階段,

    Server

    則有

    backlog + 1 + 3(成功調用了三次accept()後)

    數量的連接配接處于ESTABLISHED。

4. 實驗結果分析

  通過對以上分布實驗的結果的分析,我認為在之前對于

linux

中Tcp三次握手實作的了解是錯誤的。正确的了解應該是如下圖所示:

由Linux中listen()函數談開去由Linux中listen()函數談開去

圖4: 關于Tcp三次握手實作的正确的了解

  主要存在以下幾點誤區:

  1. Server

    成功調用

    listen()

    後,相應的套接字——我們暫且将其稱為

    A

    (注:該套接字唯一作用就是用來接受連接配接請求),将進入被動模式,之後

    A

    其實就一直處于監聽狀态LISTEN。
  2. Client

    調用

    connect()

    發起連接配接請求,處于監聽狀态的套接字

    A

    在收到連接配接請求後,首先會将其存儲在前文提到的

    SYN

    隊列,并将相應的套接字——我們将其稱為

    B

    ,設定為SYN-RCVD,并發送應答給

    Client

    。在收到

    Client

    後,

    B

    進入ESTABLISHED狀态。但需要注意的是,此時的

    B

    應該仍位于

    SYN

    隊列,隻有在判斷

    accept

    隊列未滿(小于

    backlog

    + 1)時,才會将其轉移到

    accept

    隊列。

  由于并未看過系統源碼,以上僅是結合相應實驗的到的結論,僅為個人了解,如有謬誤,還望各位批評指正。此外,針對不同的系統,結果可能仍有不同。

三、參考與連結

  1. 深入探索 Linux listen() 函數 backlog 的含義:https://blog.csdn.net/yangbodong22011/article/details/60399728
  2. How TCP backlog works in Linux:http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html
  3. 使用TCP建構的用戶端/伺服器公共架構:https://github.com/0Litost0/TcpClientServerFramework
  4. MakeFileTemplate:https://github.com/0Litost0/MakeFileTemplate
  5. netstat指令:https://www.cnblogs.com/peida/archive/2013/03/08/2949194.html
  6. 聊聊 tcpdump 與 Wireshark 抓包分析:https://www.jianshu.com/p/8d9accf1d2f1

五、文檔資訊

作者: Litost_Cheng

發表日期:2019年05月20日

更多内容:

  1. Litost_Cheng的個人部落格
  2. Litost_Cheng的Github
  3. Litost_Cheng的部落格

繼續閱讀