曾經有人問我套接字編程中listen
的第二個參數backlog
是什麼意思?多大的值合適?我不假思索地回答它表示服務器能夠接受的併發請求的最大值。然而事實真的是這樣的嗎?
TCP
經過三次握手創建鏈接的過程應該都不陌生了。從服務器的角度看,它分爲如下幾步html
TCP
狀態設置爲LISTEN
狀態,開啓監聽客戶端的鏈接請求SYN
報文後,TCP
狀態切換爲SYN RECEIVED
,併發送SYN ACK
報文ACK
報文後,TCP
三次握手完成,狀態切換爲ESTABLISHED
在Unix
系統中,開啓監聽是經過listen
完成。linux
int listen(int sockfd, int backlog)
listen
有兩個參數,第一個參數sockfd
表示要設置的套接字,本文主要關注的是其第二個參數backlog
;git
<Unix 網絡編程>將其描述爲已完成的鏈接隊列(ESTABLISHED
)與未完成鏈接隊列(SYN_RCVD
)之和的上限。編程
通常咱們將ESTABLISHED
狀態的鏈接稱爲全鏈接,而將SYN_RCVD
狀態的鏈接稱爲半鏈接ubuntu
當服務器收到一個SYN
後,它建立一個子鏈接加入到SYN_RCVD
隊列。在收到ACK
後,它將這個子鏈接移動到ESTABLISHED
隊列。最後當用戶調用accept()
時,會將鏈接從ESTABLISHED
隊列取出。小程序
listen
只是posix
標準,不是TCP
的標準!不是TCP
標準就意味着不一樣的內核能夠有本身獨立的實現服務器
POSIX是這麼說的:cookie
The
backlog
argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket's listen queue.
Linux
是什麼行爲呢 ? 查看listen
的man page
網絡
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests.
什麼意思呢?就是說的在Linux 2.2
之後, backlog
只限制完成了三次握手,處於ESTABLISHED
狀態等待accept
的子鏈接的數目了。併發
真的是這樣嗎?因而我決定抄一個小程序驗證一下:
服務器監聽50001
端口,而且設置backlog = 4
。注意,我爲了將隊列塞滿,沒有調用accept
。
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #define BACKLOG 4 int main(int argc, char **argv) { int listenfd; int connfd; struct sockaddr_in servaddr; listenfd = socket(PF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(50001); bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listenfd, BACKLOG); while(1) { sleep(1); } return 0; }
客戶端的代碼
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; sockfd = socket(PF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(50001); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (0 != connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { printf("connect failed!\n"); } else { printf("connect succeed!\n"); } sleep(30); return 1; }
爲了排除syncookie
的干擾,我首先關閉了syncookie
功能
echo 0 > /proc/sys/net/ipv4/tcp_syncookies
因爲我設置的backlog = 4
而且服務器始終不會accept
。所以預期會創建 4 個全鏈接, 但實際倒是
root@ubuntu-1:/home/user1/workspace/client# ./client & [1] 12798 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [2] 12799 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [3] 12800 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [4] 12801 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [5] 12802 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [6] 12803 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [7] 12804 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [8] 12805 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [9] 12806 root@ubuntu-1:/home/user1/workspace/client# connect succeed! ./client & [10] 12807 root@ubuntu-1:/home/user1/workspace/client# connect failed!
看!客戶器居然顯示成功創建了 9 次鏈接!
用netstat
看看TCP
鏈接狀態
> netstat -t Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 localhost:50001 localhost:55792 ESTABLISHED tcp 0 0 localhost:55792 localhost:50001 ESTABLISHED tcp 0 0 localhost:55798 localhost:50001 ESTABLISHED tcp 0 1 localhost:55806 localhost:50001 SYN_SENT tcp 0 0 localhost:50001 localhost:55784 ESTABLISHED tcp 0 0 localhost:50001 localhost:55794 SYN_RECV tcp 0 0 localhost:55786 localhost:50001 ESTABLISHED tcp 0 0 localhost:55800 localhost:50001 ESTABLISHED tcp 0 0 localhost:50001 localhost:55786 ESTABLISHED tcp 0 0 localhost:50001 localhost:55800 SYN_RECV tcp 0 0 localhost:55784 localhost:50001 ESTABLISHED tcp 0 0 localhost:50001 localhost:55796 SYN_RECV tcp 0 0 localhost:50001 localhost:55788 ESTABLISHED tcp 0 0 localhost:55794 localhost:50001 ESTABLISHED tcp 0 0 localhost:55788 localhost:50001 ESTABLISHED tcp 0 0 localhost:50001 localhost:55790 ESTABLISHED tcp 0 0 localhost:50001 localhost:55798 SYN_RECV tcp 0 0 localhost:55790 localhost:50001 ESTABLISHED tcp 0 0 localhost:55796 localhost:50001 ESTABLISHED
整理一下就是下面這樣
從上面能夠看出,一共有5條鏈接對是ESTABLISHED<->ESTABLISHED
鏈接, 但還有4條鏈接對是SYN_RECV<->ESTABLISHED
鏈接, 這表示對客戶端三次握手已經完成了,但對服務器尚未! 回顧一下TCP
三次握手的過程,形成這種鏈接對緣由只有多是服務器將客戶端最後發送的握手ACK
被丟棄了!
還有一個問題,我明明設置的backlog
的值是 4,可爲何還能創建5個鏈接 ?!
我實驗用的機器內核是
4.4.0
前面提到過已完成鏈接隊列和未完成鏈接隊列這兩個概念, Linux
有這兩個隊列嗎 ? Linux
既有又沒有! 說有是由於內核中能夠獲得兩種鏈接各自的長度; 說沒有是由於 Linux
只有已完成鏈接隊列實際存在, 而未完成鏈接隊列只有長度的記錄!
每個LISTEN
狀態的套接字都有一個struct inet_connection_sock
結構, 其中的accept_queue
從名字上也能夠看出就是已完成三次握手的子鏈接隊列.只是這個結構裏還記錄了半鏈接
請求的長度!
struct inet_connection_sock { // code omitted struct request_sock_queue icsk_accept_queue; // code omitted } struct request_sock_queue { // code omitted atomic_t qlen; // 半鏈接的長度 atomic_t young; // 通常狀況, 這個值 = qlen struct request_sock *rskq_accept_head; // 已完成鏈接的隊列頭 struct request_sock *rskq_accept_tail; // 已完成鏈接的隊列尾 // code omitted };
因此通常狀況下鏈接創建時,服務端的變化過程是這樣的:
SYN
報文, qlen
++,young
++ACK
報文, 三次握手完成,將鏈接加入accept
隊列,qlen
--,young
--accept
,將鏈接從accept
取出.再來看內核收到SYN
握手報文時的處理, 因爲我關閉了syncookie
,因此一旦知足了下面代碼中的兩個條件之一就會丟棄報文
int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb) if ((net->ipv4.sysctl_tcp_syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) { // 條件1: 半鏈接 >= backlog want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name); if (!want_cookie) goto drop; } if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { // 條件2: 全鏈接 > backlog 而且 半鏈接 > 1 NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; } // code omitted
下面是收到ACK
握手報文時的處理
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb, struct request_sock *req, struct dst_entry *dst, struct request_sock *req_unhash, bool *own_req) { // code omitted if (sk_acceptq_is_full(sk)) // 全鏈接 > backlog, 就丟棄 goto exit_overflow; newsk = tcp_create_openreq_child(sk, req, skb); // 建立子套接字了 // code omitted }
因此這樣就能夠解釋實驗現象了!
backlog
= 4, 半鏈接數目 = 0 sk_acceptq_is_full
的判斷條件是>
而不是>=
,因此依然能夠創建全鏈接backlog
,因此仍是能夠繼續回覆SYNACK
,但收到ACK
後已經不能再建立子套接字了,因此TCP
狀態依然爲SYN_RECV
.同時半鏈接的數目也增長到backlog
.而對於客戶端,它既然能收到SYNACK
握手報文,所以它能夠將TCP
狀態變爲ESTABLISHED
,backlog
,所以,這個SYN
報文會被丟棄.從以上的現象和分析中,我認爲內核存在如下問題
accept
隊列是否滿的判斷用>=
比>
更合適, 這樣才能體現backlog
的做用accept
隊列滿了,就應該拒絕半鏈接了,由於即便半鏈接握手完成,也沒法加入accept
隊列,不然就會出現SYN_RECV--ESTABLISHED
這樣狀態的鏈接對!這樣的鏈接是不能進行數據傳輸的!問題2
在16年的補丁中已經修改了! 因此若是你在更新版本的內核中進行相同的實驗, 會發現客戶端只能鏈接成功5次了,固然這也要先關閉syncookie
但問題1
尚未修改! 若是之後修改了,我也不會意外
(完)