<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 9383字,需瀏覽 19分鐘

           ·

          2021-03-09 19:19

          本文主要分析 TCP 協(xié)議的實(shí)現(xiàn),但由于 TCP 協(xié)議比較復(fù)雜,所以分幾篇文章進(jìn)行分析,這篇主要介紹 TCP 協(xié)議建立連接時(shí)的三次握手過程。

          TCP 協(xié)議應(yīng)該是 TCP/IP 協(xié)議棧中最為復(fù)雜的一個(gè)協(xié)議(沒有之一),TCP 協(xié)議的復(fù)雜性來源于其面向連接和保證可靠傳輸。

          如下圖所示,TCP 協(xié)議位于 TCP/IP 協(xié)議棧的第四層,也就是傳輸層,其建立在網(wǎng)絡(luò)層的 IP 協(xié)議。

          f2c36ffd395324374f05c9197a205674.webp

          但由于 IP 協(xié)議是一個(gè)無連接不可靠的協(xié)議,所以 TCP 協(xié)議要實(shí)現(xiàn)面向連接的可靠傳輸,就必須為每個(gè) CS(Client - Server) 連接維護(hù)一個(gè)連接狀態(tài)。由此可知,TCP 協(xié)議的連接只是維護(hù)了一個(gè)連接狀態(tài),而非真正的連接。

          由于本文主要介紹 Linux 內(nèi)核是怎么實(shí)現(xiàn) TCP 協(xié)議的,如果對(duì) TCP 協(xié)議的原理不是很清楚的話,可以參考著名的《TCP/IP協(xié)議詳解》。

          三次握手過程

          我們知道,TCP 協(xié)議是建立在無連接的 IP 協(xié)議之上,而為了實(shí)現(xiàn)面向連接,TCP 協(xié)議使用了一種協(xié)商的方式來建立連接狀態(tài),稱為:三次握手。三次握手?的過程如下圖:

          ba33090ee549692e173ad77c0bccce36.webp

          建立連接過程如下:

          • 客戶端需要發(fā)送一個(gè)?SYN包?到服務(wù)端(包含了客戶端初始化序列號(hào)),并且將連接狀態(tài)設(shè)置為?SYN_SENT。

          • 服務(wù)端接收到客戶端的?SYN包?后,需要回復(fù)一個(gè)?SYN+ACK包?給客戶端(包含了服務(wù)端初始化序列號(hào)),并且設(shè)置連接狀態(tài)為?SYN_RCVD。

          • 客戶端接收到服務(wù)端的?SYN+ACK包?后,設(shè)置連接狀態(tài)為?ESTABLISHED(表示連接已經(jīng)建立),并且回復(fù)一個(gè)?ACK包?給服務(wù)端。

          • 服務(wù)端接收到客戶端的?ACK包?后,將連接狀態(tài)設(shè)置為?ESTABLISHED(表示連接已經(jīng)建立)。

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

          TCP 頭部

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

          1a576643b87154d3b1adc689f21e70a7.webp

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

          • 源端口號(hào):用于指定本地程序綁定的端口。

          • 目的端口號(hào):用于指定遠(yuǎn)端程序綁定的端口。

          • 序列號(hào):用于本地發(fā)送數(shù)據(jù)時(shí)所使用的序列號(hào)。

          • 確認(rèn)號(hào):用于本地確認(rèn)接收到遠(yuǎn)端發(fā)送過來的數(shù)據(jù)序列號(hào)。

          • 首部長(zhǎng)度:指示 TCP 頭部的長(zhǎng)度。

          • 標(biāo)志位:用于指示 TCP 數(shù)據(jù)包的類型。

          • 窗口大小:用于流量控制,表示遠(yuǎn)端能夠接收數(shù)據(jù)的能力。

          • 校驗(yàn)和:用于校驗(yàn)數(shù)據(jù)包是否在傳輸時(shí)損壞了。

          • 緊急指針:一般比較少用,用于指定緊急數(shù)據(jù)的偏移量(URG?標(biāo)志位為1時(shí)有效)。

          • 可選項(xiàng):TCP的選項(xiàng)部分。

          我們來看看 Linux 內(nèi)核怎么定義 TCP 頭部的結(jié)構(gòu),如下:

          struct tcphdr {    __u16   source;   // 源端口    __u16   dest;     // 目的端口    __u32   seq;      // 序列號(hào)    __u32   ack_seq;  // 確認(rèn)號(hào)    __u16   doff:4,   // 頭部長(zhǎng)度            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;   // 滑動(dòng)窗口    __u16   check;    // 校驗(yàn)和    __u16   urg_ptr;  // 緊急指針};

          從上面的定義可知,結(jié)構(gòu)?tcphdr?的各個(gè)字段與 TCP 頭部的各個(gè)字段一一對(duì)應(yīng)。

          客戶端連接過程

          一個(gè) TCP 連接是由客戶端發(fā)起的,當(dāng)客戶端程序調(diào)用?connect()?系統(tǒng)調(diào)用時(shí),就會(huì)與服務(wù)端程序建立一個(gè) TCP 連接。connect()?系統(tǒng)調(diào)用的原型如下:

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

          下面是?connect()?系統(tǒng)調(diào)用各個(gè)參數(shù)的作用:

          • sockfd:由?socket()?系統(tǒng)調(diào)用創(chuàng)建的文件句柄。

          • addr:指定要連接的遠(yuǎn)端 IP 地址和端口。

          • addrlen:指定參數(shù)?addr?的長(zhǎng)度。

          當(dāng)客戶端調(diào)用?connect()?函數(shù)時(shí),會(huì)觸發(fā)內(nèi)核調(diào)用?sys_connect()?內(nèi)核函數(shù),sys_connect()?函數(shù)實(shí)現(xiàn)如下:

          int sys_connect(int fd, struct sockaddr *uservaddr, int addrlen){    struct socket *sock;    char address[MAX_SOCK_ADDR];    int err;    ...    // 獲取文件句柄對(duì)應(yīng)的socket對(duì)象    sock = sockfd_lookup(fd, &err);    ...    // 從用戶空間復(fù)制要連接的遠(yuǎn)端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 個(gè)步驟:

          • 調(diào)用?sockfd_lookup()?函數(shù)獲取?fd?文件句柄對(duì)應(yīng)的 socket 對(duì)象。

          • 調(diào)用?move_addr_to_kernel()?函數(shù)從用戶空間復(fù)制要連接的遠(yuǎn)端 IP 地址和端口信息。

          • 調(diào)用?inet_stream_connect()?函數(shù)完成連接操作。

          我們繼續(xù)分析?inet_stream_connect()?函數(shù)的實(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 {        // 嘗試自動(dòng)綁定一個(gè)本地端口        if (inet_autobind(sk) != 0)             return(-EAGAIN);        ...        // 調(diào)用 tcp_v4_connect() 進(jìn)行連接操作        err = sk->prot->connect(sk, uaddr, addr_len);        if (err < 0)            return(err);        sock->state = SS_CONNECTING;    }    ...    // 如果 socket 設(shè)置了非阻塞, 并且連接還沒建立, 那么返回 EINPROGRESS 錯(cuò)誤    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ù)的主要操作有以下幾個(gè)步驟:

          • 調(diào)用?inet_autobind()?函數(shù)嘗試自動(dòng)綁定一個(gè)本地端口。

          • 調(diào)用?tcp_v4_connect()?函數(shù)進(jìn)行 TCP 協(xié)議的連接操作。

          • 如果 socket 設(shè)置了非阻塞,并且連接還沒建立完成,那么返回 EINPROGRESS 錯(cuò)誤。

          • 調(diào)用?inet_wait_for_connect()?函數(shù)等待連接服務(wù)端操作完成。

          • 設(shè)置 socket 的狀態(tài)為?SS_CONNECTED,表示連接已經(jīng)建立完成。

          在上面的步驟中,最重要的是調(diào)用?tcp_v4_connect()?函數(shù)進(jìn)行連接操作,我們來分析一下?tcp_v4_connect()?函數(shù)的實(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. 申請(qǐng)一個(gè)skb數(shù)據(jù)包對(duì)象 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序列號(hào) tp->write_seq = secure_tcp_sequence_number(sk->saddr, sk->daddr, sk->sport, usin->sin_port); ... // 8. 重置TCP最大報(bào)文段大小 tp->mss_clamp = ~0; ... // 9. 調(diào)用 tcp_connect() 函數(shù)繼續(xù)進(jìn)行連接操作 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 對(duì)象的路由緩存中。

          • 調(diào)用?sock_wmalloc()?函數(shù)申請(qǐng)一個(gè) skb 數(shù)據(jù)包對(duì)象。

          • 設(shè)置?目的端口?和?目的 IP 地址

          • 如果沒有指定?源 IP 地址,那么使用路由信息中的?源 IP 地址。

          • 調(diào)用?secure_tcp_sequence_number()?函數(shù)初始化 TCP 序列號(hào)。

          • 重置 TCP 協(xié)議最大報(bào)文段的大小。

          • 調(diào)用?tcp_connect()?函數(shù)發(fā)送?SYN包?給服務(wù)端程序。

          由于?TCP三次握手?的第一步是由客戶端發(fā)送?SYN包?給服務(wù)端,所以我們主要關(guān)注?tcp_connect()?函數(shù)的實(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頭部長(zhǎng)度 tp->tcp_header_len = sizeof(struct tcphdr) + (sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0); ... tcp_sync_mss(sk, mtu); // 設(shè)置TCP報(bào)文段最大長(zhǎng)度 ... TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; // 設(shè)置SYN標(biāo)志為1(表示這是一個(gè)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è)置序列號(hào) TCP_SKB_CB(buff)->end_seq = tp->write_seq; // 設(shè)置確認(rèn)號(hào) tp->snd_nxt = TCP_SKB_CB(buff)->end_seq;
          // 初始化滑動(dòng)窗口的大小 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è)置超時(shí)重傳定時(shí)器 ... // 把 skb 添加到 write_queue 隊(duì)列中, 用于重傳時(shí)使用 __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ù)的實(shí)現(xiàn)雖然比較長(zhǎng),但是邏輯相對(duì)簡(jiǎn)單,就是設(shè)置 TCP 頭部各個(gè)字段的值,然后把數(shù)據(jù)包發(fā)送給服務(wù)端。下面列出?tcp_connect()?函數(shù)主要的工作:

          • 設(shè)置 TCP 頭部的?SYN 標(biāo)志位?為 1 (表示這是一個(gè)?SYN包)。

          • 設(shè)置 TCP 頭部的序列號(hào)和確認(rèn)號(hào)。

          • 初始化滑動(dòng)窗口的大小。

          • 設(shè)置 socket 的狀態(tài)為?SYN_SENT,可參考上面三次握手的狀態(tài)圖。

          • 調(diào)用?tcp_v4_hash()?函數(shù)把 socket 添加到?tcp_established_hash?哈希表中,用于通過 IP 地址和端口快速查找到對(duì)應(yīng)的 socket 對(duì)象。

          • 設(shè)置超時(shí)重傳定時(shí)器。

          • 把 skb 添加到?write_queue?隊(duì)列中, 用于超時(shí)重傳。

          • 調(diào)用?tcp_transmit_skb()?函數(shù)構(gòu)建?SYN包?發(fā)送給服務(wù)端程序。

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

          45371845e52c714dfe76c03551bba87b.webp


          通過上面的分析,構(gòu)建?SYN包?并且發(fā)送給服務(wù)端是通過?tcp_transmit_skb()?函數(shù)完成的,所以我們來分析一下?tcp_transmit_skb()?函數(shù)的實(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); // 請(qǐng)求序列號(hào) th->ack_seq = htonl(tp->rcv_nxt); // 應(yīng)答序列號(hào) th->doff = (tcp_header_size >> 2); // 頭部長(zhǎng)度 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)); // 滑動(dòng)窗口大小
          th->check = 0; // 校驗(yàn)和 th->urg_ptr = ntohs(tcb->urg_ptr); // 緊急指針 ... // 計(jì)算TCP頭部的校驗(yàn)和 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ù)的實(shí)現(xiàn)相對(duì)簡(jiǎn)單,就是構(gòu)建 TCP 協(xié)議頭部,然后調(diào)用?ip_queue_xmit()?函數(shù)將數(shù)據(jù)包交由 IP 協(xié)議發(fā)送出去。

          至此,客戶端就發(fā)送了一個(gè)?SYN包?給服務(wù)端,也就是說,TCP 三次握手?的第一步已經(jīng)完成。

          下一篇文章,我們將會(huì)分析?TCP 三次握手?的第二步,也就是服務(wù)端接收到客戶端發(fā)送過來的?SYN包?時(shí)對(duì)應(yīng)的處理。


          瀏覽 83
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  神马午夜电影一区二区 | 免费无码久久久久成人 | 国产精品99久久久久久成人 | 操逼123网 | 青青草在线视频免费观看 |