1. 程式人生 > >曹工說Redis原始碼(4)-- 通過redis server原始碼來理解 listen 函式中的 backlog 引數

曹工說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。

如何執行

程式碼地址:

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