TCP源碼分析 - 三次握手之 Connect 過程
本文轉(zhuǎn)載自微信公眾號「Linux內(nèi)核那些事」,作者songsong001。轉(zhuǎn)載本文請聯(lián)系Linux內(nèi)核那些事公眾號。
本文主要分析 TCP 協(xié)議的實現(xiàn),但由于 TCP 協(xié)議比較復(fù)雜,所以分幾篇文章進行分析,這篇主要介紹 TCP 協(xié)議建立連接時的三次握手過程。
TCP 協(xié)議應(yīng)該是 TCP/IP 協(xié)議棧中最為復(fù)雜的一個協(xié)議(沒有之一),TCP 協(xié)議的復(fù)雜性來源于其面向連接和保證可靠傳輸。
如下圖所示,TCP 協(xié)議位于 TCP/IP 協(xié)議棧的第四層,也就是傳輸層,其建立在網(wǎng)絡(luò)層的 IP 協(xié)議。
但由于 IP 協(xié)議是一個無連接不可靠的協(xié)議,所以 TCP 協(xié)議要實現(xiàn)面向連接的可靠傳輸,就必須為每個 CS(Client - Server) 連接維護一個連接狀態(tài)。由此可知,TCP 協(xié)議的連接只是維護了一個連接狀態(tài),而非真正的連接。
由于本文主要介紹 Linux 內(nèi)核是怎么實現(xiàn) TCP 協(xié)議的,如果對 TCP 協(xié)議的原理不是很清楚的話,可以參考著名的《TCP/IP協(xié)議詳解》。
三次握手過程
我們知道,TCP 協(xié)議是建立在無連接的 IP 協(xié)議之上,而為了實現(xiàn)面向連接,TCP 協(xié)議使用了一種協(xié)商的方式來建立連接狀態(tài),稱為:三次握手。三次握手 的過程如下圖:
建立連接過程如下:
- 客戶端需要發(fā)送一個 SYN包 到服務(wù)端(包含了客戶端初始化序列號),并且將連接狀態(tài)設(shè)置為 SYN_SENT。
- 服務(wù)端接收到客戶端的 SYN包 后,需要回復(fù)一個 SYN+ACK包 給客戶端(包含了服務(wù)端初始化序列號),并且設(shè)置連接狀態(tài)為 SYN_RCVD。
- 客戶端接收到服務(wù)端的 SYN+ACK包 后,設(shè)置連接狀態(tài)為 ESTABLISHED(表示連接已經(jīng)建立),并且回復(fù)一個 ACK包 給服務(wù)端。
- 服務(wù)端接收到客戶端的 ACK包 后,將連接狀態(tài)設(shè)置為 ESTABLISHED(表示連接已經(jīng)建立)。
以上過程完成后,一個 TCP 連接就此建立完成。
TCP 頭部
要分析 TCP 協(xié)議就免不了要了解 TCP 協(xié)議頭部,我們通過下面的圖片來介紹 TCP 頭部的格式:
下面介紹一下 TCP 頭部各個字段的作用:
- 源端口號:用于指定本地程序綁定的端口。
- 目的端口號:用于指定遠端程序綁定的端口。
- 序列號:用于本地發(fā)送數(shù)據(jù)時所使用的序列號。
- 確認號:用于本地確認接收到遠端發(fā)送過來的數(shù)據(jù)序列號。
- 首部長度:指示 TCP 頭部的長度。
- 標(biāo)志位:用于指示 TCP 數(shù)據(jù)包的類型。
- 窗口大?。河糜诹髁靠刂疲硎具h端能夠接收數(shù)據(jù)的能力。
- 校驗和:用于校驗數(shù)據(jù)包是否在傳輸時損壞了。
- 緊急指針:一般比較少用,用于指定緊急數(shù)據(jù)的偏移量(URG 標(biāo)志位為1時有效)。
- 可選項:TCP的選項部分。
我們來看看 Linux 內(nèi)核怎么定義 TCP 頭部的結(jié)構(gòu),如下:
- struct tcphdr {
- __u16 source; // 源端口
- __u16 dest; // 目的端口
- __u32 seq; // 序列號
- __u32 ack_seq; // 確認號
- __u16 doff:4, // 頭部長度
- res1:4, // 保留
- res2:2, // 保留
- urg:1, // 是否包含緊急數(shù)據(jù)
- ack:1, // 是否ACK包
- psh:1, // 是否Push包
- rst:1, // 是否Reset包
- syn:1, // 是否SYN包
- fin:1; // 是否FIN包
- __u16 window; // 滑動窗口
- __u16 check; // 校驗和
- __u16 urg_ptr; // 緊急指針
- };
從上面的定義可知,結(jié)構(gòu) tcphdr 的各個字段與 TCP 頭部的各個字段一一對應(yīng)。
客戶端連接過程
一個 TCP 連接是由客戶端發(fā)起的,當(dāng)客戶端程序調(diào)用 connect() 系統(tǒng)調(diào)用時,就會與服務(wù)端程序建立一個 TCP 連接。connect() 系統(tǒng)調(diào)用的原型如下:
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
下面是 connect() 系統(tǒng)調(diào)用各個參數(shù)的作用:
- sockfd:由 socket() 系統(tǒng)調(diào)用創(chuàng)建的文件句柄。
- addr:指定要連接的遠端 IP 地址和端口。
- addrlen:指定參數(shù) addr 的長度。
當(dāng)客戶端調(diào)用 connect() 函數(shù)時,會觸發(fā)內(nèi)核調(diào)用 sys_connect() 內(nèi)核函數(shù),sys_connect() 函數(shù)實現(xiàn)如下:
- int sys_connect(int fd, struct sockaddr *uservaddr, int addrlen)
- {
- struct socket *sock;
- char address[MAX_SOCK_ADDR];
- int err;
- ...
- // 獲取文件句柄對應(yīng)的socket對象
- sock = sockfd_lookup(fd, &err);
- ...
- // 從用戶空間復(fù)制要連接的遠端IP地址和端口信息
- err = move_addr_to_kernel(uservaddr, addrlen, address);
- ...
- // 調(diào)用 inet_stream_connect() 函數(shù)完成連接操作
- err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,
- sock->file->f_flags);
- ...
- return err;
- }
sys_connect() 內(nèi)核函數(shù)主要完成 3 個步驟:
- 調(diào)用 sockfd_lookup() 函數(shù)獲取 fd 文件句柄對應(yīng)的 socket 對象。
- 調(diào)用 move_addr_to_kernel() 函數(shù)從用戶空間復(fù)制要連接的遠端 IP 地址和端口信息。
- 調(diào)用 inet_stream_connect() 函數(shù)完成連接操作。
我們繼續(xù)分析 inet_stream_connect() 函數(shù)的實現(xiàn):
- int inet_stream_connect(struct socket *sock, struct sockaddr * uaddr,
- int addr_len, int flags)
- {
- struct sock *sk = sock->sk;
- int err;
- ...
- if (sock->state == SS_CONNECTING) {
- ...
- } else {
- // 嘗試自動綁定一個本地端口
- if (inet_autobind(sk) != 0)
- return(-EAGAIN);
- ...
- // 調(diào)用 tcp_v4_connect() 進行連接操作
- err = sk->prot->connect(sk, uaddr, addr_len);
- if (err < 0)
- return(err);
- sock->state = SS_CONNECTING;
- }
- ...
- // 如果 socket 設(shè)置了非阻塞, 并且連接還沒建立, 那么返回 EINPROGRESS 錯誤
- if (sk->state != TCP_ESTABLISHED && (flags & O_NONBLOCK))
- return (-EINPROGRESS);
- // 等待連接過程完成
- if (sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) {
- inet_wait_for_connect(sk);
- if (signal_pending(current))
- return -ERESTARTSYS;
- }
- sock->state = SS_CONNECTED; // 設(shè)置socket的狀態(tài)為connected
- ...
- return(0);
- }
inet_stream_connect() 函數(shù)的主要操作有以下幾個步驟:
- 調(diào)用 inet_autobind() 函數(shù)嘗試自動綁定一個本地端口。
- 調(diào)用 tcp_v4_connect() 函數(shù)進行 TCP 協(xié)議的連接操作。
- 如果 socket 設(shè)置了非阻塞,并且連接還沒建立完成,那么返回 EINPROGRESS 錯誤。
- 調(diào)用 inet_wait_for_connect() 函數(shù)等待連接服務(wù)端操作完成。
- 設(shè)置 socket 的狀態(tài)為 SS_CONNECTED,表示連接已經(jīng)建立完成。
在上面的步驟中,最重要的是調(diào)用 tcp_v4_connect() 函數(shù)進行連接操作,我們來分析一下 tcp_v4_connect() 函數(shù)的實現(xiàn):
- int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
- {
- struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);
- struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
- struct sk_buff *buff;
- struct rtable *rt;
- u32 daddr, nexthop;
- int tmp;
- ...
- nexthop = daddr = usin->sin_addr.s_addr;
- ...
- // 1. 獲取發(fā)送數(shù)據(jù)的路由信息
- tmp = ip_route_connect(&rt, nexthop, sk->saddr,
- RT_TOS(sk->ip_tos)|RTO_CONN|sk->localroute,
- sk->bound_dev_if);
- ...
- dst_release(xchg(&sk->dst_cache, rt)); // 2. 設(shè)置sk的路由信息
- // 3. 申請一個skb數(shù)據(jù)包對象
- buff = sock_wmalloc(sk, (MAX_HEADER + sk->prot->max_header), 0, GFP_KERNEL);
- ...
- sk->dport = usin->sin_port; // 4. 設(shè)置目的端口
- sk->daddr = rt->rt_dst; // 5. 設(shè)置目的IP地址
- ...
- if (!sk->saddr)
- sk->saddr = rt->rt_src; // 6. 如果沒有指定源IP地址, 那么使用路由信息的源IP地址
- sk->rcv_saddr = sk->saddr;
- ...
- // 7. 初始化TCP序列號
- tp->write_seq = secure_tcp_sequence_number(sk->saddr, sk->daddr, sk->sport,
- usin->sin_port);
- ...
- // 8. 重置TCP最大報文段大小
- tp->mss_clamp = ~0;
- ...
- // 9. 調(diào)用 tcp_connect() 函數(shù)繼續(xù)進行連接操作
- tcp_connect(sk, buff, rt->u.dst.pmtu);
- return 0;
- }
tcp_v4_connect() 函數(shù)只是做一些連接前的準(zhǔn)備工作,如下:
- 調(diào)用 ip_route_connect() 函數(shù)獲取發(fā)送數(shù)據(jù)的路由信息,并且將路由信息保存到 socket 對象的路由緩存中。
- 調(diào)用 sock_wmalloc() 函數(shù)申請一個 skb 數(shù)據(jù)包對象。
- 設(shè)置 目的端口 和 目的 IP 地址。
- 如果沒有指定 源 IP 地址,那么使用路由信息中的 源 IP 地址。
- 調(diào)用 secure_tcp_sequence_number() 函數(shù)初始化 TCP 序列號。
- 重置 TCP 協(xié)議最大報文段的大小。
- 調(diào)用 tcp_connect() 函數(shù)發(fā)送 SYN包 給服務(wù)端程序。
由于 TCP三次握手 的第一步是由客戶端發(fā)送 SYN包 給服務(wù)端,所以我們主要關(guān)注 tcp_connect() 函數(shù)的實現(xiàn),其代碼如下:
- void tcp_connect(struct sock *sk, struct sk_buff *buff, int mtu)
- {
- struct dst_entry *dst = sk->dst_cache;
- struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);
- skb_reserve(buff, MAX_HEADER + sk->prot->max_header); // 保留所有的協(xié)議頭部空間
- tp->snd_wnd = 0;
- tp->snd_wl1 = 0;
- tp->snd_wl2 = tp->write_seq;
- tp->snd_una = tp->write_seq;
- tp->rcv_nxt = 0;
- sk->err = 0;
- // 設(shè)置TCP頭部長度
- tp->tcp_header_len = sizeof(struct tcphdr) +
- (sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
- ...
- tcp_sync_mss(sk, mtu); // 設(shè)置TCP報文段最大長度
- ...
- TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; // 設(shè)置SYN標(biāo)志為1(表示這是一個SYN包)
- TCP_SKB_CB(buff)->sacked = 0;
- TCP_SKB_CB(buff)->urg_ptr = 0;
- buff->csum = 0;
- TCP_SKB_CB(buff)->seq = tp->write_seq++; // 設(shè)置序列號
- TCP_SKB_CB(buff)->end_seq = tp->write_seq; // 設(shè)置確認號
- tp->snd_nxt = TCP_SKB_CB(buff)->end_seq;
- // 初始化滑動窗口的大小
- tp->window_clamp = dst->window;
- tcp_select_initial_window(sock_rspace(sk)/2, tp->mss_clamp,
- &tp->rcv_wnd, &tp->window_clamp,
- sysctl_tcp_window_scaling, &tp->rcv_wscale);
- ...
- tcp_set_state(sk, TCP_SYN_SENT); // 設(shè)置 socket 的狀態(tài)為 SYN_SENT
- // 調(diào)用 tcp_v4_hash() 函數(shù)把 socket 添加到 tcp_established_hash 哈希表中
- sk->prot->hash(sk);
- tp->rto = dst->rtt;
- tcp_init_xmit_timers(sk); // 設(shè)置超時重傳定時器
- ...
- // 把 skb 添加到 write_queue 隊列中, 用于重傳時使用
- __skb_queue_tail(&sk->write_queue, buff);
- TCP_SKB_CB(buff)->when = jiffies;
- ...
- // 調(diào)用 tcp_transmit_skb() 函數(shù)構(gòu)建 SYN 包發(fā)送給服務(wù)端程序
- tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));
- ...
- }
tcp_connect() 函數(shù)的實現(xiàn)雖然比較長,但是邏輯相對簡單,就是設(shè)置 TCP 頭部各個字段的值,然后把數(shù)據(jù)包發(fā)送給服務(wù)端。下面列出 tcp_connect() 函數(shù)主要的工作:
- 設(shè)置 TCP 頭部的 SYN 標(biāo)志位 為 1 (表示這是一個 SYN包)。
- 設(shè)置 TCP 頭部的序列號和確認號。
- 初始化滑動窗口的大小。
- 設(shè)置 socket 的狀態(tài)為 SYN_SENT,可參考上面三次握手的狀態(tài)圖。
- 調(diào)用 tcp_v4_hash() 函數(shù)把 socket 添加到 tcp_established_hash 哈希表中,用于通過 IP 地址和端口快速查找到對應(yīng)的 socket 對象。
- 設(shè)置超時重傳定時器。
- 把 skb 添加到 write_queue 隊列中, 用于超時重傳。
- 調(diào)用 tcp_transmit_skb() 函數(shù)構(gòu)建 SYN包 發(fā)送給服務(wù)端程序。
注意:Linux 內(nèi)核通過 tcp_established_hash 哈希表來保存所有的 TCP 連接 socket 對象,而哈希表的鍵值就是連接的 IP 和端口,所以可以通過連接的 IP 和端口從 tcp_established_hash 哈希表中快速找到對應(yīng)的 socket 連接。如下圖所示:
通過上面的分析,構(gòu)建 SYN包 并且發(fā)送給服務(wù)端是通過 tcp_transmit_skb() 函數(shù)完成的,所以我們來分析一下 tcp_transmit_skb() 函數(shù)的實現(xiàn):
- void tcp_transmit_skb(struct sock *sk, struct sk_buff *skb)
- {
- if (skb != NULL) {
- struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);
- struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
- int tcp_header_size = tp->tcp_header_len;
- struct tcphdr *th;
- ...
- // TCP頭部指針
- th = (struct tcphdr *)skb_push(skb, tcp_header_size);
- skb->h.th = th;
- skb_set_owner_w(skb, sk);
- // 構(gòu)建 TCP 協(xié)議頭部
- th->source = sk->sport; // 源端口
- th->dest = sk->dport; // 目標(biāo)端口
- th->seq = htonl(TCP_SKB_CB(skb)->seq); // 請求序列號
- th->ack_seq = htonl(tp->rcv_nxt); // 應(yīng)答序列號
- th->doff = (tcp_header_size >> 2); // 頭部長度
- th->res1 = 0;
- *(((__u8 *)th) + 13) = tcb->flags; // 設(shè)置TCP頭部的標(biāo)志位
- if (!(tcb->flags & TCPCB_FLAG_SYN))
- th->window = htons(tcp_select_window(sk)); // 滑動窗口大小
- th->check = 0; // 校驗和
- th->urg_ptr = ntohs(tcb->urg_ptr); // 緊急指針
- ...
- // 計算TCP頭部的校驗和
- tp->af_specific->send_check(sk, th, skb->len, skb);
- ...
- tp->af_specific->queue_xmit(skb); // 調(diào)用 ip_queue_xmit() 函數(shù)發(fā)送數(shù)據(jù)包
- }
- }
tcp_transmit_skb() 函數(shù)的實現(xiàn)相對簡單,就是構(gòu)建 TCP 協(xié)議頭部,然后調(diào)用 ip_queue_xmit() 函數(shù)將數(shù)據(jù)包交由 IP 協(xié)議發(fā)送出去。
至此,客戶端就發(fā)送了一個 SYN包 給服務(wù)端,也就是說,TCP 三次握手 的第一步已經(jīng)完成。