昨天下午跟同事讨论TCP挥手断开的细节,越发感到TCP协议真的是非常令人讨厌,这个协议已经成了人们装逼的谈资,就是因为它非常复杂,且毫无确定性可言!如果你能说出它的任何细节方面的前因后果,那你一定就是牛人了,但这其实毫无意义。
如果你阅读TCP的诸多RFC,然后对比着4.4 BSD,Linux,Windows的实现,手边再放一本那被捧为圣经的《TCP/IP详解(卷1)》,你会发现太多类似下面的措辞:
XXX并没有严格规定YYY,但是ZZZ为了AAA是…并不明确SDJWFMCQ。。。
OH!shit!
我截一张TCP状态机的局部,来自RFC793[page 22]:https://tools.ietf.org/html/rfc793
这个图非常权威。我们注意到FINWAIT-2这个状态,它的转移条件只有一个,即收到对端的FIN,然后进入TIMEWAIT。
那么问题来了,如果对端死活不发送FIN,本端会一直待在FINWAIT-2状态吗?
按照TCP全双工的概念推论,答案显然是:
这是合理的,也是符合TCP规范的,因为TCP是一个双向全双工的传输协议,本端发送FIN仅仅意味着本端到对端这个方向上的传输结束了,而对端到本端的传输依然可以继续,直到对端也发送一个FIN过来。所以说我们看到断开连接的挥手动作是4次,其实就是两个来回,每一个来回关闭一个方向的数据传输。
非常完美的解释!非常完美的规范!
但是,….TCP总是伴随着这么些烦人的但是
如果对端故意不发送FIN,且也不传输数据,那么意味着本端始终处在FINWAIT-2状态而资源无法释放,这不正是一个DDoS的典型场景吗?但是如果不这么做,也不符合TCP双向全双工独立控制的规范啊!
是规范重要还是现实中的问题重要?
我们仔细想想迄今为止有多少TCP连接时关闭一半的,即客户端始发方向关闭连接,而服务端依然在传输大量数据这种情形,似乎几乎是没有的。相反,几乎很多的C/S模式的TCP连接都是单向的,比如文件下载,至始至终,数据几乎都是从服务器往客户端发送,在最初的客户端请求文件结束后,事实上就相当于客户端始发方向的连接已经被关闭了!
因此,为了实现双向全双工的语义,完全可以在应用层做,完全没有必要在挥手关闭连接的逻辑上照本宣科而较真儿,事实上,我认为,TCP当初这么设计就是错误的,至少是不合理的,除了增加了复杂性之外,毫无意义。也许吧,当初设计协议的都是学院派,始终保持着一种对完备性的笃信和追求,所以既然TCP取自传输控制之名,那么就必须完成传输与控制之完备性的逻辑,也许换个名字会好些吧。
从Telnet,FTP,到Apache,Nginx,几乎所有的TCP服务的实现均遵循了收到客户端的FIN之后立即发送FIN这么一个不成文的事实,也就是说,对于主动关闭的一方,当它发送完FIN进入FINWAIT-2状态后,可以在预期的时间内收到对端的FIN从而进入TIMEWAIT状态,而且这个所谓的“预期的时间”不会太长,以秒计算吧,因此给定一个超时时间是明智的。
因此,针对上面问题“如果对端死活不发送FIN,本端会一直待在FINWAIT-2状态吗?”的回答我把可能的答案罗列:
我们看到,历史选择了现实而摒弃了理想。Linux任意使用2.2内核以上的发行版,看tcp的manual,其中的:
tcp_fin_timeout (integer; default: 60; since Linux 2.2)
This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed. This is
strictly a violation of the TCP specification, but required to prevent denial-of-service attacks. In Linux 2.2,
the default value was 180.
现在FINWAIT-2为什么会有个超时时间的问题已经解释清楚了,接下来的问题是,如果FINWAIT-2的timer超时了,这个TCP连接将何去何从?
我事先还真没有了解过实现的细节,但是按照我对这个问题理解的逻辑来讲,我认为timer到期后连接应该被销毁,顺便给对端发送一个reset。我之所以这么认为,我是这么想的。
既然在预期的时间内对端没有发送FIN(是的,FIN会丢失,但是TCP也会重传,另外,网线也可能被剪断),那么说明对端是“违约”的,至少是不符合常理的,对待不遵守游戏规则的,当然也不需要规则内的措施,直接释放连接是本端的原则,而发送reset则是针对对端“违约”的告知,就是想告诉对端“你违约了,明白吗?”…
我和两位同事楼下抽根烟讨论了这个问题,一位同事持有不同意见,认为不会发送reset,事实证明他是对的,确实不会发送reset,我犯了与垃圾对话的错误。我一向秉承的就是针对我不感兴趣或者不想卷入的事情,我会保持沉默这个理念,如果TCP对端违约,按照这个理念,超时后把连接资源默默释放即可,不必再与之对话!我一直在家“教育”我的老婆和女儿,不与之交互,保持沉默是最好的策略….然而,我自己在一个系统设计问题上却犯了错误,不该!
从社会工程学的角度来看,如果你只是为了说一句“你错了”,而发送一个reset,搞不好就会被绕进去,所以不理它就是了。我们都知道婴幼儿是社会工程学领域内的高手,因为他只要一哭闹,你总是会与之交互,然后你们一来二去的,婴幼儿的目的就达到了….不多说。
好了,最后一个问题,在FINWAIT-2超时之后,连接还会进入TIMEWAIT状态吗?
我认为是不会的,连接会直接消失。但是一位同事通过代码确认了一个不同的意见,他认为在经历了FINWAIT-2之后,即FINWAIT-2的timer到期后,连接依然会进入到TIMEWAIT状态,其通过tcp_time_wait函数的调用路径可以确认,调用参数为:
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
我始终是持怀疑态度的,而且我也不善于撸代码,而且特别恶心的就是TCP的代码,代码总是有不确定的因此,一堆堆的if分支,不去实际run的话,也许你能讲清楚的那个分支反而是99%进不去的分支。
所以我依然选择设计实验来验证。
两台机器,一台作为Client,IP地址为192.168.44.138,另一个为侦听了22端口的Server,IP地址为192.168.44.111,好了,我在Client上配置以下的iptables规则,以阻止Server发送的FIN到达Client的TCP处理逻辑,以模拟对端永远不回复FIN导致FINWAIT-2到期的情形:
iptables -A INPUT -s 192.168.44.111 -p tcp --tcp-flags SYN,FIN,RST FIN -j DROP
接下来我把Client的fin_timeout设置短一些:
sysctl -w net.ipv4.tcp_fin_timeout=5
最后我在Client上发起一个连接并随即Ctrl-]+q关闭,以使一个TCP连接进入FINWAIT-2状态(iptables规则会阻止对端的FIN,因此本端将进入FINWAIT-2而不是TIMEWAIT):
root@debian:/home/zhaoya# telnet 192.168.44.111 22
Trying 192.168.44.111...
Connected to 192.168.44.111.
Escape character is '^]'.
SSH-2.0-OpenSSH_7.4
^]
telnet> q
Connection closed.
root@debian:/home/zhaoya#
迅速观察netstat:
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 ESTABLISHED 96417/telnet
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
tcp 0 0 192.168.44.138:53068 192.168.44.111:22 FIN_WAIT2 -
root@debian:/home/zhaoya# netstat -anpt|grep 111
root@debian:/home/zhaoya# netstat -anpt|grep 111
大概5秒钟,连接灰飞烟灭,什么也没有剩下,连接并没有进入到TIMEWAIT,与此同时tcpdump抓包,也没有看到任何reset。实验很low,但是却说明了问题。
连接在FINWAIT-2超时后并不会进入TIMEWAIT状态,也不会发送reset,而是直接默默消失。
关于tcp_time_wait这个函数的代码,我就不撸了,TCP的代码非常恼人的。只说几个细节:
1. 只要调用tcp_time_wait,TCP连接状态就会变成TCP_TIME_WAIT;
2. 如果以TCP_FIN_WAIT2参数调用tcp_time_wait,则TCP_FIN_WAIT2作为substate处理对端的FIN;
3. 不管是TCP_FIN_WAIT2还是TCP_TIME_WAIT,均是将TCP连接从Establish哈希链表摘除,重新分配TW item链接进入哈希表。
只要写关于TCP的任何东西,代码,文档,文章,测试case脚本,我都是感慨的,我想唱着歌爆粗,我想大笑,我又想摔电脑,不一而足。TCP是令人气愤的,TCP是过时的。
你知道TIMEWAIT持续多久吗?你真的知道吗?
很多人会回答120秒,很多人会回答2MSL,很多人不知道什么是MSL,2MSL为什么是120秒而不是360秒,我要是说这是根据光速以及地球的周长算出来的你信吗?事实上确实是和地球周长有关的。如果是在火星上,TCP的TIMEWAIT超时值一定至少半小时。
可是,对于Linux系统,上面的说法全是谎言,在Linux上,TIMEWAIT的定义是:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
* state, about 60 seconds */
你不信吗?试试看呗,还是刚才那个Low逼实验,这次把iptables规则去掉,Ctrl-]+q之后,观察TIMEWAIT的时间,观察期间把你的Windows系统右下角的钟表打开,看看是不是60秒后TIMEWAIT连接就消失了。
Why?为什么TCP的实现一而再地违反所谓的规范?到底有没有规范?到底什么是MUST的,什么是MAY的。
我同样不喜欢HTTP,确切的说,是HTTP 1.x,同样的原因,它太松散了。请问HTTP的头部最长有多长?标准并没有明确的规定,读到\r\n\r\n为结束,但是Web服务器的实现却规定了。不然呢?不然一个恶意的客户端可以产生100T的头部,瞬间耗尽服务器的所有资源。
所以我就很看好HTTP 2.0,它解决了这个问题。
最后说说企业招聘和面试。
懂TCP有什么了不起吗?并没有!可是TCP几乎是各家必考的内容,令人不解的是,其实很多面试官也不懂,一群不懂TCP的人招了另一群不懂TCP的人,这并不耽误所有的人持续跪舔TCP!
诚然,如今互联网大行其道,一个送外卖的公司都能上万人的规模,个个拿着高薪,过着小康生活,有车有房有烧烤,有啤酒有脂肪肝,这其中有TCP的功劳,毕竟我们如今的服务器几乎90%+都是基于TCP的,这意味着90%+以上的人必须和TCP打交道,当你去应聘互联网公司的技术岗时,你可以不懂快排,你可以不懂数据库,但你不能不知道TCP。这就是现实!
精不精通不重要,懂就行,TCP是行话,大家一说TCP就知道都在一个坑里找食的,这就像喝威士忌,你再能喝也没用,你得会说“纯饮”,“水割”,你得能扯橡木桶,这就像血色浪漫里关于高尔夫的言论,对于上流社会的人,你可以不喜欢高尔夫,但你不能不会…
我从2004年第一次知道3次握手,一直到昨天跟Google的人聊TCP BBR 2.0一个细节的数学推导,06年底参加工作到现在从业十几年了,面试过太多的公司,对TCP三次握手倒背如流,以至于从2010年开始当有人让我笔试TCP相关的题目时,我要么借口有事走人,要么就直接写下两个字“口述”,这种事我能扯一整夜,只要你买单烧烤和酒,咱就找个大排档单练。对了,我不喝啤酒,白酒,伏特加,咖啡,奶茶和果汁,我只喝烧酒,清酒,黄酒,威士忌,白兰地,特浓柠檬汁,或者纯净水。
很多次,若不是我急于用钱有求于人,我可能也会像徐晓冬那般跟面试官来个关于TCP的现场PK,然后爆料这些都是假的。唉…
IP层的东西难道不重要吗?
上个月轮到我值班,有人碰到一个问题,说是在一台机器上确认TCP的init cwnd就是10,然而一个进程发包的时候只能发出去1个包,问我这是怎么回事。我犯了一个错误,我把这个问题搁置了,我觉得这不是一个有难度,且好玩的问题,再说当时还有更加紧急的问题,所以我三言两语敷衍提问者,说什么你这是在虚拟机环境,而虚拟机涉及到不可控的调度….说的我自己都信了。
过了好几天,那人的问题还没解决,并且已经严重影响了业务,再次问我,态度已经不是很好了,如果不给一个明确地说法,感觉像要投诉我的样子。闲来无事就帮他看看吧,其实我宁可去排查100个Bridge,bonding的问题,也不想沾染TCP…你们知道我是怎么在1秒内解决问题的吗?
因为我精通IP层的逻辑啊,因为我精通iproute2的使用啊,我让他赶紧ip route ls tab all,结果发现了一条路由:
..... initcwnd 1
这不就是问题的根源吗?很多人不知道initcwnd还能针对路由来指定。不知道吧,哈哈,你也许你知道,但你知道这是为什么吗?
什么是路由?TCP是不管路由的。但是TCP的拥塞控制却完全离不开路由。我们都知道,一条路和另一条路的拥塞程度是完全不同的,IP层的路由正是基于这个拥塞程度的不同,想办法用参数告诉你,哪条路更好走一些,仅此而已。悲哀的是,端到端的TCP并没有“路”的概念!
关于select,poll,epoll我就不多说了。
TCP没有意思,真的没有意思,也许,真的是时候关注一下QUIC了,顺便说一句,BBR在TCP上的实现是一个半吊子实现,在QUIC上的实现才是完整的,虽然QUIC还不甚完美,但也不要先入为主地去捧TCP而唱衰QUIC。QUIC99次行1次不行就会被你们记录在案,而对于TCP,你们只是自然主观的屏蔽掉了你们不想看到的结果,而已。
不多说。