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

文章導航

Redis源碼系列的初衷,是幫助咱們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,後續能夠本身閱讀源碼,或者跟着我這邊一塊兒閱讀。因爲我用c也是好幾年之前了,些許錯誤在所不免,但願讀者能不吝指出。html

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

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

曹工說Redis源碼(3)-- redis server 啓動過程完整解析(中)linux

本講主題

早上,技術羣裏,有個同窗問了個問題:git

這樣看來,仍是有部分同窗,對backlog這個參數,不甚瞭解,因此,乾脆本講就講講這個話題。redis

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

我會拿一個例子來說,例子裏的代碼,是直接從redis的源碼中拷貝的,一行沒改,經過這個例子,咱們也能更理解redis一些。編程

demo講解

backlog參數簡單講解

好比我監聽某端口,那麼客戶端能夠來同該端口,創建socket鏈接;正常狀況下,服務端(bio模式)會一直阻塞調用accept。api

你們想過沒有,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。

如何運行

代碼地址:

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

https://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)

下面這篇文章,也不錯:
使用Netty,咱們到底在開發些什麼?

相關文章
相關標籤/搜索