深入理解TCP協議及其源代碼


本次實驗,我們來探究connect及bind、listen、accept背后的三次握手。

實驗原理

首先簡要回顧一下TCP三次握手的過程:

  1. 第一次握手:client向server發送SYN=1的數據報文表示請求連接,初始序列號(Sequence Number)字段為X。此時client端處於SYN-SENT狀態。

  2. 第二次握手:server發送ACK=1, SYN=1的報文表示確認連接請求。ack序列號為X+1, 序列號字段置為Y。此時server處於SYN-RECEIVED狀態。

  3. 第三次握手:client發送ACK=1的報文向server表示最后確認。ack序列號為Y+1,序列號為X+1。至此雙方均進入ESTABLISHED狀態,至此連接成功建立。

為何需要三次握手呢?這是因為我們TCP需要工作在不可靠的信道中。考慮兩次握手:假設客戶端發送的第一個 SYN 在網絡中滯留了,客戶端因此重發 SYN 並建立連接,直到釋放。此時滯留的第一個 SYN 終於到了,根據兩次握手的規則,服務端直接進入 ESTABLISHED 狀態,而此時客戶端根本沒有發起新的連接,不會理會服務端發送的報文,白白浪費了服務端的資源。事實上,只要信道不可靠,雙方永遠都沒有辦法確認對方知道自己將要進入連接狀態。例如三次握手,最后一次 ACK 如果丟失,則只有客戶端進入連接狀態。四次、五次、多少次握手都有類似問題,三次其實是理論和實際的一個權衡。

回顧之前的reply/hello通信程序:server依次調用了bind(), listen()以及accept()函數;而client在調用connect()之后,雙方便可以發送/接收數據了。其中bind()和listen()函數用於設置服務端本機的端口綁定和監聽。而accept()返回的正是client的套接字描述符,這意味着此時連接已經建立。不難推斷出,三次握手的連接建立主要在client調用connect()以及server調用accept()的過程中完成。下面我們以這兩個系統調用為核心,探究TCP三次握手在Linux內核中的實現。實驗環境為:基於Linux-5.0.1的64為MenuOS。

實驗過程

前面的實驗已經表明connect()和accept()對應內核函數為 __sys_connect()__sys_accept4()。首先我們來看一看前者的實現:

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

可以看到,該方法的核心是對sock->ops->connect()的調用。同樣的,__sys_accept4()的核心仍然是對sock->ops->accept()的調用。這是兩個函數指針,在TCP協議棧初始化后,指向的分別是tcp_v4_conncet和inet_csk_accept這兩個函數,這些信息被記錄在名叫tcp_prot結構體變量中。我們可以在GDB中設置斷點驗證一下:

可以看到,在依次執行server和client程序后,程序會停在相應的斷點,被綁定的函數按預期被調用。

下面通過源碼看一看這兩個函數的實現,首先是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;
struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;
​
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;
}
​
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;
}
​
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;
​
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_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)
goto failure;
​
sk_set_txhash(sk);
​
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);
rt = NULL;
​
if (likely(!tp->repair)) {
if (!tp->write_seq)
tp->write_seq = secure_tcp_seq(inet->inet_saddr,
       inet->inet_daddr,
       inet->inet_sport,
       usin->sin_port);
tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
 inet->inet_saddr,
 inet->inet_daddr);
}
​
inet->inet_id = tp->write_seq ^ jiffies;
​
if (tcp_fastopen_defer_connect(sk, &err))
return err;
if (err)
goto failure;
​
err = tcp_connect(sk);
​
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;
}

tcp_v4_connect的主要作用就是建立連接。TCP是構建於IP之上的傳輸層協議,因此可以看到,該函數也調用了很多ip層提供的函數,比如:ip_route_connect(),ip_route_newports()等等。此處我們關注的重點是tcp_set_state(sk, TCP_SYN_SENT)以及tcp_connect(sk)這兩行。前者將狀態設置為TCP_SYN_SENT,即將數據報中的SYN字段置1;后者具體負責報文的發送,同樣地,它也會調用更下層的服務。

接下來是inet_csk_accept:

/*
 * This will accept the next outstanding connection.
 */
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
​
lock_sock(sk);
​
/* We need to make sure that this socket is listening,
 * and that it has something pending.
 */
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
​
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
​
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
​
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
​
if (sk->sk_protocol == IPPROTO_TCP &&
    tcp_rsk(req)->tfo_listener) {
spin_lock_bh(&queue->fastopenq.lock);
if (tcp_rsk(req)->tfo_listener) {
/* We are still waiting for the final ACK from 3WHS
 * so can't free req now. Instead, we set req->sk to
 * NULL to signify that the child socket is taken
 * so reqsk_fastopen_remove() will free the req
 * when 3WHS finishes (or is aborted).
 */
req->sk = NULL;
req = NULL;
}
spin_unlock_bh(&queue->fastopenq.lock);
}
out:
release_sock(sk);
if (req)
reqsk_put(req);
return newsk;
out_err:
newsk = NULL;
req = NULL;
*err = error;
goto out;
}

該函數的主要作用是從請求隊列中取出數據進行處理。當隊列為空時,則會調用inet_csk_wait_for_connect(),從名稱不難看出,此時將進入阻塞狀態直至有新的請求入隊。后者通過執行無限循環實現等待,有新的連接則跳出循環結束等待。 

至此我們對基於IPv4的TCP三次握手背后的兩大核心函數做了簡要介紹。最后貼一張TCP狀態轉換圖感受一下TCP的復雜與精巧。


免責聲明!

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



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