我跟面試官聊 TCP 三次握手源碼,他夸我真棒!

大家好,我是小林。
之前我的圖解網(wǎng)絡(luò)系列,寫了很多關(guān)于 TCP 的圖解文章,很多同學(xué)看完后都跟我說(shuō),每次面試的時(shí)候,TCP 部分都能聊跨面試官。
但是對(duì)于 TCP 三次握手的源碼分析,我還沒(méi)寫過(guò)。
今天就跟大家來(lái)嘮嗑下,TCP 三次握手的源碼,看看他到底做什么?
在后端相關(guān)崗位的入職面試中,三次握手的出場(chǎng)頻率非常的高,甚至說(shuō)它是必考題也不為過(guò)。一般的答案都是說(shuō)客戶端如何發(fā)起 SYN 握手進(jìn)入 SYN_SENT 狀態(tài),服務(wù)器響應(yīng) SYN 并回復(fù) SYNACK,然后進(jìn)入 SYN_RECV,...... , 吧啦吧啦諸如此類。
但我今天想給出一份不一樣的答案。其實(shí)三次握手在內(nèi)核的實(shí)現(xiàn)中,并不只是簡(jiǎn)單的狀態(tài)的流轉(zhuǎn),還包括半連接隊(duì)列、syncookie、全連接隊(duì)列、重傳計(jì)時(shí)器等關(guān)鍵操作。如果能深刻理解這些,你對(duì)線上把握和理解將更進(jìn)一步。如果有面試官問(wèn)起你三次握手,相信這份答案一定能幫你在面試官面前贏得非常多的加分。
在基于 TCP 的服務(wù)開(kāi)發(fā)中,三次握手的主要流程圖如下。

服務(wù)器中的核心代碼是創(chuàng)建 socket,綁定端口,listen 監(jiān)聽(tīng),最后 accept 接收客戶端的請(qǐng)求。
//服務(wù)端核心代碼
int main(int argc, char const *argv[])
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, ...);
listen(fd, 128);
accept(fd, ...);
...
}
客戶端的相關(guān)代碼是創(chuàng)建 socket,然后調(diào)用 connect 連接 server。
//客戶端核心代碼
int main(){
fd = socket(AF_INET,SOCK_STREAM, 0);
connect(fd, ...);
...
}
圍繞這個(gè)三次握手圖,以及客戶端,服務(wù)端的核心代碼,我們來(lái)深度探索一下三次握手過(guò)程中的內(nèi)部操作。我們從和三次握手過(guò)程關(guān)系比較大的 listen 講起!
友情提示:本文中內(nèi)核源碼會(huì)比較多。如果你能理解的了更好,如果覺(jué)得理解起來(lái)有困難,那直接重點(diǎn)看本文中的描述性的文字,尤其是加粗部分的即可。另外文章最后有一張總結(jié)圖歸納和整理了全文內(nèi)容。
一、服務(wù)器的 listen
我們都知道,服務(wù)器在開(kāi)始提供服務(wù)之前都需要先 listen 一下。但 listen 內(nèi)部究竟干了啥,我們平時(shí)很少去琢磨。
今天就讓我們?cè)敿?xì)來(lái)看看,直接上一段 listen 時(shí)執(zhí)行到的內(nèi)核代碼。
//file: net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size = sizeof(struct listen_sock);
struct listen_sock *lopt;
//計(jì)算半連接隊(duì)列的長(zhǎng)度
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = ......
//為半連接隊(duì)列申請(qǐng)內(nèi)存
lopt_size += nr_table_entries * sizeof(struct request_sock *);
if (lopt_size > PAGE_SIZE)
lopt = vzalloc(lopt_size);
else
lopt = kzalloc(lopt_size, GFP_KERNEL);
//全連接隊(duì)列頭初始化
queue->rskq_accept_head = NULL;
//半連接隊(duì)列設(shè)置
lopt->nr_table_entries = nr_table_entries;
queue->listen_opt = lopt;
......
}
在這段代碼里,內(nèi)核計(jì)算了半連接隊(duì)列的長(zhǎng)度。然后據(jù)此算出半連接隊(duì)列所需要的實(shí)際內(nèi)存大小,開(kāi)始申請(qǐng)用于管理半連接隊(duì)列對(duì)象的內(nèi)存(半連接隊(duì)列需要快速查找,所以內(nèi)核是用哈希表來(lái)管理半連接隊(duì)列的,具體在 listen_sock 下的 syn_table 下)。最后將半連接隊(duì)列掛到了接收隊(duì)列 queue 上。
另外 queue->rskq_accept_head 代表的是全連接隊(duì)列,它是一個(gè)鏈表的形式。在 listen 這里因?yàn)檫€沒(méi)有連接,所以將全連接隊(duì)列頭 queue->rskq_accept_head 設(shè)置成 NULL。
當(dāng)全連接隊(duì)列和半連接隊(duì)列中有元素的時(shí)候,他們?cè)趦?nèi)核中的結(jié)構(gòu)圖大致如下。

在服務(wù)器 listen 的時(shí)候,主要是進(jìn)行了全/半連接隊(duì)列的長(zhǎng)度限制計(jì)算,以及相關(guān)的內(nèi)存申請(qǐng)和初始化。全/連接隊(duì)列初始化了以后才可以相應(yīng)來(lái)自客戶端的握手請(qǐng)求。
二、客戶端 connect
客戶端通過(guò)調(diào)用 connect 來(lái)發(fā)起連接。在 connect 系統(tǒng)調(diào)用中會(huì)進(jìn)入到內(nèi)核源碼的 tcp_v4_connect。
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
//設(shè)置 socket 狀態(tài)為 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
//動(dòng)態(tài)選擇一個(gè)端口
err = inet_hash_connect(&tcp_death_row, sk);
//函數(shù)用來(lái)根據(jù) sk 中的信息,構(gòu)建一個(gè)完成的 syn 報(bào)文,并將它發(fā)送出去。
err = tcp_connect(sk);
}
在這里將完成把 socket 狀態(tài)設(shè)置為 TCP_SYN_SENT。再通過(guò) inet_hash_connect 來(lái)動(dòng)態(tài)地選擇一個(gè)可用的端口后,進(jìn)入到 tcp_connect 中。
//file:net/ipv4/tcp_output.c
int tcp_connect(struct sock *sk)
{
tcp_connect_init(sk);
//申請(qǐng) skb 并構(gòu)造為一個(gè) SYN 包
......
//添加到發(fā)送隊(duì)列 sk_write_queue 上
tcp_connect_queue_skb(sk, buff);
//實(shí)際發(fā)出 syn
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
//啟動(dòng)重傳定時(shí)器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}
在 tcp_connect 申請(qǐng)和構(gòu)造 SYN 包,然后將其發(fā)出。同時(shí)還啟動(dòng)了一個(gè)重傳定時(shí)器,該定時(shí)器的作用是等到一定時(shí)間后收不到服務(wù)器的反饋的時(shí)候來(lái)開(kāi)啟重傳。在 3.10 版本中首次超時(shí)時(shí)間是 1 s,一些老版本中是 3 s。
總結(jié)一下,客戶端在 connect 的時(shí)候,把本地 socket 狀態(tài)設(shè)置成了 TCP_SYN_SENT,選了一個(gè)可用的端口,接著發(fā)出 SYN 握手請(qǐng)求并啟動(dòng)重傳定時(shí)器。
三、服務(wù)器響應(yīng) SYN
在服務(wù)器端,所有的 TCP 包(包括客戶端發(fā)來(lái)的 SYN 握手請(qǐng)求)都經(jīng)過(guò)網(wǎng)卡、軟中斷,進(jìn)入到 tcp_v4_rcv。在該函數(shù)中根據(jù)網(wǎng)絡(luò)包(skb)TCP 頭信息中的目的 IP 信息查到當(dāng)前在 listen 的 socket。然后繼續(xù)進(jìn)入 tcp_v4_do_rcv 處理握手過(guò)程。
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
//服務(wù)器收到第一步握手 SYN 或者第三步 ACK 都會(huì)走到這里
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
}
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
}
在 tcp_v4_do_rcv 中判斷當(dāng)前 socket 是 listen 狀態(tài)后,首先會(huì)到 tcp_v4_hnd_req 去查看半連接隊(duì)列。服務(wù)器第一次響應(yīng) SYN 的時(shí)候,半連接隊(duì)列里必然是空空如也,所以相當(dāng)于什么也沒(méi)干就返回了。
//file:net/ipv4/tcp_ipv4.c
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
// 查找 listen socket 的半連接隊(duì)列
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
...
return sk;
}
在 tcp_rcv_state_process 里根據(jù)不同的 socket 狀態(tài)進(jìn)行不同的處理。
//file:net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
switch (sk->sk_state) {
//第一次握手
case TCP_LISTEN:
if (th->syn) { //判斷是 SYN 握手包
...
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
......
}
其中 conn_request 是一個(gè)函數(shù)指針,指向 tcp_v4_conn_request。服務(wù)器響應(yīng) SYN 的主要處理邏輯都在這個(gè) tcp_v4_conn_request 里。
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
//看看半連接隊(duì)列是否滿了
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
if (!want_cookie)
goto drop;
}
//在全連接隊(duì)列滿的情況下,如果有 young_ack,那么直接丟
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
//分配 request_sock 內(nèi)核對(duì)象
req = inet_reqsk_alloc(&tcp_request_sock_ops);
//構(gòu)造 syn+ack 包
skb_synack = tcp_make_synack(sk, dst, req,
fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL);
if (likely(!do_fastopen)) {
//發(fā)送 syn + ack 響應(yīng)
err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,
ireq->rmt_addr, ireq->opt);
//添加到半連接隊(duì)列,并開(kāi)啟計(jì)時(shí)器
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}else ...
}
在這里首先判斷半連接隊(duì)列是否滿了,如果滿了的話進(jìn)入 tcp_syn_flood_action 去判斷是否開(kāi)啟了 tcp_syncookies 內(nèi)核參數(shù)。如果隊(duì)列滿,且未開(kāi)啟 tcp_syncookies,那么該握手包將直接被丟棄?。?/strong>
接著還要判斷全連接隊(duì)列是否滿。因?yàn)槿B接隊(duì)列滿也會(huì)導(dǎo)致握手異常的,那干脆就在第一次握手的時(shí)候也判斷了。如果全連接隊(duì)列滿了,且有 young_ack 的話,那么同樣也是直接丟棄。
young_ack 是半連接隊(duì)列里保持著的一個(gè)計(jì)數(shù)器。記錄的是剛有SYN到達(dá),沒(méi)有被SYN_ACK重傳定時(shí)器重傳過(guò)SYN_ACK,同時(shí)也沒(méi)有完成過(guò)三次握手的sock數(shù)量
接下來(lái)是構(gòu)造 synack 包,然后通過(guò) ip_build_and_send_pkt 把它發(fā)送出去。
最后把當(dāng)前握手信息添加到半連接隊(duì)列,并開(kāi)啟計(jì)時(shí)器。計(jì)時(shí)器的作用是如果某個(gè)時(shí)間之內(nèi)還收不到客戶端的第三次握手的話,服務(wù)器會(huì)重傳 synack 包。
總結(jié)一下,服務(wù)器響應(yīng) ack 是主要工作是判斷下接收隊(duì)列是否滿了,滿的話可能會(huì)丟棄該請(qǐng)求,否則發(fā)出 synack。申請(qǐng) request_sock 添加到半連接隊(duì)列中,同時(shí)啟動(dòng)定時(shí)器。
四、客戶端響應(yīng) SYNACK
客戶端收到服務(wù)器端發(fā)來(lái)的 synack 包的時(shí)候,也會(huì)進(jìn)入到 tcp_rcv_state_process 函數(shù)中來(lái)。不過(guò)由于自身 socket 的狀態(tài)是 TCP_SYN_SENT,所以會(huì)進(jìn)入到另一個(gè)不同的分支中去。
//file:net/ipv4/tcp_input.c
//除了 ESTABLISHED 和 TIME_WAIT,其他狀態(tài)下的 TCP 處理都走這里
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
switch (sk->sk_state) {
//服務(wù)器收到第一個(gè)ACK包
case TCP_LISTEN:
...
//客戶端第二次握手處理
case TCP_SYN_SENT:
//處理 synack 包
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
...
return 0;
}
tcp_rcv_synsent_state_process 是客戶端響應(yīng) synack 的主要邏輯。
//file:net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
...
tcp_ack(sk, skb, FLAG_SLOWPATH);
//連接建立完成
tcp_finish_connect(sk, skb);
if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
icsk->icsk_ack.pingpong)
//延遲確認(rèn)...
else {
tcp_send_ack(sk);
}
}
tcp_ack()->tcp_clean_rtx_queue()
//file: net/ipv4/tcp_input.c
static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets,
u32 prior_snd_una)
{
//刪除發(fā)送隊(duì)列
...
//刪除定時(shí)器
tcp_rearm_rto(sk);
}
//file: net/ipv4/tcp_input.c
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
//修改 socket 狀態(tài)
tcp_set_state(sk, TCP_ESTABLISHED);
//初始化擁塞控制
tcp_init_congestion_control(sk);
...
//?;钣?jì)時(shí)器打開(kāi)
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
}
客戶端修改自己的 socket 狀態(tài)為 ESTABLISHED,接著打開(kāi) TCP 的?;钣?jì)時(shí)器。
//file:net/ipv4/tcp_output.c
void tcp_send_ack(struct sock *sk)
{
//申請(qǐng)和構(gòu)造 ack 包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
...
//發(fā)送出去
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
}
在 tcp_send_ack 中構(gòu)造 ack 包,并把它發(fā)送了出去。
客戶端響應(yīng)來(lái)自服務(wù)器端的 synack 時(shí)清除了 connect 時(shí)設(shè)置的重傳定時(shí)器,把當(dāng)前 socket 狀態(tài)設(shè)置為 ESTABLISHED,開(kāi)啟?;钣?jì)時(shí)器后發(fā)出第三次握手的 ack 確認(rèn)。
五、服務(wù)器響應(yīng) ACK
服務(wù)器響應(yīng)第三次握手的 ack 時(shí)同樣會(huì)進(jìn)入到 tcp_v4_do_rcv
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
}
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
}
不過(guò)由于這已經(jīng)是第三次握手了,半連接隊(duì)列里會(huì)存在上次第一次握手時(shí)留下的半連接信息。所以 tcp_v4_hnd_req 的執(zhí)行邏輯會(huì)不太一樣。
//file:net/ipv4/tcp_ipv4.c
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
...
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return tcp_check_req(sk, skb, req, prev, false);
...
}
inet_csk_search_req 負(fù)責(zé)在半連接隊(duì)列里進(jìn)行查找,找到以后返回一個(gè)半連接 request_sock 對(duì)象。然后進(jìn)入到 tcp_check_req 中。
//file:net/ipv4/tcp_minisocks.c
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct request_sock **prev,
bool fastopen)
{
...
//創(chuàng)建子 socket
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
...
//清理半連接隊(duì)列
inet_csk_reqsk_queue_unlink(sk, req, prev);
inet_csk_reqsk_queue_removed(sk, req);
//添加全連接隊(duì)列
inet_csk_reqsk_queue_add(sk, req, child);
return child;
}
5.1 創(chuàng)建子 socket
icsk_af_ops->syn_recv_sock 對(duì)應(yīng)的是 tcp_v4_syn_recv_sock 函數(shù)。
//file:net/ipv4/tcp_ipv4.c
const struct inet_connection_sock_af_ops ipv4_specific = {
......
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
//三次握手接近就算是完畢了,這里創(chuàng)建 sock 內(nèi)核對(duì)象
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
//判斷接收隊(duì)列是不是滿了
if (sk_acceptq_is_full(sk))
goto exit_overflow;
//創(chuàng)建 sock && 初始化
newsk = tcp_create_openreq_child(sk, req, skb);
**注意,在第三次握手的這里又繼續(xù)判斷一次全連接隊(duì)列是否滿了,如果滿了修改一下計(jì)數(shù)器就丟棄了。**如果隊(duì)列不滿,那么就申請(qǐng)創(chuàng)建新的 sock 對(duì)象。
5.2 刪除半連接隊(duì)列
把連接請(qǐng)求塊從半連接隊(duì)列中刪除。
//file: include/net/inet_connection_sock.h
static inline void inet_csk_reqsk_queue_unlink(struct sock *sk, struct request_sock *req,
struct request_sock **prev)
{
reqsk_queue_unlink(&inet_csk(sk)->icsk_accept_queue, req, prev);
}
reqsk_queue_unlink 中把連接請(qǐng)求塊從半連接隊(duì)列中刪除。
5.3 添加全連接隊(duì)列
接著添加到全連接隊(duì)列里邊來(lái)。
//file:net/ipv4/syncookies.c
static inline void inet_csk_reqsk_queue_add(struct sock *sk,
struct request_sock *req,
struct sock *child)
{
reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child);
}
在 reqsk_queue_add 中將握手成功的 request_sock 對(duì)象插入到全連接隊(duì)列鏈表的尾部。
//file: include/net/request_sock.h
static inline void reqsk_queue_add(...)
{
req->sk = child;
sk_acceptq_added(parent);
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_head = req;
else
queue->rskq_accept_tail->dl_next = req;
queue->rskq_accept_tail = req;
req->dl_next = NULL;
}
5.4 設(shè)置連接為 ESTABLISHED
//file:net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
...
switch (sk->sk_state) {
//服務(wù)端第三次握手處理
case TCP_SYN_RECV:
//改變狀態(tài)為連接
tcp_set_state(sk, TCP_ESTABLISHED);
...
}
}
將連接設(shè)置為 TCP_ESTABLISHED 狀態(tài)。
服務(wù)器響應(yīng)第三次握手 ack 所做的工作是把當(dāng)前半連接對(duì)象刪除,創(chuàng)建了新的 sock 后加入到全連接隊(duì)列中,最后將新連接狀態(tài)設(shè)置為 ESTABLISHED。
六、服務(wù)器 accept
最后 accept 一步咱們長(zhǎng)話短說(shuō)。
//file: net/ipv4/inet_connection_sock.c
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
//從全連接隊(duì)列中獲取
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
req = reqsk_queue_remove(queue);
newsk = req->sk;
return newsk;
}
reqsk_queue_remove 這個(gè)操作很簡(jiǎn)單,就是從全連接隊(duì)列的鏈表里獲取出第一個(gè)元素返回就行了。
//file:include/net/request_sock.h
static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue)
{
struct request_sock *req = queue->rskq_accept_head;
queue->rskq_accept_head = req->dl_next;
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_tail = NULL;
return req;
}
所以,accept 的重點(diǎn)工作就是從已經(jīng)建立好的全連接隊(duì)列中取出一個(gè)返回給用戶進(jìn)程。
本文總結(jié)
在后端相關(guān)崗位的入職面試中,三次握手的出場(chǎng)頻率非常的高。其實(shí)在三次握手的過(guò)程中,不僅僅是一個(gè)握手包的發(fā)送 和 TCP 狀態(tài)的流轉(zhuǎn)。還包含了端口選擇,連接隊(duì)列創(chuàng)建與處理等很多關(guān)鍵技術(shù)點(diǎn)。通過(guò)今天一篇文章,我們深度去了解了三次握手過(guò)程中內(nèi)核中的這些內(nèi)部操作。
全文洋洋灑灑上萬(wàn)字字,其實(shí)可以用一幅圖總結(jié)起來(lái)。

1. 服務(wù)器 listen 時(shí),計(jì)算了全/半連接隊(duì)列的長(zhǎng)度,還申請(qǐng)了相關(guān)內(nèi)存并初始化。 2. 客戶端 connect 時(shí),把本地 socket 狀態(tài)設(shè)置成了 TCP_SYN_SENT,選則一個(gè)可用的端口,發(fā)出 SYN 握手請(qǐng)求并啟動(dòng)重傳定時(shí)器。 3. 服務(wù)器響應(yīng) ack 時(shí),會(huì)判斷下接收隊(duì)列是否滿了,滿的話可能會(huì)丟棄該請(qǐng)求。否則發(fā)出 synack,申請(qǐng) request_sock 添加到半連接隊(duì)列中,同時(shí)啟動(dòng)定時(shí)器。 4. 客戶端響應(yīng) synack 時(shí),清除了 connect 時(shí)設(shè)置的重傳定時(shí)器,把當(dāng)前 socket 狀態(tài)設(shè)置為 ESTABLISHED,開(kāi)啟?;钣?jì)時(shí)器后發(fā)出第三次握手的 ack 確認(rèn)。 5. 服務(wù)器響應(yīng) ack 時(shí),把對(duì)應(yīng)半連接對(duì)象刪除,創(chuàng)建了新的 sock 后加入到全連接隊(duì)列中,最后將新連接狀態(tài)設(shè)置為 ESTABLISHED。 6. accept 從已經(jīng)建立好的全連接隊(duì)列中取出一個(gè)返回給用戶進(jìn)程。
另外要注意的是,如果握手過(guò)程中發(fā)生丟包(網(wǎng)絡(luò)問(wèn)題,或者是連接隊(duì)列溢出),內(nèi)核會(huì)等待定時(shí)器到期后重試,重試時(shí)間間隔在 3.10 版本里分別是 1s 2s 4s ...。在一些老版本里,比如 2.6 里,第一次重試時(shí)間是 3 秒。最大重試次數(shù)分別由 tcp_syn_retries 和 tcp_synack_retries 控制。
如果你的線上接口正常都是幾十毫秒內(nèi)返回,但偶爾出現(xiàn)了 1 s、或者 3 s 等這種偶發(fā)的響應(yīng)耗時(shí)變長(zhǎng)的問(wèn)題,那么你就要去定位一下看看是不是出現(xiàn)了握手包的超時(shí)重傳了。
以上就是三次握手中一些更詳細(xì)的內(nèi)部操作。如果你能在面試官面前講出來(lái)內(nèi)核的這些底層邏輯,我相信面試官一定會(huì)對(duì)你刮目相看的!
推薦閱讀:
