本次實驗,我們來探究connect及bind、listen、accept背后的三次握手。
實驗原理
首先簡要回顧一下TCP三次握手的過程:
-
第一次握手:client向server發送SYN=1的數據報文表示請求連接,初始序列號(Sequence Number)字段為X。此時client端處於SYN-SENT狀態。
-
第二次握手:server發送ACK=1, SYN=1的報文表示確認連接請求。ack序列號為X+1, 序列號字段置為Y。此時server處於SYN-RECEIVED狀態。
-
第三次握手: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的復雜與精巧。