TCP連接建立及相關socket深度探析


關於TCP協議

TCP/IP協議分層模型


 

 

  可以看到,TCP協議位於運輸層,TCP將用戶數據打包構成報文段,它發送數據時啟動一個定時器,另一端收到數據進行確認,對失序的數據重新排序,丟棄重復的數據。TCP提供一種面向連接的可靠的字節流服務,面向連接意味着兩個使用TCP的應用(B/S)在彼此交換數據之前,必須先建立一個TCP連接,類似於打電話過程,先撥號振鈴,等待對方說喂,然后應答。在一個TCP連接中,只有兩方彼此通信。

  TCP可靠性來自於:

  (1)應用數據被分成TCP最合適的發送數據塊
  (2)當TCP發送一個段之后,啟動一個定時器,等待目的點確認收到報文,如果不能及時收到一個確認,將重發這個報文。
  (3)當TCP收到連接端發來的數據,就會推遲幾分之一秒發送一個確認。
  (4)TCP將保持它首部和數據的檢驗和,這是一個端對端的檢驗和,目的在於檢測數據在傳輸過程中是否發生變化。(有錯誤,就不確認,發送端就會重發)
  (5)TCP是以IP報文來傳送,IP數據是無序的,TCP收到所有數據后進行排序,再交給應用層
  (6)IP數據報會重復,所以TCP會去重
  (7)TCP能提供流量控制,TCP連接的每一個地方都有固定的緩沖空間。TCP的接收端只允許另一端發送緩存區能接納的數據。
  (8)TCP對字節流不做任何解釋,對字節流的解釋由TCP連接的雙方應用層解釋。

TCP建立連接的過程(三次握手)


  TCP是一個面向連接的協議,無論哪一方向另一方發送數據之前,都必須先在雙方之間建立一條連接,所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個TCP連接時,需要客戶端和服務端總共發送3個包以確認連接的建立。在socket編程中,這一過程由客戶端執行connect來觸發,整個流程如下圖所示:

 

 

 

(1)第一次握手:Client將標志位SYN置為1,隨機產生一個值seq=J,並將該數據包發送給Server,Client進入SYN_SENT狀態,等待Server確認。

(2)第二次握手:Server收到數據包后由標志位SYN=1知道Client請求建立連接,Server將標志位SYN和ACK都置為1,ack=J+1,隨機產生一個值seq=K,並將該數據包發送給Client以確認連接請求,Server進入SYN_RCVD狀態。

(3)第三次握手:Client收到確認后,檢查ack是否為J+1,ACK是否為1,如果正確則將標志位ACK置為1,ack=K+1,並將該數據包發送給Server,Server檢查ack是否為K+1,ACK是否為1,如果正確則連接建立成功,Client和Server進入ESTABLISHED狀態,完成三次握手,隨后Client與Server之間可以開始傳輸數據了。

簡單來說,就是:

  1、建立連接時,客戶端發送SYN包(SYN=i)到服務器,並進入到SYN-SEND狀態,等待服務器確認;

  2、服務器收到SYN包,必須確認客戶的SYN(ack=i+1),同時自己也發送一個SYN包(SYN=k),即SYN+ACK包,此時服務器進入SYN-RECV狀態;

  3、客戶端收到服務器的SYN+ACK包,向服務器發送確認報ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手,客戶端與服務器開始傳送數據。

關於SYN泛洪攻擊


 

  在三次握手過程中,Server發送SYN-ACK之后,收到Client的ACK之前的TCP連接稱為半連接(half-open connect),此時Server處於SYN_RCVD狀態,當收到ACK后,Server轉入ESTABLISHED狀態。SYN攻擊就是Client在短時間內偽造大量不存在的IP地址,並向Server不斷地發送SYN包,Server回復確認包,並等待Client的確認,由於源地址是不存在的,因此,Server需要不斷重發直至超時,這些偽造的SYN包將產時間占用未連接隊列,導致正常的SYN請求因為隊列滿而被丟棄,從而引起網絡堵塞甚至系統癱瘓。SYN攻擊時一種典型的DDOS攻擊,檢測SYN攻擊的方式非常簡單,即當Server上有大量半連接狀態且源IP地址是隨機的,則可以斷定遭到SYN攻擊了,使用如下命令可以讓之現行:

netstat -nap | grep SYN_RECV

談了TCP協議連接建立的三次握手后,接下來再復習下linux socket協議棧的相關內容:

關於linux socket

linux網絡路徑


 

 

   從上圖中可以清晰地看到socket在網絡分層中的位置,由於socket接口應該划在應用層,而我們日常的網絡編程也都基本在應用層上,所以下面將從發送端和接收端兩個角度分別對應用層進行分析:

發送端

應用層


 

(1) Socket

   應用層的各種網絡應用程序基本上都是通過 Linux Socket 編程接口來和內核空間的網絡協議棧通信的。Linux Socket 是從 BSD Socket 發展而來的,它是 Linux 操作系統的重要組成部分之一,它是網絡應用程序的基礎。從層次上來說,它位於應用層,是操作系統為應用程序員提供的 API,通過它,應用程序可以訪問傳輸層協議。

  • socket 位於傳輸層協議之上,屏蔽了不同網絡協議之間的差異
  • socket 是網絡編程的入口,它提供了大量的系統調用,構成了網絡程序的主體
  • 在Linux系統中,socket 屬於文件系統的一部分,網絡通信可以被看作是對文件的讀取,使得我們對網絡的控制和對文件的控制一樣方便。

   

 

 TCP Socket 處理過程

(2) 應用層處理流程

  1. 網絡應用調用Socket API socket (int family, int type, int protocol) 創建一個 socket,該調用最終會調用 Linux system call socket() ,並最終調用 Linux Kernel 的 sock_create() 方法。該方法返回被創建好了的那個 socket 的 file descriptor。對於每一個 userspace 網絡應用創建的 socket,在內核中都有一個對應的 struct socketstruct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩沖隊列也被初始化完成;在收據收發過程中,每個 queue 中保存要發送或者接受的每個 packet 對應的 Linux 網絡棧 sk_buffer 數據結構的實例 skb。
  2. 對於 TCP socket 來說,應用調用 connect()API ,使得客戶端和服務器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。
  3. 應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
  4. sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message
  5. _sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。

    對於 TCP ,調用 tcp_sendmsg 函數。

接收端

應用層


 

  1. 每當用戶應用調用  read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,並被轉化為 sys_recvfrom 調用,然后調用 sock_recgmsg 函數。
  2. 對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。
  3. 對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。

TCP相關源碼深度分析及GDB跟蹤調試

虛擬機:ubuntu 16.04

內核版本:linux 5.0.1

編譯方式:x86-64

模擬器:qemu

基於系統:部署好TCP通信程序的Menu OS系統

相關目錄路徑:/net/ipv4、/net/socket.c

TCP相關系統接口定義


 

  再次跑起來之前已經部署好TCP通信程序的Menu OS系統,追蹤與TCP連接相關的socket、connect、listen、accept函數的系統調用:

首先以調試模式運行Menu OS系統:

cd kernel
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s

新打開一個命令行運行GDB進行Menu OS的調試:

cd kernel
file linux-5.0.1/vmlinux
target remote:1234

設置相應的斷點:

b __sys_socket
b __sys_connect
b __sys_listen
b __sys_accept4
info breakpoints

 

   發現相應的socket系統調用函數都在 net/socket.c目錄下,打開該目錄,分析源代碼,其中socket接口函數都定義在SYSCALL_DEFINE接口里,找到主要的相關SYSCALL_DEFINE定義如下:

 1 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
 2 {
 3     return __sys_socket(family, type, protocol);
 4 }
 5 
 6 SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
 7 {
 8     return __sys_bind(fd, umyaddr, addrlen);
 9 }
10 
11 SYSCALL_DEFINE2(listen, int, fd, int, backlog)
12 {
13     return __sys_listen(fd, backlog);
14 }
15 
16 
17 SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
18         int __user *, upeer_addrlen)
19 {
20     return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
21 }
22 
23 SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
24         int, addrlen)
25 {
26     return __sys_connect(fd, uservaddr, addrlen);
27 }
28 
29 SYSCALL_DEFINE3(getsockname, int, fd, struct sockaddr __user *, usockaddr,
30         int __user *, usockaddr_len)
31 {
32     return __sys_getsockname(fd, usockaddr, usockaddr_len);
33 }
34 
35 SYSCALL_DEFINE3(getpeername, int, fd, struct sockaddr __user *, usockaddr,
36         int __user *, usockaddr_len)
37 {
38     return __sys_getpeername(fd, usockaddr, usockaddr_len);
39 }
40 
41 
42 SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
43         unsigned int, flags)
44 {
45     return __sys_sendto(fd, buff, len, flags, NULL, 0);
46 }
47 
48 SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
49         unsigned int, flags)
50 {
51     return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
52 }

逐步分析TCP三次握手的系統級實現


 

TCP連接前的初始化過程


  

  1)調用__sys_socket, __sys_socket源碼如下:

int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    /* Check the SOCK_* constants for consistency.  */
    BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;

    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        return retval;

    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

  可以看到,__sys_socket調用了sock_createsock_map_fd函數;

       2)調用sock_create():創建socket結構,針對每種不同的family的socket結構的初始化,就需要調用不同的create函數來完成。對應於inet類型的地址來說,在網絡協議初始化時調用sock_register()函數中完成注冊的定義如下:

 struct net_proto_family inet_family_ops={
                PF_INET;
                inet_create
        };

所以inet協議最后會調用inet_create函數。

       3)調用inet_create: 初始化sock的狀態設置為SS_UNCONNECTED,申請一個新的sock結構,並且初始化socket的成員ops初始化為inet_stream_ops,而sock的成員prot初始化為tcp_prot。然后調用sock_init_data,將該socket結構的變量sock和sock類型的變量關聯起來。

inet_create函數源碼如下:

 1 static int inet_create(struct net *net, struct socket *sock, int protocol,int kern)
 2 {
 3 ...
 4      /* Look for the requested type/protocol pair. */
 5      lookup_protocol:
 6      err = -ESOCKTNOSUPPORT;
 7      rcu_read_lock();
 8 
 9            // TCP套接字、UDP套接字、原始套接字的inet_protosw實 例都在inetsw_array數組中定義,
10            //這些實例會調inet_register_protosw()注冊到inetsw中
11           //根據protocol查找要創建的套接字對應的四層傳輸協議。
12      list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
13            ...
14      }
15 
16            //如果沒有找到,則調用request_module()來嘗試加載協議所屬的模塊,正常情況下不會發生。
17      if (unlikely(err)) {
18              if (try_loading_module < 2) {
19                      rcu_read_unlock();
20 ...
21 }

   4)調用sock_map_fd()獲取一個未被使用的文件描述符,並且申請並初始化對應的file{}結構。

TCP連接的三次握手過程深度解析


 

  接下來通過閱讀TCP源代碼的方式,一步步地追蹤解析系統級TCP三次握手的詳細過程。首先從__sys_connect源碼開始,逐步向深處探究:

 1 int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
 2 {
 3     struct socket *sock;
 4     struct sockaddr_storage address;
 5     int err, fput_needed;
 6     //得到socket對象
 7     sock = sockfd_lookup_light(fd, &err, &fput_needed);
 8     if (!sock)
 9         goto out;
10     //將地址對象從用戶空間拷貝到內核空間
11     err = move_addr_to_kernel(uservaddr, addrlen, &address);
12     if (err < 0)
13         goto out_put;
14     //內核相關
15     err =
16         security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
17     if (err)
18         goto out_put;
19     //對於流式套接字,sock->ops為 inet_stream_ops -->inet_stream_connect
20 
21     //對於數據報套接字,sock->ops為 inet_dgram_ops --> inet_dgram_connect
22     err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
23                  sock->file->f_flags);
24 out_put:
25     fput_light(sock->file, fput_needed);
26 out:
27     return err;
28 }               

在該函數中做了三件事:

  1. 根據文件描述符找到指定的socket對象;

  2. 將地址信息從用戶空間拷貝到內核空間;

  3. 調用指定類型套接字的connect函數。

對應流式套接字的connect函數是inet_stream_connect,接着我們分析該函數:

在GDB中設置斷點找到源文件:

 1 int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
 2             int addr_len, int flags)
 3 {
 4     int err;
 5  
 6     lock_sock(sock->sk);
 7     err = __inet_stream_connect(sock, uaddr, addr_len, flags);
 8     release_sock(sock->sk);
 9     return err;
10 }
11  
12 /*
13  *    Connect to a remote host. There is regrettably still a little
14  *    TCP 'magic' in here.
15  */
16  
17 //1. 檢查socket地址長度和使用的協議族。
18 //2. 檢查socket的狀態,必須是SS_UNCONNECTED或SS_CONNECTING。
19 //3. 調用tcp_v4_connect()來發送SYN包。
20 //4. 等待后續握手的完成:
21 int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
22               int addr_len, int flags)
23 ...
24 后面太多便不再展示,可直接看源碼

該函數主要做了幾件事:

  1. 檢查socket地址長度和使用的協議族;
  2. 檢查socket的狀態,必須是SS_UNCONNECTED或SS_CONNECTING;
  3. 調用實現協議的connect函數,對於流式套接字,實現協議是tcp,調用的是tcp_v4_connect();

  4.對於阻塞調用,等待后續握手的完成;對於非阻塞調用,則直接返回 -EINPROGRESS

我們先關注tcp_v4_connect,同樣先在gdb中設置斷點,找到源文件所在位置:

 1 /* This will initiate an outgoing connection. */
 2  
 3 //對於TCP 協議來說,其連接實際上就是發送一個 SYN 報文,在服務器的應答到來時,回答它一個 ack 報文,也就是完成三次握手中的第一和第三次
 4 int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
 5 {
 6     struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
 7     struct inet_sock *inet = inet_sk(sk);
 8     struct tcp_sock *tp = tcp_sk(sk);
 9     __be16 orig_sport, orig_dport;
10     __be32 daddr, nexthop;
11     struct flowi4 *fl4;
12     struct rtable *rt;
13     int err;
14     struct ip_options_rcu *inet_opt;
15  
16     if (addr_len < sizeof(struct sockaddr_in))
17         return -EINVAL;
18  
19     if (usin->sin_family != AF_INET)
20         return -EAFNOSUPPORT;
21  
22     nexthop = daddr = usin->sin_addr.s_addr;
23     inet_opt = rcu_dereference_protected(inet->inet_opt,
24                          lockdep_sock_is_held(sk));
25  
26     //將下一跳地址和目的地址的臨時變量都暫時設為用戶提交的地址。 
27     if (inet_opt && inet_opt->opt.srr) {
28         if (!daddr)
29             return -EINVAL;
30         nexthop = inet_opt->opt.faddr;
31     }
32  
33     //源端口
34     orig_sport = inet->inet_sport;
35  
36     //目的端口
37     orig_dport = usin->sin_port;
38     
39     fl4 = &inet->cork.fl.u.ip4;
40  
41     //如果使用了來源地址路由,選擇一個合適的下一跳地址。 
42     rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
43                   RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
44                   IPPROTO_TCP,
45                   orig_sport, orig_dport, sk);
46     if (IS_ERR(rt)) {
47         err = PTR_ERR(rt);
48         if (err == -ENETUNREACH)
49             IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
50         return err;
51     }
52 ...
53 后面較長,不再展示。

在該函數主要完成:

  1. 路由查找,得到下一跳地址,並更新socket對象的下一跳地址;

  2. 將socket對象的狀態設置為TCP_SYN_SENT;

  3. 如果沒設置序號初值,則選定一個隨機初值;

  4. 調用函數tcp_connect完成報文構建和發送。

我接着看下tcp_connect

 1 /* Build a SYN and send it off. */
 2 //由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()發送,並置為TCP_SYN_SENT.
 3 int tcp_connect(struct sock *sk)
 4 {
 5     struct tcp_sock *tp = tcp_sk(sk);
 6     struct sk_buff *buff;
 7     int err;
 8  
 9     //初始化傳輸控制塊中與連接相關的成員
10     tcp_connect_init(sk);
11  
12     if (unlikely(tp->repair)) {
13         tcp_finish_connect(sk, NULL);
14         return 0;
15     }
16     //分配skbuff   --> 為SYN段分配報文並進行初始化
17     buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
18     if (unlikely(!buff))
19         return -ENOBUFS;
20  
21     //構建syn報文
22     
23     //在函數tcp_v4_connect中write_seq已經被初始化隨機值
24     tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
25     
26     tp->retrans_stamp = tcp_time_stamp;
27  
28     //將報文添加到發送隊列上
29     tcp_connect_queue_skb(sk, buff);
30  
31     //顯式擁塞通告 ---> 
32     //路由器在出現擁塞時通知TCP。當TCP段傳遞時,路由器使用IP首部中的2位來記錄擁塞,當TCP段到達后,
33     //接收方知道報文段是否在某個位置經歷過擁塞。然而,需要了解擁塞發生情況的是發送方,而非接收方。因
34     //此,接收方使用下一個ACK通知發送方有擁塞發生,然后,發送方做出響應,縮小自己的擁塞窗口。
35     tcp_ecn_send_syn(sk, buff);
36  
37     /* Send off SYN; include data in Fast Open. */
38     err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
39  
40           //構造tcp頭和ip頭並發送
41           tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
42     if (err == -ECONNREFUSED)
43         return err;
44  
45     /* We change tp->snd_nxt after the tcp_transmit_skb() call
46      * in order to make this packet get counted in tcpOutSegs.
47      */
48     tp->snd_nxt = tp->write_seq;
49     tp->pushed_seq = tp->write_seq;
50     TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
51  
52     /* Timer for repeating the SYN until an answer. */
53  
54     //啟動重傳定時器
55     inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
56                   inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
57     return 0;
58 }

該函數完成:

  1. 初始化套接字跟連接相關的字段;

  2.  申請sk_buff空間;

  3 . 將sk_buff初始化為syn報文,實質是操作tcp_skb_cb,在初始化TCP頭的時候會用到;

  4 . 調用tcp_connect_queue_skb()函數將報文sk_buff添加到發送隊列sk->sk_write_queue;

  5 . 調用tcp_transmit_skb()函數構造tcp頭,然后交給網絡層;

  6.  初始化重傳定時器。

接着我們進入tcp_connect_queue_skb

 1 /* This routine actually transmits TCP packets queued in by
 2  * tcp_do_sendmsg().  This is used by both the initial
 3  * transmission and possible later retransmissions.
 4  * All SKB's seen here are completely headerless.  It is our
 5  * job to build the TCP header, and pass the packet down to
 6  * IP so it can do the same plus pass the packet off to the
 7  * device.
 8  *
 9  * We are working here with either a clone of the original
10  * SKB, or a fresh unique copy made by the retransmit engine.
11  */
12 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
13                 gfp_t gfp_mask)
14 {
15     const struct inet_connection_sock *icsk = inet_csk(sk);
16     struct inet_sock *inet;
17     struct tcp_sock *tp;
18     struct tcp_skb_cb *tcb;
19     struct tcp_out_options opts;
20     unsigned int tcp_options_size, tcp_header_size;
21     struct tcp_md5sig_key *md5;
22     struct tcphdr *th;
23     int err;
24  
25     BUG_ON(!skb || !tcp_skb_pcount(skb));
26     tp = tcp_sk(sk);
27  
28     //根據傳遞進來的clone_it參數來確定是否需要克隆待發送的報文
29     if (clone_it) {
30         skb_mstamp_get(&skb->skb_mstamp);
31         TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq
32             - tp->snd_una;
33         tcp_rate_skb_sent(sk, skb);
34  
35         //如果一個SKB會被不同的用戶獨立操作,而這些用戶可能只是修改SKB描述符中的某些字段值,如h、nh,則內核沒有必要為每個用戶復制一份完整
36         //的SKB描述及其相應的數據緩存區,而會為了提高性能,只作克隆操作。克隆過程只復制SKB描述符,同時增加數據緩存區的引用計數,以免共享數
37         //據被提前釋放。完成這些功能的是skb_clone()。一個使用包克隆的場景是,一個接收包程序要把該包傳遞給多個接收者,例如包處理函數或者一
38         //個或多個網絡模塊。原始的及克隆的SKB描述符的cloned值都會被設置為1,克隆SKB描述符的users值置為1,這樣在第一次釋放時就會釋放掉。同時
39         //將數據緩存區引用計數dataref遞增1,因為又多了一個克隆SKB描述符指向它
40         
41         if (unlikely(skb_cloned(skb)))
42             //如果skb已經被clone,則只能復制該skb的數據到新分配的skb中
43             skb = pskb_copy(skb, gfp_mask);
44         else
45             skb = skb_clone(skb, gfp_mask);
46         if (unlikely(!skb))
47             return -ENOBUFS;
48     }
49 ...
50 后面較長,不再展示。

  可以看到主要是移動sk_buff的data指針,然后填充TCP頭,接着的事就是交給網絡層,將報文發出。這樣三次握手中的第一次握手在客戶端的層面完成,報文到達服務端,由服務端處理完畢后,第一次握手完成,客戶端socket狀態變為TCP_SYN_SENT。下面我們看下服務端的處理

  數據到達網卡的時候,對於TCP協議,將大致要經過這個一個調用鏈:

  網卡驅動 ---> netif_receive_skb() ---> ip_rcv() ---> ip_local_deliver_finish() ---> tcp_v4_rcv()

我們直接看tcp_v4_rcv():

 1 /*
 2  *    From tcp_input.c
 3  */
 4  
 5 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
 6 int tcp_v4_rcv(struct sk_buff *skb)
 7 {
 8     struct net *net = dev_net(skb->dev);
 9     const struct iphdr *iph;
10     const struct tcphdr *th;
11     bool refcounted;
12     struct sock *sk;
13     int ret;
14  
15     //如果不是發往本地的數據包,則直接丟棄
16     if (skb->pkt_type != PACKET_HOST)
17         goto discard_it;
18  
19     /* Count it even if it's bad */
20     __TCP_INC_STATS(net, TCP_MIB_INSEGS);
21  
22  
23     ////包長是否大於TCP頭的長度
24     if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
25         goto discard_it;
26  
27     //tcp頭   --> 不是很懂為何老是獲取tcp頭
28     th = (const struct tcphdr *)skb->data;
29 ...
30 后面較長,不再展示。

  該函數主要工作就是根據tcp頭部信息查到處理報文的socket對象,然后檢查socket狀態做不同處理,我們這里是監聽狀態TCP_LISTEN,直接調用函數tcp_v4_do_rcv():

 1 /* The socket must have it's spinlock held when we get
 2  * here, unless it is a TCP_LISTEN socket.
 3  *
 4  * We have a potential double-lock case here, so even when
 5  * doing backlog processing we use the BH locking scheme.
 6  * This is because we cannot sleep with the original spinlock
 7  * held.
 8  */
 9  
10 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
11  
12  
13 //tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack()
14 int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
15 {
16     struct sock *rsk;
17  
18    //如果是連接已建立狀態 
19     if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
20         struct dst_entry *dst = sk->sk_rx_dst;
21  
22         sock_rps_save_rxhash(sk, skb);
23         sk_mark_napi_id(sk, skb);
24         if (dst) {
25             if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
26                 !dst->ops->check(dst, 0)) {
27                 dst_release(dst);
28                 sk->sk_rx_dst = NULL;
29             }
30         }
31         tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
32         return 0;
33     }
34 ...
35 后面較長,不再展示。

  在這里並沒有很多代碼,做的東西也不是很多,對於監聽狀態的套接字,主要是一個SYN FLOOD防范相關的東西,不是我們研究的重點;接着就是調用tcp_rcv_state_process():

 1 /*
 2  *    This function implements the receiving procedure of RFC 793 for
 3  *    all states except ESTABLISHED and TIME_WAIT.
 4  *    It's called from both tcp_v4_rcv and tcp_v6_rcv and should be
 5  *    address independent.
 6  */
 7  
 8  
 9 //除了ESTABLISHED和TIME_WAIT狀態外,其他狀態下的TCP段處理都由本函數實現
10  
11 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().        
12 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
13 {
14     struct tcp_sock *tp = tcp_sk(sk);
15     struct inet_connection_sock *icsk = inet_csk(sk);
16     const struct tcphdr *th = tcp_hdr(skb);
17     struct request_sock *req;
18     int queued = 0;
19     bool acceptable;
20  
21     switch (sk->sk_state) {
22         
23     //SYN_RECV狀態的處理 
24     case TCP_CLOSE:
25         goto discard;
26  
27     //服務端第一次握手處理
28     case TCP_LISTEN:
29         if (th->ack)
30             return 1;
31  
32         if (th->rst)
33             goto discard;
34  
35         if (th->syn) {
36             if (th->fin)
37                 goto discard;
38             // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().        
39             if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
40                 return 1;
41  
42             consume_skb(skb);
43             return 0;
44         }
45         goto discard;
46 
47 ...

  這是TCP建立連接的核心所在,幾乎所有狀態的套接字,在收到數據報時都在這里完成處理。對於服務端來說,收到第一次握手報文時的狀態為TCP_LISTEN,處理代碼為:

 1 //服務端第一次握手處理
 2     case TCP_LISTEN:
 3         if (th->ack)
 4             return 1;
 5  
 6         if (th->rst)
 7             goto discard;
 8  
 9         if (th->syn) {
10             if (th->fin)
11                 goto discard;
12             // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().        
13             if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
14                 return 1;
15  
16             consume_skb(skb);
17             return 0;
18         }
19         goto discard;

  接下將由tcp_v4_conn_request函數處理,而tcp_v4_conn_request實際上調用tcp_conn_request

 1 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().        
 2 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
 3 {
 4     /* Never answer to SYNs send to broadcast or multicast */
 5     if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
 6         goto drop;
 7  
 8     //tcp_request_sock_ops 定義在 tcp_ipv4.c    1256行
 9  
10     //inet_init --> proto_register --> req_prot_init -->初始化cache名
11     return tcp_conn_request(&tcp_request_sock_ops,
12                 &tcp_request_sock_ipv4_ops, sk, skb);
13  
14 drop:
15     tcp_listendrop(sk);
16     return 0;
17 }
18 int tcp_conn_request(struct request_sock_ops *rsk_ops,
19              const struct tcp_request_sock_ops *af_ops,
20              struct sock *sk, struct sk_buff *skb)
21 ...

在該函數中做了不少的事情,但是我們這里重點了解兩點:

  1. 分配一個request_sock對象來代表這次連接請求(狀態為TCP_NEW_SYN_RECV),如果沒有設置防范syn  flood相關的選項,則將該request_sock添加到established狀態的tcp_sock散列表(如果設置了防范選項,則request_sock對象都沒有,只有建立完成時才會分配);

  2. 調用tcp_v4_send_synack回復客戶端ack開啟第二次握手

我們看下該函數:

 1 //向客戶端發送SYN+ACK報文
 2 static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
 3                   struct flowi *fl,
 4                   struct request_sock *req,
 5                   struct tcp_fastopen_cookie *foc,
 6                   enum tcp_synack_type synack_type)
 7 {
 8     const struct inet_request_sock *ireq = inet_rsk(req);
 9     struct flowi4 fl4;
10     int err = -1;
11     struct sk_buff *skb;
12  
13     /* First, grab a route. */
14  
15     //查找到客戶端的路由
16     if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL)
17         return -1;
18  
19     //根據路由、傳輸控制塊、連接請求塊中的構建SYN+ACK段
20     skb = tcp_make_synack(sk, dst, req, foc, synack_type);
21  
22     //生成SYN+ACK段成功
23     if (skb) {
24  
25         //生成校驗碼
26         __tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
27  
28  
29         //生成IP數據報並發送出去
30         err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
31                         ireq->ir_rmt_addr,
32                         ireq->opt);
33         err = net_xmit_eval(err);
34     }
35  
36     return err;
37 }

  代碼較少,查找客戶端路由,構造syn包,然后調用ip_build_and_send_pkt,依靠網絡層將數據報發出去。至此,第一次握手完成,第二次握手服務端層面完成。

  數據報到達客戶端網卡,同樣經過:網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() --> tcp_v4_do_rcv()

  客戶端socket的狀態為TCP_SYN_SENT,所以直接進入tcp_rcv_state_process,處理該狀態的代碼為:

 1 //客戶端第二次握手處理 
 2     case TCP_SYN_SENT:
 3         tp->rx_opt.saw_tstamp = 0;
 4  
 5         //處理SYN_SENT狀態下接收到的TCP段
 6         queued = tcp_rcv_synsent_state_process(sk, skb, th);
 7         if (queued >= 0)
 8             return queued;
 9  
10         /* Do step6 onward by hand. */
11  
12         //處理完第二次握手后,還需要處理帶外數據
13         tcp_urg(sk, skb, th);
14         __kfree_skb(skb);
15  
16         //檢測是否有數據需要發送
17         tcp_data_snd_check(sk);
18         return 0;
19     }

  接着看tcp_rcv_synsent_state_process:

 1 //在SYN_SENT狀態下處理接收到的段,但是不處理帶外數據 
 2 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
 3                      const struct tcphdr *th)
 4 {
 5     struct inet_connection_sock *icsk = inet_csk(sk);
 6     struct tcp_sock *tp = tcp_sk(sk);
 7     struct tcp_fastopen_cookie foc = { .len = -1 };
 8     int saved_clamp = tp->rx_opt.mss_clamp;
 9  
10     //解析TCP選項並保存到傳輸控制塊中
11     tcp_parse_options(skb, &tp->rx_opt, 0, &foc);
12     if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr)
13         tp->rx_opt.rcv_tsecr -= tp->tsoffset;
14 
15 ...

處理三種可能的包:

  1. 帶ack標志的,這是我們預期的;

  2. 帶rst標志的,直接丟掉傳輸控制塊;

  3. 帶syn標志,但是沒有ack標志,兩者同時發起連接。

我們重點研究第一種情況。首先調用tcp_finish_connect設置sock狀態為TCP_ESTABLISHED

 1 void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct tcp_sock *tp = tcp_sk(sk);
 4     struct inet_connection_sock *icsk = inet_csk(sk);
 5  
 6     //設置sock狀態為TCP_ESTABLISHED
 7     tcp_set_state(sk, TCP_ESTABLISHED);
 8  
 9     if (skb) {
10         icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
11         security_inet_conn_established(sk, skb);
12     }
13 ...

  接着延時發送或立即發送確認ack,我們先不去了解延時確認的東西,我們直接看直接發送確認ack,調用tcp_send_ack

 1 //主動連接時,向服務器端發送ACK完成連接,並更新窗口
 2 void tcp_send_ack(struct sock *sk)
 3 {
 4     struct sk_buff *buff;
 5  
 6     /* If we have been reset, we may not send again. */
 7     if (sk->sk_state == TCP_CLOSE)
 8         return;
 9  
10     tcp_ca_event(sk, CA_EVENT_NON_DELAYED_ACK);
11 ...

  比較簡單,無非是構造報文,然后交給網絡層發送。至此第二次握手完成,客戶端sock狀態變為TCP_ESTABLISHED第三次握手開始。我們之前說到服務端的sock的狀態為TCP_NEW_SYN_RECV,報文到達網卡:

  網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv(),報文將被以下代碼處理:

 1 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
 2 int tcp_v4_rcv(struct sk_buff *skb)
 3 {
 4 .............
 5  
 6  
 7     //收到握手最后一個ack后,會找到TCP_NEW_SYN_RECV狀態的req,然后創建一個新的sock進入TCP_SYN_RECV狀態,最終進入TCP_ESTABLISHED狀態. 並放入accept隊列通知select/epoll
 8     if (sk->sk_state == TCP_NEW_SYN_RECV) {
 9         struct request_sock *req = inet_reqsk(sk);
10         struct sock *nsk;
11  
12         sk = req->rsk_listener;
13         if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
14             sk_drops_add(sk, skb);
15             reqsk_put(req);
16             goto discard_it;
17         }
18 ...

  看下如何創建新sock,進入tcp_check_req():

 1 struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
 2                struct request_sock *req,
 3                bool fastopen)
 4 {
 5     struct tcp_options_received tmp_opt;
 6     struct sock *child;
 7     const struct tcphdr *th = tcp_hdr(skb);
 8     __be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK);
 9     bool paws_reject = false;
10     bool own_req;
11 ...

  又回到了函數tcp_rcv_state_process,TCP_SYN_RECV狀態的套接字將由一下代碼處理:

 1 //服務端第三次握手處理
 2     case TCP_SYN_RECV:
 3         if (!acceptable)
 4             return 1;
 5  
 6         if (!tp->srtt_us)
 7             tcp_synack_rtt_meas(sk, req);
 8  
 9         /* Once we leave TCP_SYN_RECV, we no longer need req
10          * so release it.
11          */
12         if (req) {
13             inet_csk(sk)->icsk_retransmits = 0;
14             reqsk_fastopen_remove(sk, req, false);
15         } else {
16             /* Make sure socket is routed, for correct metrics. */
17  
18             //建立路由,初始化擁塞控制模塊
19             icsk->icsk_af_ops->rebuild_header(sk);
20             tcp_init_congestion_control(sk);
21  
22             tcp_mtup_init(sk);
23             tp->copied_seq = tp->rcv_nxt;
24             tcp_init_buffer_space(sk);
25         }
26         smp_mb();
27         //正常的第三次握手,設置連接狀態為TCP_ESTABLISHED 
28         tcp_set_state(sk, TCP_ESTABLISHED);
29         sk->sk_state_change(sk);
30  
31         /* Note, that this wakeup is only for marginal crossed SYN case.
32          * Passively open sockets are not waked up, because
33          * sk->sk_sleep == NULL and sk->sk_socket == NULL.
34          */
35  
36         //狀態已經正常,喚醒那些等待的線程
37         if (sk->sk_socket)
38             sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
39  
40         tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
41         tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
42         tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
43  
44         if (tp->rx_opt.tstamp_ok)
45             tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
46  
47         if (req) {
48             /* Re-arm the timer because data may have been sent out.
49              * This is similar to the regular data transmission case
50              * when new data has just been ack'ed.
51              *
52              * (TFO) - we could try to be more aggressive and
53              * retransmitting any data sooner based on when they
54              * are sent out.
55              */
56             tcp_rearm_rto(sk);
57         } else
58             tcp_init_metrics(sk);
59  
60         if (!inet_csk(sk)->icsk_ca_ops->cong_control)
61             tcp_update_pacing_rate(sk);
62  
63         /* Prevent spurious tcp_cwnd_restart() on first data packet */
64  
65         //更新最近一次發送數據包的時間
66         tp->lsndtime = tcp_time_stamp;
67  
68         tcp_initialize_rcv_mss(sk);
69  
70         //計算有關TCP首部預測的標志
71         tcp_fast_path_on(tp);
72         break;

  可以看到到代碼對sock的窗口,mss等進行設置,以及最后將sock的狀態設置為TCP_ESTABLISHED至此三次握手完成。等待用戶調用accept調用,取出套接字使用。

分析過程中的斷點設置情況如下圖:

 

 

現在給出大體的TCP三次握手協議棧從上至下提供的接口,具體的詳細過程見上面的分析:

TCP三次握手協議棧從上至下提供的接口


 

  如下圖所示:

 總結


 

   本文內容主要分為三個部分,首先講述了TCP協議的相關知識,接着談到了與TCP通信聯系密切的linux socket接口函數及應用層處理TCP連接的詳細流程,最后通過gdb調試加閱讀源碼的方式,一步步揭開socket接口函數背后的TCP三次握手的神秘面紗,詳細地從linux 系統函數調用的角度理解了TCP連接建立三次握手的過程,相信我們對TCP協議的理解又有了一個新的高度!

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM