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一些。編程
好比我監聽某端口,那麼客戶端能夠來同該端口,創建socket鏈接;正常狀況下,服務端(bio模式)會一直阻塞調用accept。api
你們想過沒有,accept是怎麼拿到這個新進來的socket的?其實,這中間就有個阻塞隊列,當隊列沒有元素的時候,accept就會阻塞在這個隊列的take操做中,因此,我我的感受,accept操做,其實和隊列的從隊尾或隊頭取一個元素,是同樣的。服務器
當新客戶端創建鏈接時,完成了三次握手後,就會被放到這個隊列中,這個隊列,咱們通常叫作:全鏈接隊列。
而這個隊列的最大容量,或者說size,就是backlog這個整數的大小。
正常狀況下,只要服務端程序,accept不要卡殼,這個backlog隊列多大多小都無所謂;若是設置大一點,就能在服務端accept速度比較慢的時候,起到削峯的做用,怎麼感受和mq有點像,哈哈。
說完了,下面開始測試了,首先測試程序正常accept的狀況。
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去從隊列取鏈接
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; }
代碼地址:
你們把上面這兩個文件,本身放到一個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)
我這邊開了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那些呢?有個命令叫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。
當咱們程序不去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,咱們到底在開發些什麼?