TCP源碼分析 - 三次握手之 connect 過程(基於 Linux-2.4.0已更新)


TCP源碼分析 - 三次握手之 connect 過程

本文主要分析 TCP 協議的實現,但由於 TCP 協議比較復雜,所以分幾篇文章進行分析,這篇主要介紹 TCP 協議建立連接時的三次握手過程。

TCP 協議應該是 TCP/IP 協議棧中最為復雜的一個協議(沒有之一),TCP 協議的復雜性來源於其面向連接和保證可靠傳輸。

如下圖所示,TCP 協議位於 TCP/IP 協議棧的第四層,也就是傳輸層,其建立在網絡層的 IP 協議。

image

但由於 IP 協議是一個無連接不可靠的協議,所以 TCP 協議要實現面向連接的可靠傳輸,就必須為每個 CS(Client - Server) 連接維護一個連接狀態。由此可知,TCP 協議的連接只是維護了一個連接狀態,而非真正的連接。

由於本文主要介紹 Linux 內核是怎么實現 TCP 協議的,如果對 TCP 協議的原理不是很清楚的話,可以參考著名的《TCP/IP協議詳解》。

三次握手過程

我們知道,TCP 協議是建立在無連接的 IP 協議之上,而為了實現面向連接,TCP 協議使用了一種協商的方式來建立連接狀態,稱為:三次握手三次握手 的過程如下圖:

image

建立連接過程如下:

  • 客戶端需要發送一個 SYN包 到服務端(包含了客戶端初始化序列號),並且將連接狀態設置為 SYN_SENT
  • 服務端接收到客戶端的 SYN包 后,需要回復一個 SYN+ACK包 給客戶端(包含了服務端初始化序列號),並且設置連接狀態為 SYN_RCVD
  • 客戶端接收到服務端的 SYN+ACK包 后,設置連接狀態為 ESTABLISHED(表示連接已經建立),並且回復一個 ACK包 給服務端。
  • 服務端接收到客戶端的 ACK包 后,將連接狀態設置為 ESTABLISHED(表示連接已經建立)。

以上過程完成后,一個 TCP 連接就此建立完成。

TCP 頭部

要分析 TCP 協議就免不了要了解 TCP 協議頭部,我們通過下面的圖片來介紹 TCP 頭部的格式:

image

下面介紹一下 TCP 頭部各個字段的作用:

  • 源端口號:用於指定本地程序綁定的端口。
  • 目的端口號:用於指定遠端程序綁定的端口。
  • 序列號:用於本地發送數據時所使用的序列號。
  • 確認號:用於本地確認接收到遠端發送過來的數據序列號。
  • 首部長度:指示 TCP 頭部的長度。
  • 標志位:用於指示 TCP 數據包的類型。
  • 窗口大小:用於流量控制,表示遠端能夠接收數據的能力。
  • 校驗和:用於校驗數據包是否在傳輸時損壞了。
  • 緊急指針:一般比較少用,用於指定緊急數據的偏移量(URG 標志位為1時有效)。
  • 可選項:TCP的選項部分。

我們來看看 Linux 內核怎么定義 TCP 頭部的結構,如下:

struct tcphdr {
    __u16   source;   // 源端口
    __u16   dest;     // 目的端口
    __u32   seq;      // 序列號
    __u32   ack_seq;  // 確認號
    __u16   doff:4,   // 頭部長度
            res1:4,   // 保留
            res2:2,   // 保留
            urg:1,    // 是否包含緊急數據
            ack:1,    // 是否ACK包
            psh:1,    // 是否Push包
            rst:1,    // 是否Reset包
            syn:1,    // 是否SYN包
            fin:1;    // 是否FIN包
    __u16   window;   // 滑動窗口
    __u16   check;    // 校驗和
    __u16   urg_ptr;  // 緊急指針
};

從上面的定義可知,結構 tcphdr 的各個字段與 TCP 頭部的各個字段一一對應。

客戶端連接過程

一個 TCP 連接是由客戶端發起的,當客戶端程序調用 connect() 系統調用時,就會與服務端程序建立一個 TCP 連接。connect() 系統調用的原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

下面是 connect() 系統調用各個參數的作用:

  • sockfd:由 socket() 系統調用創建的文件句柄。
  • addr:指定要連接的遠端 IP 地址和端口。
  • addrlen:指定參數 addr 的長度。

當客戶端調用 connect() 函數時,會觸發內核調用 sys_connect() 內核函數,sys_connect() 函數實現如下:

int sys_connect(int fd, struct sockaddr *uservaddr, int addrlen)
{
    struct socket *sock;
    char address[MAX_SOCK_ADDR];
    int err;
    ...
    // 獲取文件句柄對應的socket對象
    sock = sockfd_lookup(fd, &err);
    ...
    // 從用戶空間復制要連接的遠端IP地址和端口信息
    err = move_addr_to_kernel(uservaddr, addrlen, address);
    ...
    // 調用 inet_stream_connect() 函數完成連接操作
    err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,
                             sock->file->f_flags);
    ...
    return err;
}

sys_connect() 內核函數主要完成 3 個步驟:

  • 調用 sockfd_lookup() 函數獲取 fd 文件句柄對應的 socket 對象。
  • 調用 move_addr_to_kernel() 函數從用戶空間復制要連接的遠端 IP 地址和端口信息。
  • 調用 inet_stream_connect() 函數完成連接操作。

我們繼續分析 inet_stream_connect() 函數的實現:

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);
        ...
        // 調用 tcp_v4_connect() 進行連接操作
        err = sk->prot->connect(sk, uaddr, addr_len);
        if (err < 0)
            return(err);
        sock->state = SS_CONNECTING;
    }
    ...
    // 如果 socket 設置了非阻塞, 並且連接還沒建立, 那么返回 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; // 設置socket的狀態為connected
    ...
    return(0);
}

inet_stream_connect() 函數的主要操作有以下幾個步驟:

  • 調用 inet_autobind() 函數嘗試自動綁定一個本地端口。
  • 調用 tcp_v4_connect() 函數進行 TCP 協議的連接操作。
  • 如果 socket 設置了非阻塞,並且連接還沒建立完成,那么返回 EINPROGRESS 錯誤。
  • 調用 inet_wait_for_connect() 函數等待連接服務端操作完成。
  • 設置 socket 的狀態為 SS_CONNECTED,表示連接已經建立完成。

在上面的步驟中,最重要的是調用 tcp_v4_connect() 函數進行連接操作,我們來分析一下 tcp_v4_connect() 函數的實現:

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. 獲取發送數據的路由信息
    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. 設置sk的路由信息

    // 3. 申請一個skb數據包對象
    buff = sock_wmalloc(sk, (MAX_HEADER + sk->prot->max_header), 0, GFP_KERNEL);
    ...
    sk->dport = usin->sin_port; // 4. 設置目的端口
    sk->daddr = rt->rt_dst;     // 5. 設置目的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. 調用 tcp_connect() 函數繼續進行連接操作
    tcp_connect(sk, buff, rt->u.dst.pmtu);
    return 0;
}

tcp_v4_connect() 函數只是做一些連接前的准備工作,如下:

  • 調用 ip_route_connect() 函數獲取發送數據的路由信息,並且將路由信息保存到 socket 對象的路由緩存中。
  • 調用 sock_wmalloc() 函數申請一個 skb 數據包對象。
  • 設置 目的端口目的 IP 地址
  • 如果沒有指定 源 IP 地址,那么使用路由信息中的 源 IP 地址
  • 調用 secure_tcp_sequence_number() 函數初始化 TCP 序列號。
  • 重置 TCP 協議最大報文段的大小。
  • 調用 tcp_connect() 函數發送 SYN包 給服務端程序。

由於 TCP三次握手 的第一步是由客戶端發送 SYN包 給服務端,所以我們主要關注 tcp_connect() 函數的實現,其代碼如下:

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); // 保留所有的協議頭部空間

    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;
    // 設置TCP頭部長度
    tp->tcp_header_len = sizeof(struct tcphdr) +
                           (sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
    ...
    tcp_sync_mss(sk, mtu); // 設置TCP報文段最大長度
    ...
    TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; // 設置SYN標志為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++;   // 設置序列號
    TCP_SKB_CB(buff)->end_seq = tp->write_seq; // 設置確認號
    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); // 設置 socket 的狀態為 SYN_SENT

    // 調用 tcp_v4_hash() 函數把 socket 添加到 tcp_established_hash 哈希表中
    sk->prot->hash(sk);

    tp->rto = dst->rtt;
    tcp_init_xmit_timers(sk); // 設置超時重傳定時器
    ...
    // 把 skb 添加到 write_queue 隊列中, 用於重傳時使用
    __skb_queue_tail(&sk->write_queue, buff);
    TCP_SKB_CB(buff)->when = jiffies;
    ...
    // 調用 tcp_transmit_skb() 函數構建 SYN 包發送給服務端程序
    tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));
    ...
}

tcp_connect() 函數的實現雖然比較長,但是邏輯相對簡單,就是設置 TCP 頭部各個字段的值,然后把數據包發送給服務端。下面列出 tcp_connect() 函數主要的工作:

  • 設置 TCP 頭部的 SYN 標志位 為 1 (表示這是一個 SYN包)。
  • 設置 TCP 頭部的序列號和確認號。
  • 初始化滑動窗口的大小。
  • 設置 socket 的狀態為 SYN_SENT,可參考上面三次握手的狀態圖。
  • 調用 tcp_v4_hash() 函數把 socket 添加到 tcp_established_hash 哈希表中,用於通過 IP 地址和端口快速查找到對應的 socket 對象。
  • 設置超時重傳定時器。
  • 把 skb 添加到 write_queue 隊列中, 用於超時重傳。
  • 調用 tcp_transmit_skb() 函數構建 SYN包 發送給服務端程序。

注意:Linux 內核通過 tcp_established_hash 哈希表來保存所有的 TCP 連接 socket 對象,而哈希表的鍵值就是連接的 IP 和端口,所以可以通過連接的 IP 和端口從 tcp_established_hash 哈希表中快速找到對應的 socket 連接。如下圖所示:

image

通過上面的分析,構建 SYN包 並且發送給服務端是通過 tcp_transmit_skb() 函數完成的,所以我們來分析一下 tcp_transmit_skb() 函數的實現:

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);

        // 構建 TCP 協議頭部
        th->source = sk->sport;                // 源端口
        th->dest = sk->dport;                  // 目標端口
        th->seq = htonl(TCP_SKB_CB(skb)->seq); // 請求序列號
        th->ack_seq = htonl(tp->rcv_nxt);      // 應答序列號
        th->doff = (tcp_header_size >> 2);     // 頭部長度
        th->res1 = 0;
        *(((__u8 *)th) + 13) = tcb->flags;     // 設置TCP頭部的標志位

        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); // 調用 ip_queue_xmit() 函數發送數據包
    }
}

tcp_transmit_skb() 函數的實現相對簡單,就是構建 TCP 協議頭部,然后調用 ip_queue_xmit() 函數將數據包交由 IP 協議發送出去。

至此,客戶端就發送了一個 SYN包 給服務端,也就是說,TCP 三次握手 的第一步已經完成。

下一篇文章,我們將會分析 TCP 三次握手 的第二步,也就是服務端接收到客戶端發送過來的 SYN包 時對應的處理。


免責聲明!

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



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