深入理解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协议及其源代码

为何需要三次握手呢?这是因为我们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中设置断点验证一下:

深入理解TCP协议及其源代码

可以看到,在依次执行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的复杂与精巧。

深入理解TCP协议及其源代码

上一篇:Python 协程


下一篇:python,socket网络编程,最简单的server端和client端代码