很多人总觉得学习TCP/IP协议没什么用,觉得日常编程开发只需要知道socket接口怎么用就可以了。如果大家定位过线上问题就会知道,实际上并非如此。如果应用在局域网内,且设备一切正常的情况下可能确实如此,但如果一旦出现诸如中间交换机不稳定、物理服务器宕机或者其它异常情况时,此时引起的问题如果只停留在套接字接口的理解层面将无法解决。因此,深入理解TCP/IP协议,对我们分析异常问题有很大的帮助。
下图是网络通信中常见的架构,也就是CS架构。其中程序包括两部分,分别为客户端(Client)和服务端(Server)。当然,实际的环境还要复杂的多,在客户端和服务端之间可能有多种不同种类和数量的设备,这些设备都会增加网络通信的复杂性。自然,也会增加程序开发容错的复杂性。
图1 基本架构
TCP的基本流程
在分析异常情况之前,我们先回忆一下TCP协议的基本逻辑。在客户端和服务端能够收发数据之前首先必需建立连接。连接的建立在协议层面也是通过收发数据包完成,只不过在用户层面就是客户端调用了一个connect函数。连接的过程俗称“三次握手”,具体流程如图2所示。
图2 TCP的三次握手流程
TCP连接的断开也是比较复杂的,需要经过所谓的“四次挥手”的流程。其原因是因为TCP是双工通信,分别需要从客户端和服务端2侧断开连接。
图3 TCP的四次挥手
另外一个比较重要的内容是TCP协议的状态转换,理解了这个内容,我们才能清楚出现各种异常情况下数据包的内容。
图4 TCP状态转换图
异常情况分析
了解了TCP的基本流程之后,我们再看一下各种异常情况。这些异常情况才是我们在后续解决问题的时候的关键。了解了这些异常情况及原理,后面解决问题才能游刃有余。
1. 试图与一个不存在的端口建立连接(主机正常)
这里的不存在的端口是指在服务器端没有程序监听在该端口。我们的客户端就调用connect,试图与其建立连接。这时会发生什么呢?
这种情况下我们在客户端通常会收到如下异常内容:
[Errno 111] Connection refused(连接拒绝)
具体含义可以查一下linux的相关手册,或者用搜索引擎搜索一下。试想一下,服务端本来就没有程序监听在这个接口,因此在服务端是无法完成连接的建立过程的。我们参考‘三次握手’的流程可以知道当客户端的SYNC包到达服务端时,TCP协议没有找到监听的套接字,就会向客户端发送一个错误的报文,告诉客户端产生了错误。而该错误报文就是一个包含RST的报文。这种异常情况也很容易模拟,我们只需要写一个小程序,连接服务器上没有监听的端口即可。如下是通过wireshark捕获的数据包,可以看到红色部分的RST报文。
图5 数据包截图
继续深入理解一下,在操作系统层面,TCP的服务端实际上就是从网卡的寄存器中读取数据,然后进行解析。对于TCP自然会解析出目的端口这个关键信息,然后根据这个信息查看有没有这样的套接字。这个套接字是什么呢?在用户层面是一个文件句柄,但在内核中实际是一个数据结构,里面记录了很多信息。这个数据结构存储在一个哈希表中,通过函数__inet_lookup_skb(net/inet_hashtables.h)可以实现对该数据结构的查找。对于上述情况,自然无法找到该套接字,因此TCP服务端会进行错误处理,处理的方式就是给客户端发送一个RST(通过函数tcp_v4_send_reset进行发送)。
2. 试图与一个某端口建立连接但该主机已经宕机(主机宕机)
这也是一种比较常见的情况,当某台服务器主机宕机了,而客户端并不知道,仍然尝试去与其建立连接。这种场景也是分为2种情况的,一种是刚刚宕机,另外一种是宕机了很长时间。为什么要分这2种情况?
这主要根ARP协议有关系,ARP会在本地缓存失效,TCP客户端就无法想目的服务端发送数据包了。
(192.168.1.100) 位于 08:00:27:1a:7a:0a [ether] 在 eth0
了解了上述情况,我们分析一下刚刚宕机的情况,此时客户端是可以向服务端发送数据包的。但是由于服务器宕机,因此不会给客户端发送任何回复。
图6 数据包截图
由于客户端并不知道服务端宕机,因此会重复发送SYNC数据包,如图6所示,可以看到客户端每隔几秒会向服务端发送一个SYNC数据包。这里面具体的时间是跟TCP协议相关的,具体时间不同的操作系统实现可能稍有不同。
3. 建立连接时,服务器应用被阻塞(或者僵死)
还有一种情况是在客户端建立连接的过程中服务端应用处于僵死状态,这种情况在实际中也会经常出现(我们假设仅仅应用程序僵死,而内核没有僵死)。此时会出现什么状态?TCP的三次是否可以完成?客户端是否可以收发数据?
在用户层面我们知道,服务端通过accept接口返回一个新的套接字,这时就可以和客户端进行数据往来了。也就是在用户层面来说,accept返回结果说明3次握手完成了,否则accept会被阻塞。在我们假设的情况下,其实就相当于应用程序无法进行accept操作了。
如果想彻底理解上面我们假设的问题,需要理解两点,一点是accept函数具体做了什么,另外一点是TCP三次握手的本质。
我们先试着理解第一点,accept会通过软中断陷入内核中,最终会调用tcp协议的inet_csk_accept函数,该函数会从队列中查找是否有处于ESTABLISHED状态的套接字。如果有则返回该套接字,否则阻塞当前进程。也就是说这里只是一个查询的过程,并不参与三次握手的任何逻辑。
三次握手的本质是什么呢?实际上就是客户端与服务端一个不断交流的过程,而这个交流过程就是通过3个数据包完成的。而这个数据包的发送和处理实际上都是在内核中完成的。对于TCP的服务端来说,当它收到SYNC数据包时,就会创建一个套接字的数据结构并给客户端回复ACK,再次收到客户端的ACK时会将套接字数据结构的状态转换为ESTABLISHED,并将其发送就绪队列中。而这整个过程跟应用程序没有半毛钱的关系。
当上面套接字加入就绪队列时,accept函数就被唤醒了,然后就可以获得新的套接字并返回。但我们回过头来看一下,在accept返回之前,其实三次握手已经完成,也就是连接已经建立了。
另外一个是如果accept没有返回,客户端是否可以发送数据?答案是可以的。因为数据的发送和接受都是在内核态进行的。客户端发送数据后,服务端的网卡会先接收,然后通过中断通知IP层,再上传到TCP层。TCP层根据目的端口和地址将数据存入关联的缓冲区。如果此时应用程序有读操作(例如read或recv),那么数据会从内核态的缓冲区拷贝到用户态的缓存。否则,数据会一直在内核态的缓冲区中。总的来说,TCP的客户端是否可以发送数据与服务端程序是否工作没有任何关系。
当然,如果是整个机器都卡死了,那就是另外一种情况了。这种情况就我们之前分析的第2种情况一直了。因为,由于机器完全卡死,TCP服务端无法接受任何消息,自然也无法给客户端发送任何应答报文。
TCP协议源代码跟踪分析
1.TCP的三次握手从用户程序的角度看就是客户端connect和服务端accept建立起连接时背后的完成的工作。由上次的实验我们可以知道,在socket接口层这两个socket API函数分别对应着sys_connect和sys_accept4函数,课上老师说明, sys_connect和sys_accecpt是通过函数指针sock->opt->connect和sock->opt->accept调用了具体的函数来实现的,在即调用了tcp_v4_connect函数和inet_csk_accept函数,这两个函数进一步触及TCP数据收发过程tcp_transmit_skb和tcp_v4_rcv函数。
在net/ipv4/tcp-ipv4.c文件下的结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数:
1 struct proto tcp_prot = { 2 .name = "TCP", 3 .owner = THIS_MODULE, 4 .close = tcp_close, 5 .pre_connect = tcp_v4_pre_connect, 6 .connect = tcp_v4_connect, 7 .disconnect = tcp_disconnect, 8 .accept = inet_csk_accept, 9 .ioctl = tcp_ioctl, 10 .init = tcp_v4_init_sock, 11 .destroy = tcp_v4_destroy_sock, 12 .shutdown = tcp_shutdown, 13 .setsockopt = tcp_setsockopt, 14 .getsockopt = tcp_getsockopt, 15 .keepalive = tcp_set_keepalive, 16 .recvmsg = tcp_recvmsg, 17 .sendmsg = tcp_sendmsg, 18 .sendpage = tcp_sendpage, 19 .backlog_rcv = tcp_v4_do_rcv, 20 .release_cb = tcp_release_cb, 21 .hash = inet_hash, 22 .unhash = inet_unhash, 23 .get_port = inet_csk_get_port, 24 .enter_memory_pressure = tcp_enter_memory_pressure, 25 .leave_memory_pressure = tcp_leave_memory_pressure, 26 .stream_memory_free = tcp_stream_memory_free, 27 .sockets_allocated = &tcp_sockets_allocated, 28 .orphan_count = &tcp_orphan_count, 29 .memory_allocated = &tcp_memory_allocated, 30 .memory_pressure = &tcp_memory_pressure, 31 .sysctl_mem = sysctl_tcp_mem, 32 .sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem), 33 .sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem), 34 .max_header = MAX_TCP_HEADER, 35 .obj_size = sizeof(struct tcp_sock), 36 .slab_flags = SLAB_TYPESAFE_BY_RCU, 37 .twsk_prot = &tcp_timewait_sock_ops, 38 .rsk_prot = &tcp_request_sock_ops, 39 .h.hashinfo = &tcp_hashinfo, 40 .no_autobind = true, 41 #ifdef CONFIG_COMPAT 42 .compat_setsockopt = compat_tcp_setsockopt, 43 .compat_getsockopt = compat_tcp_getsockopt, 44 #endif 45 .diag_destroy = tcp_abort, 46 };
在这里,我们可以看到socket接口层里sock->opt->connect和sock->opt->accept实际调用的函数tcp_v4_connect和inet_csk_accept。
2.接下来通过MenuOS的内核调试环境设置断点跟踪tcp_v4_connect函数和inet_csk_accept函数来进一步验证三次握手的过程。
在tcp_v4_connect处打个断点:
可以发现tcp_v4_connect函数在net/ipv4/tcp_ipv4.c处定义,看下代码:
1 /* This will initiate an outgoing connection. */ 2 int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) 3 { 4 struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; 5 struct inet_sock *inet = inet_sk(sk); 6 struct tcp_sock *tp = tcp_sk(sk); 7 __be16 orig_sport, orig_dport; 8 __be32 daddr, nexthop; 9 struct flowi4 *fl4; 10 struct rtable *rt; 11 int err; 12 struct ip_options_rcu *inet_opt; 13 struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row; 14 15 16 if (addr_len < sizeof(struct sockaddr_in)) 17 return -EINVAL; 18 19 20 if (usin->sin_family != AF_INET) 21 return -EAFNOSUPPORT; 22 23 24 nexthop = daddr = usin->sin_addr.s_addr; 25 inet_opt = rcu_dereference_protected(inet->inet_opt, 26 lockdep_sock_is_held(sk)); 27 if (inet_opt && inet_opt->opt.srr) { 28 if (!daddr) 29 return -EINVAL; 30 nexthop = inet_opt->opt.faddr; 31 } 32 33 34 orig_sport = inet->inet_sport; 35 orig_dport = usin->sin_port; 36 fl4 = &inet->cork.fl.u.ip4; 37 rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, 38 RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, 39 IPPROTO_TCP, 40 orig_sport, orig_dport, sk); 41 if (IS_ERR(rt)) { 42 err = PTR_ERR(rt); 43 if (err == -ENETUNREACH) 44 IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); 45 return err; 46 } 47 48 49 if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { 50 ip_rt_put(rt); 51 return -ENETUNREACH; 52 } 53 54 55 if (!inet_opt || !inet_opt->opt.srr) 56 daddr = fl4->daddr; 57 58 59 if (!inet->inet_saddr) 60 inet->inet_saddr = fl4->saddr; 61 sk_rcv_saddr_set(sk, inet->inet_saddr); 62 63 64 if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) { 65 /* Reset inherited state */ 66 tp->rx_opt.ts_recent = 0; 67 tp->rx_opt.ts_recent_stamp = 0; 68 if (likely(!tp->repair)) 69 tp->write_seq = 0; 70 } 71 72 73 inet->inet_dport = usin->sin_port; 74 sk_daddr_set(sk, daddr); 75 76 77 inet_csk(sk)->icsk_ext_hdr_len = 0; 78 if (inet_opt) 79 inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen; 80 81 82 tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; 83 84 85 /* Socket identity is still unknown (sport may be zero). 86 * However we set state to SYN-SENT and not releasing socket 87 * lock select source port, enter ourselves into the hash tables and 88 * complete initialization after this. 89 */ 90 tcp_set_state(sk, TCP_SYN_SENT); 91 err = inet_hash_connect(tcp_death_row, sk); 92 if (err) 93 goto failure; 94 95 96 sk_set_txhash(sk); 97 98 99 rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, 100 inet->inet_sport, inet->inet_dport, sk); 101 if (IS_ERR(rt)) { 102 err = PTR_ERR(rt); 103 rt = NULL; 104 goto failure; 105 } 106 /* OK, now commit destination to socket. */ 107 sk->sk_gso_type = SKB_GSO_TCPV4; 108 sk_setup_caps(sk, &rt->dst); 109 rt = NULL; 110 111 112 if (likely(!tp->repair)) { 113 if (!tp->write_seq) 114 tp->write_seq = secure_tcp_seq(inet->inet_saddr, 115 inet->inet_daddr, 116 inet->inet_sport, 117 usin->sin_port); 118 tp->tsoffset = secure_tcp_ts_off(sock_net(sk), 119 inet->inet_saddr, 120 inet->inet_daddr); 121 } 122 123 124 inet->inet_id = tp->write_seq ^ jiffies; 125 126 127 if (tcp_fastopen_defer_connect(sk, &err)) 128 return err; 129 if (err) 130 goto failure; 131 132 133 err = tcp_connect(sk); 134 135 136 if (err) 137 goto failure; 138 139 140 return 0; 141 142 143 failure: 144 /* 145 * This unhashes the socket and releases the local port, 146 * if necessary. 147 */ 148 tcp_set_state(sk, TCP_CLOSE); 149 ip_rt_put(rt); 150 sk->sk_route_caps = 0; 151 inet->inet_dport = 0; 152 return err; 153 }
分析代码,可以看出tcp_v4_connect函数的主要作用就是发起一个TCP连接,从这个函数中可以看到它调用了IP层提供的一些服务,比如ip_route_connect和ip_route_newports,同时在tcp_v4_connect函数中,调用了tcp_set_state函数,它设置了TCP_SYN_SENT,并进一步调用了tcp_connect(sk)来实际构造SYN并发送出去。
tcp_connect函数具体负责构造一个携带SYN标志位的TCP头并发送出去,同时还设置了计时器超时重发。这个函数定义在net/ipv4/tcp_output.c文件中,看看代码:
1 /* Build a SYN and send it off. */ 2 int tcp_connect(struct sock *sk) 3 { 4 struct tcp_sock *tp = tcp_sk(sk); 5 struct sk_buff *buff; 6 int err; 7 8 9 tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL); 10 11 12 if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk)) 13 return -EHOSTUNREACH; /* Routing failure or similar. */ 14 15 16 tcp_connect_init(sk); 17 18 19 if (unlikely(tp->repair)) { 20 tcp_finish_connect(sk, NULL); 21 return 0; 22 } 23 24 25 buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true); 26 if (unlikely(!buff)) 27 return -ENOBUFS; 28 29 30 tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); 31 tcp_mstamp_refresh(tp); 32 tp->retrans_stamp = tcp_time_stamp(tp); 33 tcp_connect_queue_skb(sk, buff); 34 tcp_ecn_send_syn(sk, buff); 35 tcp_rbtree_insert(&sk->tcp_rtx_queue, buff); 36 37 38 /* Send off SYN; include data in Fast Open. */ 39 err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : 40 tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); 41 if (err == -ECONNREFUSED) 42 return err; 43 44 45 /* We change tp->snd_nxt after the tcp_transmit_skb() call 46 * in order to make this packet get counted in tcpOutSegs. 47 */ 48 tp->snd_nxt = tp->write_seq; 49 tp->pushed_seq = tp->write_seq; 50 buff = tcp_send_head(sk); 51 if (unlikely(buff)) { 52 tp->snd_nxt = TCP_SKB_CB(buff)->seq; 53 tp->pushed_seq = TCP_SKB_CB(buff)->seq; 54 } 55 TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS); 56 57 58 /* Timer for repeating the SYN until an answer. */ 59 inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, 60 inet_csk(sk)->icsk_rto, TCP_RTO_MAX); 61 return 0; 62 } 63 EXPORT_SYMBOL(tcp_connect);
其中tcp_transmit_skb函数将tcp数据发送出去。
这边,客户端的一个tcp数据包发送出去了,服务端将做出什么反应呢,下面来看看服务端的inet_csk_accept函数,首先在inet_csk_accept处打上断点:
inet_csk_accept函数在net/ipv4/inet_connection_sock.c文件中:
1 /* 2 * This will accept the next outstanding connection. 3 */ 4 struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern) 5 { 6 struct inet_connection_sock *icsk = inet_csk(sk); 7 struct request_sock_queue *queue = &icsk->icsk_accept_queue; 8 struct request_sock *req; 9 struct sock *newsk; 10 int error; 11 12 13 lock_sock(sk); 14 15 16 /* We need to make sure that this socket is listening, 17 * and that it has something pending. 18 */ 19 error = -EINVAL; 20 if (sk->sk_state != TCP_LISTEN) 21 goto out_err; 22 23 24 /* Find already established connection */ 25 if (reqsk_queue_empty(queue)) { 26 long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); 27 28 29 /* If this is a non blocking socket don't sleep */ 30 error = -EAGAIN; 31 if (!timeo) 32 goto out_err; 33 34 35 error = inet_csk_wait_for_connect(sk, timeo); 36 if (error) 37 goto out_err; 38 } 39 req = reqsk_queue_remove(queue, sk); 40 newsk = req->sk; 41 42 43 if (sk->sk_protocol == IPPROTO_TCP && 44 tcp_rsk(req)->tfo_listener) { 45 spin_lock_bh(&queue->fastopenq.lock); 46 if (tcp_rsk(req)->tfo_listener) { 47 /* We are still waiting for the final ACK from 3WHS 48 * so can't free req now. Instead, we set req->sk to 49 * NULL to signify that the child socket is taken 50 * so reqsk_fastopen_remove() will free the req 51 * when 3WHS finishes (or is aborted). 52 */ 53 req->sk = NULL; 54 req = NULL; 55 } 56 spin_unlock_bh(&queue->fastopenq.lock); 57 } 58 out: 59 release_sock(sk); 60 if (req) 61 reqsk_put(req); 62 return newsk; 63 out_err: 64 newsk = NULL; 65 req = NULL; 66 *err = error; 67 goto out; 68 } 69 EXPORT_SYMBOL(inet_csk_accept);
服务端的inet_csk_accept函数会从请求队列中取出一个连接请求,如果队列为空则通过inet_csk_wait_for_connect阻塞住等待客户端的连接。
inet_csk_wait_for_connect函数定义在net/ipv4/inet_connection_sock.c文件中:
1 static int inet_csk_wait_for_connect(struct sock *sk, long timeo) 2 { 3 struct inet_connection_sock *icsk = inet_csk(sk); 4 DEFINE_WAIT(wait); 5 int err; 6 for (;;) { 7 prepare_to_wait_exclusive(sk_sleep(sk), &wait, 8 TASK_INTERRUPTIBLE); 9 release_sock(sk); 10 if (reqsk_queue_empty(&icsk->icsk_accept_queue)) 11 timeo = schedule_timeout(timeo); 12 sched_annotate_sleep(); 13 lock_sock(sk); 14 err = 0; 15 if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) 16 break; 17 err = -EINVAL; 18 if (sk->sk_state != TCP_LISTEN) 19 break; 20 err = sock_intr_errno(timeo); 21 if (signal_pending(current)) 22 break; 23 err = -EAGAIN; 24 if (!timeo) 25 break; 26 } 27 finish_wait(sk_sleep(sk), &wait); 28 return err; 29 }
根据代码可以分析出整个三次握手的过程为:客户端通过tcp_v4_connect函数调用到tcp_connect函数,将请求发送数据包出去,服务器端则通过inet_csk_accept函数调用inet_csk_wait_for_connect函数中的for循环进入阻塞,直到监听到请求才跳出循环。connect启动到返回和accept返回之间就是所谓三次握手的时间。
3.三次握手中携带SYN/ACK的TCP头数据的发送和接收
以上分析了用户程序调用socket接口、通过系统调用陷入内核进入内核态的socket接口层代码,然后根据创建socket时指定协议选择适当的函数指针进入协议处理代码中。那么网卡接收到数据后是如何通知上层协议来接收并处理数据的呢。其实在TCP/IP协议栈的初始化过程中,协议栈将handler赋值为tcp_v4_rcv的函数指针,也就是TCP协议中负责接收处理数据的入口,接收TCP连接请求及进行三次握手处理过程也都是从这里开始。
内核在处理接收到的TCP报文时使用了4个队列容器,分别为receive、out_of_order、prequeue、backlog队列。当网卡接收到报文并判断为TCP协议后,将会调用到内核的tcp_v4_rcv方法。tcp_v4_rcv方法会把这个报文直接插入到receive队列中。
在该函数定义在net/ipv4/tcp_ipv4.c文件中。
tcp_v4_rcv函数只要做以下几个工作:
(1) 设置TCP_CB
(2) 查找控制块
(3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理
(4) 接收TCP段
以上完成了将接收数据放入accept队列中,之后服务端接收客户端发来的tcp报文,并发送回SYN+ACK。
当前客户端处于TCP_SYN_SENT状态,并调用tcp_rcv_synsent_state_process处理SYN_SENT状态下接收到的TCP段,发送ACK报文
到这里,三次握手期间tcp接收处理数据包的过程基本完成。