深入理解TCP協議及其源代碼——connect及bind、listen、accept背后的三次握手


1 TCP概述

  傳輸控制協議(TCP,Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,是為了在不可靠的互聯網絡上提供可靠的端到端字節流而專門設計的一個傳輸協議。互聯網絡與單個網絡有很大的不同,因為互聯網絡的不同部分可能有截然不同的拓撲結構、帶寬、延遲、數據包大小和其他參數。TCP的設計目標是能夠動態地適應互聯網絡的這些特性,而且具備面對各種故障時的健壯性。

主要功能:

  當應用層向TCP層發送用於網間傳輸的、用8位字節表示的數據流,TCP則把數據流分割成適當長度的報文段,最大傳輸段大小(MSS)通常受該計算機連接的網絡的數據鏈路層的最大傳送單元(MTU)限制。之后TCP把數據包傳給IP層,由它來通過網絡將包傳送給接收端實體的TCP層。TCP為了保證報文傳輸的可靠,就給每個包一個序號,同時序號也保證了傳送到接收端實體的包的按序接收。然后接收端實體對已成功收到的字節發回一個相應的確認(ACK);如果發送端實體在合理的往返時延(RTT)內未收到確認,那么對應的數據(假設丟失了)將會被重傳。
  • 在數據正確性與合法性上,TCP用一個校驗和函數來檢驗數據是否有錯誤,在發送和接收時都要計算校驗和;同時可以使用md5認證對數據進行加密。
  • 在保證可靠性上,采用超時重傳和捎帶確認機制。
  • 在流量控制上,采用滑動窗口協議,協議中規定,對於窗口內未經確認的分組需要重傳。
  • 在擁塞控制上,采用TCP擁塞控制算法:慢啟動、擁塞避免、快速重傳、快速恢復。

2 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之間可以開始傳輸數據了。

簡單來說,就是:

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

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

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

2 connect及bind、listen、accept背后的三次握手

1)首先再lab3目錄下執行以下命令,在qemu中啟動gdb server

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

qemu虛擬機啟動后重新打開一個終端,輸入以下命令

gdb
file ~/LinuxKernel/linux-5.0.1/vmlinux
target remote:1234
# 依次為各個函數設置斷點
b __sys_socket
b __sys_bind
b __sys_connect
b __sys_listen
b __sys_accept4

  

 

 

 斷點設置成功,然后輸入replyhi,不斷在終端輸入c,可以通過終端輸出的斷點信息知道哦各個函數執行順序,如圖所示

 

 

 

 斷點順序為1,2,4,5,1,3,5,所以函數的執行順序為,__sys_socket, __sys_bind, __sys_listen, __sys_accept4, __sys_socket, __sys_connect, __sys_accept4,接下來對相應的函數源碼進行分析。

bind 函數主要是服務器端使用,把一個本地協議地址賦予套接字;socket 函數並沒有為套接字綁定本地地址和端口號,對於服務器端則必須顯性綁定地址和端口號。

isten() 函數的主要作用就是將套接字( sockfd )變成被動的連接監聽套接字(被動等待客戶端的連接),至於參數 backlog 的作用是設置內核中連接隊列的長度(這個長度有什么用,后面做詳細的解釋),TCP 三次握手也不是由這個函數完成,listen()的作用僅僅告訴內核一些信息。

accept()函數功能是,從處於 established 狀態的連接隊列頭部取出一個已經完成的連接,如果這個隊列沒有已經完成的連接,accept()函數就會阻塞,直到取出隊列中已完成的用戶連接為止。與三次握手也沒有直接的聯系,

然后是connect函數,這里是真正tcp三次握手的開始,由客戶端調用connect主動發起連接,查詢__sys_connect函數源碼:

/*
*    Attempt to connect to a socket with the server address.  The address
*    is in user space so we verify it is OK and move it to kernel space.
*
*    For 1003.1g we need to add clean support for a bind to AF_UNSPEC to
*    break bindings
*
*    NOTE: 1003.1g draft 6.3 is broken with respect to AX.25/NetROM and
*    other SEQPACKET protocols that take time to connect() as it doesn't
*    include the -EINPROGRESS status for such sockets.
*/

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;
    //將地址對象從用戶空間拷貝到內核空間
        err = move_addr_to_kernel(uservaddr, addrlen, &address);
    if (err < 0)
        goto out_put;

    err =
        security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
    if (err)
        goto out_put;

    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                 sock->file->f_flags);
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
}

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
        int, addrlen)
{
    return __sys_connect(fd, uservaddr, addrlen);
}

該函數根據文件描述符找到指定的socket對象,將地址信息從用戶空間拷貝到內核空間,調用指定類型套接字的connect函數。

對應流式套接字的connect函數是inet_stream_connect:

/* connect系統調用的套接口層實現 */

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,

            int addr_len, int flags)

{

    struct sock *sk = sock->sk;

    int err;

    long timeo;

 

    lock_sock(sk);/* 獲取套接口的鎖 */

 

    if (uaddr->sa_family == AF_UNSPEC) {/* 未指定地址類型,錯誤 */

        err = sk->sk_prot->disconnect(sk, flags);

        sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;

        goto out;

    }

 

    switch (sock->state) {

    default:

        err = -EINVAL;

        goto out;

    case SS_CONNECTED:/* 已經與對方端口連接*/

        err = -EISCONN;

        goto out;

    case SS_CONNECTING:/*正在連接過程中*/

        err = -EALREADY;

        /* Fall out of switch with err, set for this state */

        break;

    case SS_UNCONNECTED:/* 只有此狀態才能調用connect */

        err = -EISCONN;

        if (sk->sk_state != TCP_CLOSE)/* 如果不是TCP_CLOSE狀態,說明已經連接了 */

            goto out;

 

/* 調用傳輸層接口tcp_v4_connect建立與服務器連接,並發送SYN段 */

        err = sk->sk_prot->connect(sk, uaddr, addr_len);

        if (err < 0)

            goto out;

 

        /* 發送SYN段后,設置狀態為SS_CONNECTING */

        sock->state = SS_CONNECTING;

 

 

        err = -EINPROGRESS;/* 如果是以非阻塞方式進行連接,則默認的返回值為

EINPROGRESS,表示正在連接 */

        break;

    }

 

    /* 獲取連接超時時間,如果指定非阻塞方式,則不等待直接返回 */

    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

 

    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {/* 發送完SYN

后,連接狀態一般為這兩種狀態,但是如果連接建立非常快,則可能越過這兩種狀態 */

        if (!timeo || !inet_wait_for_connect(sk, timeo))/* 等待連接完成或超時 */

            goto out;

 

        err = sock_intr_errno(timeo);

        if (signal_pending(current))

            goto out;

    }

 

    if (sk->sk_state == TCP_CLOSE)/* 運行到這里說明連接建立失敗 */

        goto sock_error;

 

    sock->state = SS_CONNECTED;/* 連接建立成功,設置為已經連接狀態 */

    err = 0;

out:

    release_sock(sk);

    return err;

 

sock_error:

    err = sock_error(sk) ? : -ECONNABORTED;

    sock->state = SS_UNCONNECTED;

    if (sk->sk_prot->disconnect(sk, flags))

        sock->state = SS_DISCONNECTING;

    goto out;

}

該函數首先檢查socket地址長度和使用的協議族,檢查socket的狀態,必須是SS_UNCONNECTED或SS_CONNECTING,調用實現協議的connect函數,對於流式套接字,實現協議是tcp,調用的是tcp_v4_connect(),對於阻塞調用,等待后續握手的完成;對於非阻塞調用,則直接返回 -EINPROGRESS。

然后是tcp_v4_connect的源代碼:

/* This will initiate an outgoing connection. */
 
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    __be16 orig_sport, orig_dport;
    __be32 daddr, nexthop;
    struct flowi4 *fl4;
    struct rtable *rt;
    int err;
    struct ip_options_rcu *inet_opt;
 
    if (addr_len < sizeof(struct sockaddr_in))
        return -EINVAL;
 
    if (usin->sin_family != AF_INET)
        return -EAFNOSUPPORT;
 
    nexthop = daddr = usin->sin_addr.s_addr;
    inet_opt = rcu_dereference_protected(inet->inet_opt,
                         lockdep_sock_is_held(sk));
 
    //將下一跳地址和目的地址的臨時變量都暫時設為用戶提交的地址。 
    if (inet_opt && inet_opt->opt.srr) {
        if (!daddr)
            return -EINVAL;
        nexthop = inet_opt->opt.faddr;
    }
 
    //源端口
    orig_sport = inet->inet_sport;
 
    //目的端口
    orig_dport = usin->sin_port;
    
    fl4 = &inet->cork.fl.u.ip4;
 
    //如果使用了來源地址路由,選擇一個合適的下一跳地址。 
    rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                  RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                  IPPROTO_TCP,
                  orig_sport, orig_dport, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        if (err == -ENETUNREACH)
            IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
        return err;
    }
 
    if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
        ip_rt_put(rt);
        return -ENETUNREACH;
    }
 
    //進行路由查找,並校驗返回的路由的類型,TCP是不被允許使用多播和廣播的
    if (!inet_opt || !inet_opt->opt.srr)
        daddr = fl4->daddr;
 
    //更新目的地址臨時變量——使用路由查找后返回的值
    if (!inet->inet_saddr)
        inet->inet_saddr = fl4->saddr;
    sk_rcv_saddr_set(sk, inet->inet_saddr);
    
    //如果還沒有設置源地址,和本地發送地址,則使用路由中返回的值
    if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
        /* Reset inherited state */
        tp->rx_opt.ts_recent       = 0;
        tp->rx_opt.ts_recent_stamp = 0;
        if (likely(!tp->repair))
            tp->write_seq       = 0;
    }
 
    if (tcp_death_row.sysctl_tw_recycle &&
        !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr)
        tcp_fetch_timewait_stamp(sk, &rt->dst);
 
    //保存目的地址及端口
    inet->inet_dport = usin->sin_port;
    sk_daddr_set(sk, daddr);
 
    inet_csk(sk)->icsk_ext_hdr_len = 0;
    if (inet_opt)
        inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
 
    //設置最小允許的mss值 536
    tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
 
    /* Socket identity is still unknown (sport may be zero).
     * However we set state to SYN-SENT and not releasing socket
     * lock select source port, enter ourselves into the hash tables and
     * complete initialization after this.
     */
 
    //套接字狀態被置為 TCP_SYN_SENT, 
    tcp_set_state(sk, TCP_SYN_SENT);
    err = inet_hash_connect(&tcp_death_row, sk);
    if (err)
        goto failure;
 
    sk_set_txhash(sk);
    
    //動態選擇一個本地端口,並加入 hash 表,與bind(2)選擇端口類似
    rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                   inet->inet_sport, inet->inet_dport, sk);
 
                   
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        goto failure;
    }
    /* OK, now commit destination to socket.  */
 
    sk->sk_gso_type = SKB_GSO_TCPV4;
    sk_setup_caps(sk, &rt->dst);

    if (!tp->write_seq && likely(!tp->repair))

        tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
                               inet->inet_daddr,
                               inet->inet_sport,
                               usin->sin_port);

    inet->inet_id = tp->write_seq ^ jiffies;
    
    //函數用來根據 sk 中的信息,構建一個完成的 syn 報文,並將它發送出去。
    err = tcp_connect(sk);
 
    rt = NULL;
    if (err)
        goto failure;
 
    return 0;
 
failure:
    /*
     * This unhashes the socket and releases the local port,
     * if necessary.
     */
    tcp_set_state(sk, TCP_CLOSE);
    ip_rt_put(rt);
    sk->sk_route_caps = 0;
    inet->inet_dport = 0;
    return err;
}

  

該函數完成了路由查找,得到下一跳地址,並更新socket對象的下一跳地址,將socket對象的狀態設置為TCP_SYN_SENT,如果沒設置序號初值,則選定一個隨機初值, 調用函數tcp_connect完成報文構建和發送。

tcp_connect的源碼:

/* 構造並發送SYN段 */

int tcp_connect(struct sock *sk)

{

    struct tcp_sock *tp = tcp_sk(sk);

    struct sk_buff *buff;

 

    tcp_connect_init(sk);/* 初始化傳輸控制塊中與連接相關的成員 */

 

    /* 為SYN段分配報文並進行初始化 */

    buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation);

    if (unlikely(buff == NULL))

        return -ENOBUFS;

 

    /* Reserve space for headers. */

    skb_reserve(buff, MAX_TCP_HEADER);

 

    TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN;

    TCP_ECN_send_syn(sk, tp, buff);

    TCP_SKB_CB(buff)->sacked = 0;

    skb_shinfo(buff)->tso_segs = 1;

    skb_shinfo(buff)->tso_size = 0;

    buff->csum = 0;

    TCP_SKB_CB(buff)->seq = tp->write_seq++;

    TCP_SKB_CB(buff)->end_seq = tp->write_seq;

    tp->snd_nxt = tp->write_seq;

    tp->pushed_seq = tp->write_seq;

    tcp_ca_init(tp);

 

    /* Send it off. */

    TCP_SKB_CB(buff)->when = tcp_time_stamp;

    tp->retrans_stamp = TCP_SKB_CB(buff)->when;

 

    /* 將報文添加到發送隊列上 */

    __skb_queue_tail(&sk->sk_write_queue, buff);

    sk_charge_skb(sk, buff);

    tp->packets_out += tcp_skb_pcount(buff);

    /* 發送SYN段 */

    tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));

    TCP_INC_STATS(TCP_MIB_ACTIVEOPENS);

 

    /* Timer for repeating the SYN until an answer. */

    /* 啟動重傳定時器 */

    tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);

    return 0;

}

  tcp_connect()中又調用了tcp_transmit_skb函數:

/* 發送一個TCP報文 */

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb)

{

    if (skb != NULL) {

        struct inet_sock *inet = inet_sk(sk);

        struct tcp_sock *tp = tcp_sk(sk);

        struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);

        int tcp_header_size = tp->tcp_header_len;

        struct tcphdr *th;

        int sysctl_flags;

        int err;

 

        BUG_ON(!tcp_skb_pcount(skb));

 

#define SYSCTL_FLAG_TSTAMPS 0x1

#define SYSCTL_FLAG_WSCALE  0x2

#define SYSCTL_FLAG_SACK    0x4

 

        sysctl_flags = 0;/* 標識TCP選項 */

        /* 根據TCP選項調整TCP首部長度 */

        if (tcb->flags & TCPCB_FLAG_SYN) {/* 如果當前段是SYN段,需要特殊處理一下 */

            /* SYN段必須通告MSS,因此報頭加上MSS通告選項的長度 */

            tcp_header_size = sizeof(struct tcphdr) + TCPOLEN_MSS;

            if(sysctl_tcp_timestamps) {/* 啟用了時間戳 */

                /* 報頭加上時間戳標志 */

                tcp_header_size += TCPOLEN_TSTAMP_ALIGNED;

                sysctl_flags |= SYSCTL_FLAG_TSTAMPS;

            }

            if(sysctl_tcp_window_scaling) {/* 處理窗口擴大因子選項 */

                tcp_header_size += TCPOLEN_WSCALE_ALIGNED;

                sysctl_flags |= SYSCTL_FLAG_WSCALE;

            }

            if(sysctl_tcp_sack) {/* 處理SACK選項 */

                sysctl_flags |= SYSCTL_FLAG_SACK;

                if(!(sysctl_flags & SYSCTL_FLAG_TSTAMPS))

                    tcp_header_size += TCPOLEN_SACKPERM_ALIGNED;

            }

        } else if (tp->rx_opt.eff_sacks) {/* 非SYN段,但是有SACK塊 */

            /* 根據SACK塊數調整TCP首部長度 */

            tcp_header_size += (TCPOLEN_SACK_BASE_ALIGNED +

                        (tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK));

        }

 

        if (tcp_is_vegas(tp) && tcp_packets_in_flight(tp) == 0)

            tcp_vegas_enable(tp);

 

        /* 在報文首部中加入TCP首部 */

        th = (struct tcphdr *) skb_push(skb, tcp_header_size);

        /* 更新TCP首部指針 */

        skb->h.th = th;

        /* 設置報文的傳輸控制塊 */

        skb_set_owner_w(skb, sk);

 

        /* Build TCP header and checksum it. */

        /* 填充TCP首部中的數據 */

        th->source      = inet->sport;

        th->dest        = inet->dport;

        th->seq         = htonl(tcb->seq);

        th->ack_seq     = htonl(tp->rcv_nxt);

        *(((__u16 *)th) + 6)    = htons(((tcp_header_size >> 2) << 12) | tcb->flags);

        /* 設置TCP首部的接收窗口 */

        if (tcb->flags & TCPCB_FLAG_SYN) {

            th->window  = htons(tp->rcv_wnd);/* 對SYN段來說,接收窗口初始值為rcv_wnd */

        } else {

            /* 對其他段來說,調用tcp_select_window計算當前接收窗口的大小 */

            th->window  = htons(tcp_select_window(sk));

        }

        /* 初始化校驗碼和帶外數據指針 */

        th->check       = 0;

        th->urg_ptr     = 0;

 

        if (tp->urg_mode &&/* 發送時設置了緊急方式 */

            between(tp->snd_up, tcb->seq+1, tcb->seq+0xFFFF)) {/*

緊急指針在報文序號開始的65535范圍內 */

            /* 設置緊急指針和帶外數據標志位 */

            th->urg_ptr     = htons(tp->snd_up-tcb->seq);

            th->urg         = 1;

        }

 

        /* 開始構建TCP首部選項 */

        if (tcb->flags & TCPCB_FLAG_SYN) {

            /* 調用tcp_syn_build_options構建SYN段的首部 */

            tcp_syn_build_options((__u32 *)(th + 1),

                          tcp_advertise_mss(sk),

                          (sysctl_flags & SYSCTL_FLAG_TSTAMPS),

                          (sysctl_flags & SYSCTL_FLAG_SACK),

                          (sysctl_flags & SYSCTL_FLAG_WSCALE),

                          tp->rx_opt.rcv_wscale,

                          tcb->when,

                              tp->rx_opt.ts_recent);

        } else {

            /* 構建普通段的首部 */

            tcp_build_and_update_options((__u32 *)(th + 1),

                             tp, tcb->when);

 

            TCP_ECN_send(sk, tp, skb, tcp_header_size);

        }

        /* 計算傳輸層的校驗和 */

        tp->af_specific->send_check(sk, th, skb->len, skb);

 

        /* 如果發送的段有ACK標志,則通知延時確認模塊,遞減快速發送ACK

段的數量,同時停止延時確認定時器 */

        if (tcb->flags & TCPCB_FLAG_ACK)

            tcp_event_ack_sent(sk);

 

        if (skb->len != tcp_header_size)/*

發送的段有負載,則檢測擁塞窗口閑置是否超時 */

            tcp_event_data_sent(tp, skb, sk);

 

        TCP_INC_STATS(TCP_MIB_OUTSEGS);

 

        /* 調用IP層的發送函數發送報文 */

        err = tp->af_specific->queue_xmit(skb, 0);

        if (err <= 0)

            return err;

 

        /* 如果發送失敗,則類似於接收到顯式擁塞通知的處理 */

        tcp_enter_cwr(tp);

 

        return err == NET_XMIT_CN ? 0 : err;

    }

    return -ENOBUFS;

#undef SYSCTL_FLAG_TSTAMPS

#undef SYSCTL_FLAG_WSCALE

#undef SYSCTL_FLAG_SACK

}

  


免責聲明!

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



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