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