本次实验,我们来探究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的复杂与精巧。