從源碼看tcp三次握手(上)

2021-02-20 embed linux share




閱讀本文需要對level-ip的整體架構有所了解,如果讀者尚未接觸過level-ip,請先閱讀下面文章:

分享一款Linux平臺下的tcp協議棧!超級透徹!

Linux系統中間件的巧妙實現--以用戶空間的tcp協議棧為例

請根據上述文章中的指引獲取leve-ip的全部源碼,並且嘗試在任意Linux發行版本上編譯運行。

著名的TCP三次握手

前面已經介紹過常用套接字接口函數,也就是伺服器調用bind、listen 以及 accept 等待客戶端進行連接,而客戶端connect函數主動請求連接伺服器。

客戶在調用函數 connect 前不必非得調用 bind 函數,因為如果需要的話,內核會確定源IP 地址,並按照一定的算法選擇一個臨時埠作為源埠。當客戶端使用tcp套接字進行連接時,調用 connect 函數將激發 TCP 的三次握手過程。如下圖:

TCP三次握手的剖析

這裡我們使用的網絡編程模型都是阻塞式的。所謂阻塞式,就是調用發起後不會直接返回,由作業系統內核處理之後才會返回。相對的,還有一種叫做非阻塞式的,暫時先不討論。

下面是具體的過程:

客戶端的協議棧向伺服器端發送了 SYN 包,並告訴伺服器端當前發送序列號 j,客戶端進入 SYNC_SENT 狀態;伺服器端的協議棧收到這個包之後,和客戶端進行 ACK 應答,應答的值為 j+1,表示對 SYN 包 j 的確認,同時伺服器也發送一個 SYN 包,告訴客戶端當前我的發送序列號為 k,伺服器端進入 SYNC_RCVD 狀態;客戶端協議棧收到 ACK 之後,使得應用程式從 connect 調用返回,表示客戶端到伺服器端的單向連接建立成功,客戶端的狀態為 ESTABLISHED,同時客戶端協議棧也會對伺服器端的 SYN 包進行應答,應答數據為 k+1;應答包到達伺服器端後,伺服器端協議棧使得 accept 阻塞調用返回,這個時候伺服器端到客戶端的單向連接也建立成功,伺服器端也進入 ESTABLISHED 狀態。從socket()函數來看CLOSE狀態

前面的文章分析了linux系統中間件的實現思路,明白了應用程式中使用的socket()函數,實際上是調用了level-ip協議站中的ipc_socket()函數,該函數的核心是_socket()函數,我們來看一下這個函數,如下:

int _socket(pid_t pid, int domain, int type, int protocol){    struct socket *sock;    struct net_family *family;
if ((sock = alloc_socket(pid)) == NULL) { print_err("Could not alloc socket\n"); return -1; } ... family = families[domain];
if (!family) { print_err("Domain not supported: %d\n", domain); goto abort_socket; } if (family->create(sock, protocol) != 0) { print_err("Creating domain failed\n"); goto abort_socket; } ...}

在_socket()函數裡面,我們是調用alloc_socket)()函數來分配一個管理套接字的結構體sock,在該結構體裡面保存了套接字初始狀態和應用程式的相關信息,如下圖:

static struct socket *alloc_socket(pid_t pid){    // TODO: Figure out a way to not shadow kernel file descriptors.    // Now, we'll just expect the fds for a process to never exceed this.    static int fd = 4097;    struct socket *sock = malloc(sizeof (struct socket));    list_init(&sock->list);
sock->pid = pid; sock->refcnt = 1;
pthread_rwlock_wrlock(&slock); sock->fd = fd++; pthread_rwlock_unlock(&slock);
sock->state = SS_UNCONNECTED; sock->ops = NULL; sock->flags = O_RDWR; wait_init(&sock->sleep); pthread_rwlock_init(&sock->lock, NULL); return sock;}

在第16行,設置了該套接字的初始狀態為SS_UNCONNECTED,注意此處並不是指tcp的SYNC_SENT狀態,大家不要把它們相混淆。

接著在_socket()函數裡面調用了family->create()函數,這裡考慮到多種協議族的支持,通過函數指針做了一個代碼分離。該函數指針真正指向的函數為inet_create()函數,我們來分析一下它:

int inet_create(struct socket *sock, int protocol){    struct sock *sk;    struct sock_type *skt = NULL;
for (int i = 0; i < INET_OPS; i++) { if (inet_ops[i].type & sock->type) { skt = &inet_ops[i]; break; } }
if (!skt) { print_err("Could not find socktype for socket\n"); return 1; }
sock->ops = skt->sock_ops;
sk = sk_alloc(skt->net_ops, protocol); sk->protocol = protocol; sock_init_data(sock, sk); return 0;}

第6~10行:獲取tcp連接的相關操作接口集合inet_ops,它裡面又細分出tcp操作接口集合tcp_ops和乙太網底層操作接口集合inet_stream_ops。如下:

static struct sock_type inet_ops[] = {    {        .sock_ops = &inet_stream_ops,        .net_ops = &tcp_ops,        .type = SOCK_STREAM,        .protocol = IPPROTO_TCP,    }};

第29行:調用sk_alloc函數分配sk結構體,在該函數裡面調用了tcp_ops接口裡面的tcp_alloc_sock()函數來完成分配工作,如下:

struct sock *sk_alloc(struct net_ops *ops, int protocol){    struct sock *sk;    sk = ops->alloc_sock(protocol);    sk->ops = ops;    return sk;}

第4行:調用tcp_ops中的tcp_alloc_sock()函數,產生管理tcp通信的接=結構體sk

第5行:把tcp操作接口集合tcp_ops記錄在sk->ops中

其中,tcp_alloc_sock()函數的實現如下:

struct sock *tcp_alloc_sock(){    struct tcp_sock *tsk = malloc(sizeof(struct tcp_sock));
memset(tsk, 0, sizeof(struct tcp_sock)); tsk->sk.state = TCP_CLOSE; tsk->sackok = 1; tsk->rmss = 1460; // Default to 536 as per spec tsk->smss = 536;
skb_queue_init(&tsk->ofo_queue); return (struct sock *)tsk;}

第3~5行:使用malloc函數動態申請內存,並初始化內存值為0

第6行:初始化tcp的連接狀態為close,終於看到tcp的初始連接狀態了!!!

第13行:初始化tcp無序隊列,當tcp接收到的數據不是有序的時候,先把數據掛載在這個隊列上。

從connect()函數來看SYNC_SENT狀態

同理,應用程式中的connect()函數,實際上是調用了level-ip協議站中的ipc_connect()函數,該函數的核心是_connect()函數,我們來看一下這個函數,如下:

int _connect(pid_t pid, int sockfd, const struct sockaddr *addr, socklen_t addrlen){    struct socket *sock;
if ((sock = get_socket(pid, sockfd)) == NULL) { print_err("Connect: could not find socket (fd %u) for connection (pid %d)\n", sockfd, pid); return -EBADF; }
socket_wr_acquire(sock);
int rc = sock->ops->connect(sock, addr, addrlen, 0); switch (rc) { case -EINVAL: case -EAFNOSUPPORT: case -ECONNREFUSED: case -ETIMEDOUT: socket_release(sock); socket_free(sock); break; default: socket_release(sock); } return rc;}

第5行:獲取_socket()函數申請到的套接字結構體sock,以進一步操作該次tcp連結。

第12行:調用乙太網底層接口inet_stream_connect,該函數實現如下:

static int inet_stream_connect(struct socket *sock, const struct sockaddr *addr,                        int addr_len, int flags){    struct sock *sk = sock->sk;    int rc = 0;    ...    switch (sock->state) {    default:        sk->err = -EINVAL;        goto out;    case SS_CONNECTED:        sk->err = -EISCONN;        goto out;    case SS_CONNECTING:        sk->err = -EALREADY;        goto out;    case SS_UNCONNECTED:        sk->err = -EISCONN;        if (sk->state != TCP_CLOSE) {            goto out;        }
sk->ops->connect(sk, addr, addr_len, flags); sock->state = SS_CONNECTING; sk->err = -EINPROGRESS;
if (sock->flags & O_NONBLOCK) { goto out; }
pthread_mutex_lock(&sock->sleep.lock); while (sock->state == SS_CONNECTING && sk->err == -EINPROGRESS) { socket_release(sock); wait_sleep(&sock->sleep); socket_wr_acquire(sock); } pthread_mutex_unlock(&sock->sleep.lock); socket_wr_acquire(sock); switch (sk->err) { case -ETIMEDOUT: case -ECONNREFUSED: goto sock_error; }
if (sk->err != 0) { goto out; }
sock->state = SS_CONNECTED; break; }...
}

第7~21行,判斷套接字狀態是否正常,前面我們已經通過_socket()函數把套接字狀態初始化為SS_UNCONNECTED了,因此這裡將進入最後一個分支來執行代碼。

第23行:調用tcp操作接口集合tcp_ops中的tcp_v4_connect函數,在該函數中會構建tcp數據幀,然後調用ip數據幀發送接口來進行數據的發送。

第34行:發送握手幀之後,線程進行睡眠狀態,直到伺服器返回應答幀之後再喚醒。

其中,tcp_v4_connect()調用了tcp_connect()函數,實現如下:

int tcp_connect(struct sock *sk){    struct tcp_sock *tsk = tcp_sk(sk);    struct tcb *tcb = &tsk->tcb;    int rc = 0;        tsk->tcp_header_len = sizeof(struct tcphdr);    tcb->iss = generate_iss();    tcb->snd_wnd = 0;    tcb->snd_wl1 = 0;
tcb->snd_una = tcb->iss; tcb->snd_up = tcb->iss; tcb->snd_nxt = tcb->iss; tcb->rcv_nxt = 0;
tcp_select_initial_window(&tsk->tcb.rcv_wnd);
rc = tcp_send_syn(sk); tcb->snd_nxt++; return rc;}

第8行、第14行:隨機產生了一個序列號,填充到tcp首部中

第18行:進一步調用tcp_send_syn()函數,該函數實現如下:

static int tcp_send_syn(struct sock *sk){    if (sk->state != TCP_SYN_SENT && sk->state != TCP_CLOSE && sk->state != TCP_LISTEN) {        print_err("Socket was not in correct state (closed or listen)\n");        return 1;    }
struct sk_buff *skb; struct tcphdr *th; struct tcp_options opts = { 0 }; int tcp_options_len = 0;
tcp_options_len = tcp_syn_options(sk, &opts); skb = tcp_alloc_skb(tcp_options_len, 0); th = tcp_hdr(skb);
tcp_write_syn_options(th, &opts, tcp_options_len); sk->state = TCP_SYN_SENT; th->syn = 1;
return tcp_queue_transmit_skb(sk, skb);}

第18行:此時tcp的連接狀態已經變為TCP_SYN_SENT狀態了

第19行:數據幀中握手標誌置1

第21行:發送tcp握手幀

總結

暫時先分析握手的第一個狀態變化,後面繼續把剩餘部分分析完,今天先到這裡。

相關焦點

  • Wireshark 基本介紹和學習 TCP 三次握手
    記得大學的時候就學習過TCP的三次握手協議,那時候只是知道,雖然在書上看過很多TCP和UDP的資料,但是從來沒有真正見過這些數據包, 老是感覺在雲上飄一樣,學得不踏實。有了wireshark就能截獲這些網絡數據包,可以清晰的看到數據包中的每一個欄位。更能加深我們對網絡協議的理解。對我而言, wireshark 是學習網絡協議最好的工具。
  • 35 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題
    02 TCP 連接建立TCP 三次握手過程和狀態變遷TCP 是面向連接的協議,所以使用 TCP 前必須先建立連接,而建立連接是通過三次握手而進行的。TCP 三次握手一開始,客戶端和服務端都處於 CLOSED 狀態。
  • 理解TCP/IP三次握手與四次揮手的正確姿勢
    順便說一句,原則上任何數據傳輸都無法確保絕對可靠,三次握手只是確保可靠的基本需要。2二、三次握手TCP(Transmission Control Protocol) 傳輸控制協議TCP是主機對主機層的傳輸控制協議,提供可靠的連接服務,採用三次握手確認建立一個連接位碼即tcp
  • 網際網路基礎-TCP網絡「握手」協議
    有一些網絡基礎的同學都知道TCP連接需要三次握手,斷開需要4次握手。但是如果要說TCP連接建立和斷開過程中,連接狀態是如何變化的,估計很難說出具體情況,下面我們看一張圖,描述了TCP連接狀態變化過程: tcp連接狀態變化過程這裡有幾個問題需要注意:1、為什麼建立連接時還需要第3次確認
  • TCP和UDP區別
    看一下TCP和UDP 協議頭的源碼文件頭在include/uapi/linux/tcp.h(unsigned short ==__be16 == __u16 == __sum16)(unsigned int == __be32)TCP頭UDP頭字節數差別點__be16 source
  • Linux 內核 TCP MSS 機制詳細分析
    [2][3] 而我在嘗試復現CVE-2019-11477漏洞的過程中,在第一步設置MSS的問題上就遇到問題了,無法達到預期效果,但是目前公開的分析文章卻沒對該部分內容進行詳細分析。所以本文將通過Linux內核源碼對TCP的MSS機制進行詳細分析。
  • 為什麼 TCP 建立連接需要三次握手
    需要注意的是我們會將重點放到為什麼需要 TCP 建立連接需要『三次握手』,而不僅僅是為什麼需要『三次』握手。很多人嘗試回答或者思考這個問題的時候其實關注點都放在了三次握手中的三次上面,這確實很重要,但是如果重新審視這個問題,我們對於『什麼是連接』真的清楚?只有知道連接的定義,我們才能去嘗試回答為什麼 TCP 建立連接需要三次握手。
  • 從linux源碼看epoll
    從linux源碼看epoll前言在linux的高性能網絡編程中,繞不開的就是epoll。
  • 面試官問:為什麼 TCP 建立連接需要三次握手 ?
    需要注意的是我們會將重點放到為什麼需要 TCP 建立連接需要『三次握手』,而不僅僅是為什麼需要『三次』握手。很多人嘗試回答或者思考這個問題的時候其實關注點都放在了三次握手中的三次上面,這確實很重要,但是如果重新審視這個問題,我們對於『什麼是連接』真的清楚?只有知道連接的定義,我們才能去嘗試回答為什麼 TCP 建立連接需要三次握手。
  • Linux TCP隊列相關參數的總結
    注意,本文內容均來源於參考文檔,沒有去讀相關的內核源碼做驗證,不能保證內容嚴謹正確。作為Java程式設計師沒讀過內核源碼是硬傷。下面我以server端為視角,從連接建立、 數據包接收和數據包發送這3條路徑對參數進行歸類梳理。
  • 重學TCP/IP協議和三次握手四次揮手
    劃重點:TCP(傳輸控制協議)和IP(網際協議) 是最先定義的兩個核心協議,所以才統稱為TCP/IP協議族TCP的三次握手四次揮手TCP是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,在發送數據前,通信雙方必須在彼此間建立一條連接。
  • TCP 三次握手、四手揮手,這樣說你能明白吧!
    TCP建立連接 TCP建立連接需要三個步驟,也就是大家熟知的三次握手。下圖了正常情形下通過三次握手建立連接的過程: 以為驗證三次握手是否描述正確,在下使用進行抓包驗證。
  • 深入OpenFlowPlugin源碼分析OpenFlow握手過程(一)
    本文轉自網易遊戲運維公眾號(neteasegameops) ,點擊查看原文。
  • 近兩萬字 TCP 硬核知識,教你吊打面試官!
    接下來,將以三個角度來闡述提升 TCP 的策略,分別是:TCP 三次握手的性能提升;TCP 四次揮手的性能提升;TCP 數據傳輸的性能提升;TCP 三次握手的性能提升TCP 是面向連接的、可靠的、雙向傳輸的傳輸層通信協議,所以在傳輸數據之前需要經過三次握手才能建立連接。
  • 他連 TCP 這幾個參數都不懂
    那麼,三次握手的過程在一個 HTTP 請求的平均時間佔比 10% 以上,在網絡狀態不佳、高並發或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調節三次握手中的參數,就會對性能產生很多的影響。如何正確有效的使用這些參數,來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態變遷」,這樣當出現問題時,先用netstat命令查看是哪個握手階段出現了問題,再來對症下藥,而不是病急亂投醫。客戶端和服務端都可以針對三次握手優化性能。
  • 為什麼 TCP 會被 UDP 取代?
    :TCP 的三次握手增加了數據傳輸的延遲和額外開銷;在上述的三個原因中,擁塞控制算法是導致 TCP 在弱網環境下有著較差表現的首要原因,三次握手和累計應答兩者的影響依次遞減,但是也加劇了 TCP 的性能問題。
  • 為什麼 TCP 會被 UDP 取代
    :TCP 的三次握手增加了數據傳輸的延遲和額外開銷;在上述的三個原因中,擁塞控制算法是導致 TCP 在弱網環境下有著較差表現的首要原因,三次握手和累計應答兩者的影響依次遞減,但是也加劇了 TCP 的性能問題。
  • 解Bug之路-NAT引發的性能瓶頸|埠|tcp|nginx|ip|ack_網易訂閱
    ,四次揮手裡面的Seq和Ack對應的值和三次回收中那個錯誤的ACK完全一致!那麼什麼時候PAWS會不通過呢,我們直接看下tcp_paws_reject的源碼吧:  static inline int tcp_paws_reject(const struct tcp_options_received *rx_opt,int rst){ if (tcp_paws_check(rx_opt, 0)) return 0; // 如果是rst,則放鬆要求
  • 總結的23 個 TCP高頻面試問題
    三次握手 明確了協議頭的要點之後,我們再來看三次握手。 三次握手真是個老生常談的問題了,但是真的懂了麼?不是浮在表面?能不能延伸出一些點別的? 我們先來看一下熟悉的流程。
  • 史上最簡單的 Wireshark 和 TCP 入門指南
    記得大學的時候就學習過TCP的三次握手協議,那時候只是知道,雖然在書上看過很多TCP和UDP的資料,但是從來沒有真正見過這些數據包, 老是感覺在雲上飄一樣,學得不踏實。有了wireshark就能截獲這些網絡數據包,可以清晰的看到數據包中的每一個欄位。更能加深我們對網絡協議的理解。對我而言, wireshark 是學習網絡協議最好的工具。