TCP的三次握手恐怕是網絡從業者最先接觸的幾個概念之一了(協議棧的分層架構應該是另外一個),可是一直以來你覺得的三次握手流程真的是你覺得的那樣嗎,此次咱們就重點關注一下TCP_SYN_RECV這個狀態,順藤摸瓜,看看它在Linux協議棧中的實現與預期是否相符。node
TCP_SYN_RECV這個狀態是出如今三次握手流程中被動打開一方的,通常在現實中很難觀察到,由於它是由內核協議棧處理的,與應用程序無關,只要鏈接雙方的網絡暢通,這個狀態會稍縱即逝,若是咱們想要觀察到它,就須要藉助一些手段來實現了。網絡
按照直觀的理解,TCP_SYN_RECV應該表明被動打開的一方(一般是server)接收到了來自client的SYN包,並使用SYN/ACK做爲迴應,下一步只要server端正確接收到來自client的最後一個確認包,三次握手就算大功告成了。那麼,第一個問題來了,若是client端的最後一個ACK丟失了會怎樣?
架構
爲了模擬client端最後一個ACK丟失的情形而又不影響其它報文和流程,最方便的辦法是在server上經過iptables實現(server端的服務在54321端口監聽):
socket
iptables -t filter -I INPUT -p tcp --dport 54321 -m state --state ESTABLISHED -j DROP
這樣發往54321端口的第一個SYN包可以正確經過,但最後一個ACK卻會被丟棄了(若是在--state中加上NEW狀態,則第一個SYN也會被丟棄)tcp
經過抓包能夠看到,server端因爲一直沒有收到最後一個ACK,會重傳SYN/ACK,重傳的次數由sysctl_tcp_synack_retries控制,默認是5。在重傳過程當中server端的netstat顯示狀態爲SYN_RECV,而client端是ESTABLISHED,當達到重傳次數的限制後,server端將放棄該鏈接,而client則對此一無所知,留下了一條半開鏈接。函數
server端的抓包以下(注:No.3所顯示的ACK是tcpdump抓到到,隨後即被防火牆規則丟棄了,沒有機會進入TCP模塊的處理流程):
測試
SYN/ACK重傳超時前:ui
server: tcp 0 0 10.237.101.81:54321 0.0.0.0:* LISTEN 31983/tcp_server tcp 0 0 10.237.101.81:54321 10.237.101.43:55972 SYN_RECV - client: tcp 0 0 192.168.28.67:55972 10.237.101.81:54321 ESTABLISHED -
SYN/ACK重傳超時後:atom
server: tcp 0 0 10.237.101.81:54321 0.0.0.0:* LISTEN 31983/tcp_server client: tcp 0 0 192.168.28.67:55972 10.237.101.81:54321 ESTABLISHED - # cat /proc/sys/net/ipv4/tcp_synack_retries 5
在上一個測試中client端在發起鏈接後什麼也沒作,但這種狀況通常不會發生,client一般會在鏈接創建後當即開始發送數據。爲了觀察效果,咱們在程序中控制client端在鏈接創建80s以後開始發送數據,並在此前將防火牆規則刪除,以便client端的數據可以正常接收。以下圖server端抓包所示,在SYN/ACK重傳4次以後,咱們將防火牆規則刪除,client的數據也如期而至,這時,server端的鏈接可以正確轉換至ESTABLISHED狀態。
spa
client開始數據傳輸前:
server: tcp 0 0 10.237.101.81:54321 0.0.0.0:* LISTEN 396/tcp_server tcp 0 0 10.237.101.81:54321 10.237.101.43:55975 SYN_RECV - client: tcp 0 0 192.168.28.67:55975 10.237.101.81:54321 ESTABLISHED -
client開始傳輸數據後:
server: tcp 0 0 10.237.101.81:54321 0.0.0.0:* LISTEN 396/tcp_server tcp 0 0 10.237.101.81:54321 10.237.101.43:55975 ESTABLISHED 396/tcp_server client: tcp 0 0 192.168.28.67:55975 10.237.101.81:54321 ESTABLISHED -
因此說即便client端的最後一個ACK丟失了,只要它隨後當即發送數據並順利到達對端,server端依然可以正確轉換至ESTABLISHED狀態,這是由於數據報文中攜帶的ACK也可以起到確認的做用,這也是爲何在協議中沒有對最後一個ACK設置超時等待的緣由。
順着tcp_synack_retries的線索,再來找一找synack重傳定時器的實現。
sysctl_tcp_synack_retires是在inet_csk_reqsk_queue_prune()函數中調用的,而inet_csk_reqsk_queue_prune()又爲tcp_synack_timer()所調用:
/* * Timer for listening sockets */ static void tcp_synack_timer(struct sock *sk) { inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL, TCP_TIMEOUT_INIT, TCP_RTO_MAX); } static void tcp_keepalive_timer (unsigned long data) { ... if (sk->sk_state == TCP_LISTEN) { tcp_synack_timer(sk); goto out; } ... resched: inet_csk_reset_keepalive_timer (sk, elapsed); goto out; death: tcp_done(sk); out: bh_unlock_sock(sk); sock_put(sk); } void tcp_init_xmit_timers(struct sock *sk) { inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer); }
tcp_init_xmit_timers是在初始化一個socket(包括爲一條新接收的鏈接建立socket)時調用的。可見,在初始化一個新的socket時會同時建立一個synack retransmit timer,並須要的時候負責SYN/ACK的重傳工做。
上面分析的一大段如今看起來好像沒有任何意義,由於在咱們的認識中它就應該是這個樣子的,server收到SYN後會以SYN/ACK做爲應答並進入TCP_SYN_RECV狀態,合理又完美。但當咱們想從協議棧源碼中找到對應的依據時卻傻眼了,由於根據協議棧的源碼來分析的話,在收到SYN並應答SYN/ACK的處理流程中並無設置鏈接爲TCP_SYN_RECV狀態這一步,並且事實上在這個流程中甚至並無分配一個真正的struct sock結構,而只是分配了一個struct request_sock結構,那麼netstat顯示的SYN_RECV狀態又該如何理解呢?
難道是netstat顯示時作了處理?
netstat在讀取TCP鏈接信息時實際上讀取的是/proc/net/tcp這個文件,在tcp_ipv4.c中有對tcp procfs的註冊及處理函數,netstat中顯示的SYN_RECV狀態實際是由該文件中的get_openreq4()函數處理的:
static void get_openreq4(const struct sock *sk, const struct request_sock *req, struct seq_file *f, int i, int uid, int *len) { const struct inet_request_sock *ireq = inet_rsk(req); int ttd = req->expires - jiffies; seq_printf(f, "%4d: %08X:%04X %08X:%04X" " %02X %08X:%08X %02X:%08lX %08X %5d %8d %u %d %pK%n", i, ireq->loc_addr, ntohs(inet_sk(sk)->inet_sport), ireq->rmt_addr, ntohs(ireq->rmt_port), TCP_SYN_RECV, 0, 0, /* could print option size, but that is af dependent. */ 1, /* timers active (only the expire timer) */ jiffies_to_clock_t(ttd), req->retrans, uid, 0, /* non standard timer */ 0, /* open_requests have no inode */ atomic_read(&sk->sk_refcnt), req, len); } static int tcp4_seq_show(struct seq_file *seq, void *v) { struct tcp_iter_state *st; int len; if (v == SEQ_START_TOKEN) { seq_printf(seq, "%-*s\n", TMPSZ - 1, " sl local_address rem_address st tx_queue " "rx_queue tr tm->when retrnsmt uid timeout " "inode"); goto out; } st = seq->private; switch (st->state) { case TCP_SEQ_STATE_LISTENING: case TCP_SEQ_STATE_ESTABLISHED: get_tcp4_sock(v, seq, st->num, &len); break; case TCP_SEQ_STATE_OPENREQ: get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len); break; case TCP_SEQ_STATE_TIME_WAIT: get_timewait4_sock(v, seq, st->num, &len); break; } seq_printf(seq, "%*s\n", TMPSZ - 1 - len, ""); out: return 0; }
能夠看到,對於TCP_SEQ_STATE_OPENREQ這個狀態,會默認顯示爲SYN_RECV。
關於這一部分的內容尚未仔細研究過,就再也不指手畫腳了,歡迎指教。
結論:你覺得你知道的,可能並無那麼知道。