作为一名现代开发人员,在日常的开发中不可避免的会接触到网络编程。网络编程已经成为现代开发人员不可或缺的基本素养,网络编程本身又绕不开socket与tcp。虽然各个语言都提供了丰富的网络库,开发人员直接使用socket api的机会很少,但是对于socket api的行为与tcp协议栈的交互过程也应该有所了解。这样对于日常的开发设计与故障诊断都有所帮助。
本文将以图示的方式讨论了socket函数的行为与tcp之间的交互。介绍了《UNIX网络编程卷1:套接字联网API》中的补充总结。
例子使用unpv13e/tcpcliserv。异常情况在Centos7.x86_64上进行测试。所码出的文字尽量做到严谨,但限于能力有限,如有错误,请指正,以防误导他人。
下文首先介绍socket api正常的使用情况与tcp之间的交互过程,再对各个异常情况分别描述。
我们使用echo服务例子来介绍正常情况下,socket api行为与tcp协议之间交互过程。
由客户端发送数据,服务端回传结果。数据收发函数统一使用阻塞read/write,也可以使用recv/send,recvmsg/sendmsg等。此例很多处理不够严谨,不可作为生产代码流程使用。
先上图:
图中socket api使用默认阻塞模式。client app指客户端应用程序,即直接调用socket api的进程。client TCP指client端的tcp协议栈,即client端操作系统内核。server端相同。
至此连接建立完成,之后便可使用建立好的socket进行数据收发了。tcp数据传输是一个很大的主题,这里只关心与socket api交互过程。
If a blocked call to one of the following interfaces is interrupted by a signal handler, then the call will be automatically restarted after the signal handler returns if
the SA_RESTART flag was used; otherwise the call will fail with the error EINTR:
* read(2), readv(2), write(2), writev(2), and ioctl(2) calls on "slow" devices. A "slow" device is one where the I/O call may block for an indefinite time, for example,
a terminal, pipe, or socket. (A disk is not a slow device according to this definition.) If an I/O call on a slow device has already transferred some data by the
time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).
日常开发中所说的 沾包 与 分包 就是短读短写的直接产物。下面用一张图概括一下沾包分包的产生。
所以,在开发阶段与应用层协议设计时要考虑这些情况,比如要增加循环写入来避免短写,增加包体长度字段来处理短读问题。
先调用close的一方,称为 主动关闭,另一方为 被动关闭:
异常情况总结有以下几种:
其中1/2可能发生在的任何环节,其中大部分都会触发超时重传机制。所以需要先介绍下tcp的重传。
我们知道tcp在发送一个包以后,都需要ACK确认。如果没有收到ACK就会发生重传。而重传又分为两种重传, 基于定时器重传 和 快速重传 。快速重传大部分是处理包乱序的情况。我们讨论的异常情况一般都是没有后续包的情况。基于定时器重传又叫超时重传,要基于一个超时时间,这个时间就叫做 RTO (Retransmission Timeout)。RTO的计算又要基于 RTT (round-trip-time)来评估初始值。在每次超时后,以指数增长RTO用于下次的超时时间。
以server app崩溃为例:
如果client app在 server app在崩溃后调用connect,server TCP将回复RST报文,connect返回ECONNREFUSED错误。
还以server主机崩溃为例。server主机崩溃意味着server TCP失去响应,client TCP发送的任何数据报都得不到ACK。这和对端网络不可达其实是相同的。都会引起client TCP的超时重传。崩溃发生在不同的阶段,判断重传失败的策略有些细微差别。
当client app调用connect函数,发送SYN后,服务器主机崩溃,或者干脆网络不可达。
使用iptables drop 掉SYN包进行测试。
iptables -A INPUT -p tcp --dport 9877 --syn -j DROP
从tcpdump抓包看到,client发送SYN后,又连续发送了6个包,每个时间间隔为1, 2, 4, 8, 16, 32s,之后又等了64s,connect函数返回ETIMEDOUT。总共等了127s。
$ date; ./tcpcli01 127.0.0.1; date
Wed Jul 11 00:46:24 CST 2018
connect error: Connection timed out
Wed Jul 11 00:48:31 CST 2018
这个例子中有两个问题:
重传次数6,是net.ipv4.tcp_syn_retries = 6参数控制的。
首次超时时间1s,由于SYN发出后,ACK还没有收到所以无法计算RTO,所以使用了初始设置,具体实现。
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */
随带介绍一下SYN + ACK后无回复的情况。
当client app调用connect函数,client TCP发送SYN,server TCP回复了SYN + ACK,这时client主机崩溃。我们使用
iptables -A INPUT -p tcp --dport 9877 --tcp-flags ALL ACK -j DROP
屏蔽掉client TCP回复的ACK包,进行抓包:
由于net.ipv4.tcp_synack_retries = 5,所以server TCP重传了5次。第6次超时后,tcp_write_err。
期间server端运行netstat,socket在SYN_RECV状态停留63s后消失。
netstat -npt | grep 9877
tcp 0 0 server-ip:9877 client-ip:51151 SYN_RECV -
著名的SYN Flood攻击,便是此种情况。
在数据收发阶段的超时重传,超时时间使用到了RTO的计算,而重传次数受
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 15
这两个参数约束,具体计算方法就不赘述了。可以参考源码,或1 2。
由于此例使用MacOS测试,结果与linux有些出入,但原理相同。
关闭阶段对于app端的影响不是很大,但是对于高并发大请求量服务器却至关重要。
client调用close,client TCP进入FIN_WAIT_1后收不到ACK。tcp协议栈使用net.ipv4.tcp_orphan_retries = 0配置项来约束了FIN包的重传次数。源码,如果没有设置,将使用默认值8。
使用iptables进行测试:
iptables -A INPUT -p tcp --dport 9877 --tcp-flags ALL FIN,ACK -j DROP
17:09:40 tcp 0 1 127.0.0.1:51393 127.0.0.1:9877 FIN_WAIT1
......
17:11:22 tcp 0 1 127.0.0.1:51393 127.0.0.1:9877 FIN_WAIT1
此情况不会造成重传,socket停留在FIN_WAIT_2的时间受net.ipv4.tcp_fin_timeout = 60控制。
可以使用脚本测试:
while sleep 1; do
netstat -ant | grep FIN_WAIT2 | while read content; do
echo -n $(date +"%T") ""
echo $content
done
done
观察结果可得到时间大约为1min。
在发送完第三次挥手即FIN后,该端状态为LAST_ACK,之后对端崩溃或网络不可达造成接收不到最后的ACK。这时会触发重传FIN。达到重传上限后,tcp_write_err。
15:35:51 tcp 0 1 127.0.0.1:9877 127.0.0.1:49828 LAST_ACK
.....
15:37:33 tcp 0 1 127.0.0.1:9877 127.0.0.1:49828 LAST_ACK
验证一下:
17:25:51 tcp 0 1 127.0.0.1:51659 127.0.0.1:9877 FIN_WAIT1
....
17:26:02 tcp 0 1 127.0.0.1:51659 127.0.0.1:9877 FIN_WAIT1
17:29:48 tcp 0 1 127.0.0.1:9877 127.0.0.1:51708 LAST_ACK
....
17:30:00 tcp 0 1 127.0.0.1:9877 127.0.0.1:51708 LAST_ACK
看来这两个状态都受net.ipv4.tcp_orphan_retries配置影响,只不过不是次数的意思。
由此可以看到,这些状态停留时间还是很长的,我使用内网测试,RTO都相对很少,如果在公网时间会更长。
对于反向代理服务器,由于端口限制,在处理短连接请求时,如果过多的连接得不到释放,将大大降低服务器并发量,net.ipv4.ip_local_port_range如果安3w算的话,一个请求停留在TIME_WAIT为1min,那么QPS只能到500。
网上有大量的优化帖子讨论的系统参数配置,都是对于以上几种状态时间的优化。
client TCP发送SYN,server TCP接收到SYN后需要将该纪录添加到半连接队列,等待之后的ACK到达,如果这时连接队列满了,tcp协议栈会做什么处理呢?
tcp协议栈在处理连接队列满的情况时,只是简单丢弃。推荐阅读:
How TCP backlog works in Linux
linux里的backlog详解
源码
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
// ......
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
// ......
drop:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
return 0;
}
linux源码中会有一个定时器定期清理超时的半连接请求。
当client的SYN到达server TCP时,如果server TCP全连接队列满了并且半连接队列>1,server TCP只是简单的丢弃该包,不做任何的后续处理,之后的情形如同”SYN无回复“,client收不到SYN + ACK,会定时重传SYN。在127s后,client的connect函数返回ETIMEDOUT错误。
源码连接
/*
* The three way handshake has completed - we got a valid synack -
* now create the new socket.
*/
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
// ......
if (sk_acceptq_is_full(sk))
goto exit_overflow;
// ......
exit_overflow:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
exit_nonewsk:
dst_release(dst);
exit:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
return NULL;
// ....
}
同样简单丢弃,只是做了一些统计。之后就如同 ”SYN + ACK无回复“ 的情形一样了。
server TCP定时重传SYN + ACK包,63s后服务端关闭socket。但此时client已经认为连接成功,client TCP为ESTABLISHED,如果client app不做任何读写操作,将不会感知到对端连接关闭。当client app调用write,client TCP向对端发送PSH后,server TCP会回复RST。client app之后的调用将返回ECONNRESET。
图示:
以上简单的介绍了一下socket api通常的使用过程,并与tcp协议之间的交互。帮助大家理解记忆,并熟悉异常情况的协议栈的行为,socket api的反应。
从文章可以看出虽然都说tcp是可靠传输,但是对于应用层来说,只有其在全部正常情况时,才能可靠。如果发送异常情况,其可靠性是不能得到保证的。