曹工說Redis源碼(4)– 通過redis server源碼來理解 listen 函數中的 backlog 參數

文章導航

Redis源碼系列的初衷,是幫助我們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,後續可以自己閱讀源碼,或者跟着我這邊一起閱讀。由於我用c也是好幾年以前了,些許錯誤在所難免,希望讀者能不吝指出。

曹工說Redis源碼(1)– redis debug環境搭建,使用clion,達到和調試java一樣的效果

曹工說Redis源碼(2)– redis server 啟動過程解析及簡單c語言基礎知識補充

曹工說Redis源碼(3)– redis server 啟動過程完整解析(中)

本講主題

早上,技術群里,有個同學問了個問題:

這樣看來,還是有部分同學,對backlog這個參數,不甚了解,所以,乾脆本講就講講這個話題。

本來可以直接拿java來舉例,不過這幾天正好在看redis,而且 redis server就是服務端,也是對外提供監聽端口的,而且其用 c 語言編寫,直接調用操作系統的api,不像java那樣封裝了一層,我們直接拿redis server的代碼來分析,就能離真相更近一點。

我會拿一個例子來講,例子里的代碼,是直接從redis的源碼中拷貝的,一行沒改,通過這個例子,我們也能更理解redis一些。

demo講解

backlog參數簡單講解

比如我監聽某端口,那麼客戶端可以來同該端口,建立socket連接;正常情況下,服務端(bio模式)會一直阻塞調用accept。

大家想過沒有,accept是怎麼拿到這個新進來的socket的?其實,這中間就有個阻塞隊列,當隊列沒有元素的時候,accept就會阻塞在這個隊列的take操作中,所以,我個人感覺,accept操作,其實和隊列的從隊尾或隊頭取一個元素,是一樣的。

當新客戶端建立連接時,完成了三次握手後,就會被放到這個隊列中,這個隊列,我們一般叫做:全連接隊列。

而這個隊列的最大容量,或者說size,就是backlog這個整數的大小。

正常情況下,只要服務端程序,accept不要卡殼,這個backlog隊列多大多小都無所謂;如果設置大一點,就能在服務端accept速度比較慢的時候,起到削峰的作用,怎麼感覺和mq有點像,哈哈。

說完了,下面開始測試了,首先測試程序正常accept的情況。

main測試程序


int main() {
    // 1
    char *pVoid = malloc(10);
    // 2
    int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
    printf("listening...");
    
    while (1) {
        int fd;
        struct sockaddr_storage sa;
        socklen_t salen = sizeof(sa);
		// 3
        char* err = malloc(20);
        // 4
        if ((fd = anetGenericAccept(err, serverSocket, (struct sockaddr*)&sa, &salen)) == -1)
            return ANET_ERR;
        printf("accept...%d",fd);
    }
}
  • 1處,我們先分配了一個10位元組的內存,這個主要是存放錯誤信息,在c語言編程中,不能像高級語言一樣拋異常,所以,返回值一般用來返回0/1,表示函數調用的成功失敗;如果需要在函數內部修改什麼東西,一般就會先new一個內存出來,然後把指針傳進去,然後在裏面就對這片內存空間進行操作,這裡也是一樣。

  • anetTcpServer 是我們自定義的,內部會實現如下邏輯:在本機的6380端口上進行監聽,backlog參數即全連接隊列的size,設為2。如果出錯的話,就會把錯誤信息,寫入1處的那個內存中。

    這一步調用完成後,端口就起好了。

  • 3處,同樣分配了一點內存,供accept連接出錯時使用,和1處作用類似

  • 4處,調用accept去從隊列取連接

anetTcpServer,監聽端口

int anetTcpServer(char *err, int port, char *bindaddr, int backlog) {
    return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}


static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog) {
    int s, rv;
    char _port[6];  /* strlen("65535") */
    struct addrinfo hints, *servinfo, *p;

    snprintf(_port, 6, "%d", port);
    // 1
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = af;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;    /* No effect if bindaddr != NULL */
	
    // 2
    if ((rv = getaddrinfo(bindaddr, _port, &hints, &servinfo)) != 0) {
        anetSetError(err, "%s", gai_strerror(rv));
        return ANET_ERR;
    }
    for (p = servinfo; p != NULL; p = p->ai_next) {
        // 3
        if ((s = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
            continue;
		// 4
        if (anetSetReuseAddr(err, s) == ANET_ERR) goto error;
        // 5
        if (anetListen(err, s, p->ai_addr, p->ai_addrlen, backlog) == ANET_ERR) goto error;
        goto end;
    }

    error:
    	s = ANET_ERR;
    end:
    	freeaddrinfo(servinfo);
    return s;
}
  • 1處,new一個結構體,c語言中,new一個對象比較麻煩,要先定義一個結構體類型的變量,如struct addrinfo hints,,然後調用memset來初始化內存,然後設置各個屬性。總體來說,這裡就是new了一個ipv4的地址

  • 2處,因為一般服務器都有多網卡,多個ip地址,還有環回網卡之類的,這裡的getaddrinfo,是利用我們第一步的hints,去幫助我們篩選出一個最終的網卡地址出來,然後賦值給 servinfo 變量。

    這裡可能有不準確的地方,大家可以直接看官方文檔:

    int getaddrinfo(const char *node, const char *service,
    const struct addrinfo *hints,
    struct addrinfo **res);

    Given node and service, which identify an Internet host and a service, getaddrinfo() returns one or more addrinfo structures, each of which contains an Internet address that can be specified in a call to bind(2) or connect(2).

  • 3處,使用第二步拿到的地址,new一個socket

  • 4處,anetSetReuseAddr,設置SO_REUSEADDR選項,我簡單查了下,可參考:

    [socket常見選項之SO_REUSEADDR,SO_REUSEPORT]

    SO_REUSEADDR
    一般來說,一個端口釋放後會等待兩分鐘之後才能再被使用,SO_REUSEADDR是讓端口釋放後立即就可以被再次使用

  • 5處,調用listen進行監聽,這裡用到了我們傳入的backlog參數。

    其中,backlog參數的官方說明,如下,意思也就是說,是隊列的size:

其中,anetListen是我們自定義的,我們接着看:

/*
 * 綁定並創建監聽套接字
 */
static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
    // 1
    if (bind(s, sa, len) == -1) {
        anetSetError(err, "bind: %s", strerror(errno));
        close(s);
        return ANET_ERR;
    }
	// 2
    if (listen(s, backlog) == -1) {
        anetSetError(err, "listen: %s", strerror(errno));
        close(s);
        return ANET_ERR;
    }
    return ANET_OK;
}
  • 1處,這裡進行綁定
  • 2處,這裡調用操作系統的函數,進行監聽,其中,第一個參數就是前面的socket file descriptor,第二個,就是backlog。

如何運行

代碼地址:

//gitee.com/ckl111/redis-3.0-annotated-cmake-in-clion/blob/master/our-redis-implementation/my_anet.c

//gitee.com/ckl111/redis-3.0-annotated-cmake-in-clion/blob/master/our-redis-implementation/my_anet.h

大家把上面這兩個文件,自己放到一個linux操作系統的文件夾下,然後執行以下命令,就能把這個demo啟動起來:

測試

查看監聽端口是否啟動

[root@mini2 ~]# netstat -ano|grep 6380
tcp        0      0 0.0.0.0:6380            0.0.0.0:*               LISTEN      off (0.00/0/0)

開啟一個shell,連接到6380端口

我這邊開了3個shell,去連接6380端口,然後,我執行:

[root@mini2 ~]# netstat -ano|grep 6380
tcp        0      0 0.0.0.0:6380            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:51386         127.0.0.1:6380          ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:54442         127.0.0.1:6380          ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:51930         127.0.0.1:6380          ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:6380          127.0.0.1:51386         ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:6380          127.0.0.1:54442         ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:6380          127.0.0.1:51930         ESTABLISHED off (0.00/0/0)

可以看到,已經有3個socket,連接到6380端口了。

查看端口對應的backlog隊列的相關東西

怎麼看backlog那些呢?有個命令叫ss,其是netstat的升級版,執行以下命令如下:

[root@mini2 ~]# ss -l |grep 6380
tcp    LISTEN     0      2       *:6380                  *:*     

上面我們查詢了6380這個監聽端口的狀態,其中,

  • 第一列,tcp,傳輸協議的名稱

  • 第二列,狀態,LISTEN

  • 第三列,查閱man netstat可以看到,

    Recv-Q
           Established: The count of bytes not copied by the user program connected to this socket.  
           Listening: Since Kernel 2.6.18 this column contains  the  current syn backlog.
    

    當其為Established狀態時,應該是緩衝區中沒被拷貝到用戶程序的位元組的數量;

    當其為LISTEN狀態時,表示當前backlog這個隊列,即前面說的全連接隊列的,容量的大小;這裡,因為我們的程序一直在accept連接,所以這裡為0

  • 第4列,官方文檔:

    Send-Q
    Established: The count of bytes not acknowledged by the remote host.  	
    
    Listening:   Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.
    

    當其為Established時,表示我方緩衝區中還沒有被對方ack的位元組數量

    當其為Listen時,表示全連接隊列的最大容量,我們是設為2的,所以這裡是2。

測試2

當我們程序不去accept的時候,會怎麼樣呢,修改程序如下:

int main() {
    char *pVoid = malloc(10);
    int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
    printf("listening...");

    while (1){
        sleep(100000);
    }

}

然後我們再去開啟3個客戶端連接,然後,最後看ss命令的情況:

[root@mini2 ~]# ss -l |grep 6380
tcp    LISTEN     3      2       *:6380                  *:*   

再執行netstat看看:

[root@mini2 ~]# netstat -ano|grep 6380
tcp        0      0 127.0.0.1:50238         127.0.0.1:6380          ESTABLISHED off (0.00/0/0)
tcp        0      0 127.0.0.1:50362         127.0.0.1:6380          ESTABLISHED off (0.00/0/0)

發現了嗎,只有2個連接是ok的。因為我們的全連接隊列,最大為2,現在已經full了啊,所以新連接進不來了。

總結

大家可以跟着我的demo試一下,相信理解會更深刻一點。

以前我也寫了一篇,大家可以參考下。

Linux中,Tomcat 怎麼承載高並發(深入Tcp參數 backlog)