gitbook/趣谈网络协议/docs/8975.md
2022-09-03 22:05:03 +08:00

14 KiB
Raw Permalink Blame History

第11讲 | TCP协议因性恶而复杂先恶后善反轻松

上一节我们讲的UDP基本上包括了传输层所必须的端口字段。它就像我们小时候一样简单相信“网之初性本善不丢包不乱序”。

后来呢我们都慢慢长大了解了社会的残酷变得复杂而成熟就像TCP协议一样。它之所以这么复杂那是因为它秉承的是“性恶论”。它天然认为网络环境是恶劣的丢包、乱序、重传拥塞都是常有的事情一言不合就可能送达不了因而要从算法层面来保证可靠性。

TCP包头格式

我们先来看TCP头的格式。从这个图上可以看出它比UDP复杂得多。

首先源端口号和目标端口号是不可少的这一点和UDP是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。

接下来是包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机,做事当然要稳重,一件件来,面临再复杂的情况,也临危不乱。

还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复。

TCP是靠谱的协议但是这不能说明它面临的网络环境好。从IP层面来讲如果网络状况的确那么差是没有任何可靠性保证的而作为IP的上一层TCP也无能为力唯一能做的就是更加努力不断重传通过各种算法保证。也就是说对于TCP来讲IP层你丢不丢包我管不着但是我在我的层面上会努力保证可靠性。

这有点像如果你在北京,和客户约十点见面,那么你应该清楚堵车是常态,你干预不了,也控制不了,你唯一能做的就是早走。打车不行就改乘地铁,尽力不失约。

接下来有一些状态位。例如SYN是发起一个连接ACK是回复RST是重新连接FIN是结束连接等。TCP是面向连接的因而双方要维护连接的状态这些带状态位的包的发送会引起双方的状态变更。

不像小时候,随便一个不认识的小朋友都能玩在一起,人大了,就变得礼貌,优雅而警觉,人与人遇到会互相热情的寒暄,离开会不舍地道别,但是人与人之间的信任会经过多次交互才能建立。

还有一个重要的就是窗口大小。TCP要做流量控制通信双方各声明一个窗口标识自己当前能够的处理能力别发送的太快撑死我也别发的太慢饿死我。

作为老司机做事情要有分寸待人要把握尺度既能适当提出自己的要求又不强人所难。除了做流量控制以外TCP还会做拥塞控制对于真正的通路堵车不堵车它无能为力唯一能做的就是控制自己也即控制发送的速度。不能改变世界就改变自己嘛。

作为老司机,要会自我控制,知进退,知道什么时候应该坚持,什么时候应该让步。

通过对TCP头的解析我们知道要掌握TCP协议重点应该关注以下几个问题

  • 顺序问题 ,稳重不乱;

  • 丢包问题,承诺靠谱;

  • 连接维护,有始有终;

  • 流量控制,把握分寸;

  • 拥塞控制,知进知退。

TCP的三次握手

所有的问题,首先都要先建立一个连接,所以我们先来看连接维护问题。

TCP的连接建立我们常常称为三次握手。

A您好我是A。

B您好A我是B。

A您好B。

我们也常称为“请求->应答->应答之应答”的三个回合。这个看起来简单,其实里面还是有很多的学问,很多的细节。

首先,为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?

我们还是假设这个通路是非常不可靠的A要发起一个连接当发了第一个请求杳无音信的时候会有很多的可能性比如第一个请求包丢了再如没有丢但是绕了弯路超时了还有B没有响应不想和我连接。

A不能确认结果于是再发再发。终于有一个请求包到了B但是请求包到了B的这个事情目前A还是不知道的A还有可能再发。

B收到了请求包就知道了A的存在并且知道A要和它建立连接。如果B不乐意建立连接则A会重试一阵后放弃连接建立失败没有问题如果B是乐意建立连接的则会发送应答包给A。

当然对于B来说这个应答包也是一入网络深似海不知道能不能到达A。这个时候B自然不能认为连接是建立好了因为应答包仍然会丢会绕弯路或者A已经挂了都有可能。

而且这个时候B还能碰到一个诡异的现象就是A和B原来建立了连接做了简单通信后结束了连接。还记得吗A建立连接的时候请求包重复发了几次有的请求包绕了一大圈又回来了B会认为这也是一个正常的的请求的话因此建立了连接可以想象这个连接不会进行下去也没有个终结的时候纯属单相思了。因而两次握手肯定不行。

B发送的应答可能会发送多次但是只要一次到达AA就认为连接已经建立了因为对于A来讲他的消息有去有回。A会给B发送应答之应答而B也在等这个消息才能确认连接的建立只有等到了这个消息对于B来讲才算它的消息有去有回。

当然A发给B的应答之应答也会丢也会绕路甚至B挂了。按理来说还应该有个应答之应答之应答这样下去就没底了。所以四次握手是可以的四十次都可以关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回就基本可以了。

好在大部分情况下A和B建立了连接之后A会马上发送数据的一旦A发送数据则很多问题都得到了解决。例如A发给B的应答丢了当A后续发送的数据到达的时候B可以认为这个连接已经建立或者B压根就挂了A发送的数据会报错说B不可达A就知道B出事情了。

当然你可以说A比较坏就是不发数据建立连接后空着。我们在程序设计的时候可以要求开启keepalive机制即使没有真实的数据包也有探活包。

另外你作为服务端B的程序设计者对于A这种长时间不发包的客户端可以主动关闭从而空出资源来给其他客户端使用。

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP包的序号的问题

A要告诉B我这面发起的包的序号起始是从哪个号开始的B同样也要告诉AB发起的包的序号起始是从哪个号开始的。为什么序号不能都从1开始呢因为这样往往会出现冲突。

例如A连上B之后发送了1、2、3三个包但是发送3的时候中间丢了或者绕路了于是重新发送后来A掉线了重新连上B后序号又从1开始然后发送2但是压根没想发送3但是上次绕路的那个3又回来了发给了BB自然认为这就是下一个包于是发生了错误。

因而每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的可以看成一个32位的计数器每4微秒加一如果计算一下如果到重复需要4个多小时那个绕路的包早就死翘翘了因为我们都知道IP包头里面有个TTL也即生存时间。

好了,双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。

一开始客户端和服务端都处于CLOSED状态。先是服务端主动监听某个端口处于LISTEN状态。然后客户端主动发起连接SYN之后处于SYN-SENT状态。服务端收到发起的连接返回SYN并且ACK客户端的SYN之后处于SYN-RCVD状态。客户端收到服务端发送的SYN和ACK之后发送ACK的ACK之后处于ESTABLISHED状态因为它一发一收成功了。服务端收到ACK的ACK之后处于ESTABLISHED状态因为它也一发一收了。

TCP四次挥手

好了,说完了连接,接下来说一说“拜拜”,好说好散。这常被称为四次挥手。

AB啊我不想玩了。

B你不想玩了啊我知道了。

这个时候还只是A不想玩了也即A不会再发送数据但是B能不能在ACK的时候直接关闭呢当然不可以了很有可能A是发完了最后的数据就准备不玩了但是B还没做完自己的事情还是可以发送数据的所以称为半关闭的状态。

这个时候A可以选择不再接收数据了也可以选择最后再接收一段数据等待B也主动关闭。

BA啊好吧我也不玩了拜拜。

A好的拜拜。

这样整个连接就关闭了。但是这个过程有没有异常情况呢?当然有,上面是和平分手的场面。

A开始说“不玩了”B说“知道了”这个回合是没什么问题的因为在此之前双方还处于合作的状态如果A说“不玩了”没有收到回复则A会重新发送“不玩了”。但是这个回合结束之后就有可能出现异常情况了因为已经有一方率先撕破脸。

一种情况是A说完“不玩了”之后直接跑路是会有问题的因为B还没有发起结束而如果A跑路B就算发起结束也得不到回答B就不知道该怎么办了。另一种情况是A说完“不玩了”B直接跑路也是有问题的因为A不知道B是还有事情要处理还是过一会儿会发送结束。

那怎么解决这些问题呢TCP协议专门设计了几个状态来处理这些问题。我们来看断开连接的时候的状态时序图

断开的时候我们可以看到当A说“不玩了”就进入FIN_WAIT_1的状态B收到“A不玩”的消息后发送知道了就进入CLOSE_WAIT的状态。

A收到“B说知道了”就进入FIN_WAIT_2的状态如果这个时候B直接跑路则A将永远在这个状态。TCP协议里面并没有对这个状态的处理但是Linux有可以调整tcp_fin_timeout这个参数设置一个超时时间。

如果B没有跑路发送了“B也不玩了”的请求到达A时A发送“知道B也不玩了”的ACK后从FIN_WAIT_2状态结束按说A可以跑路了但是最后的这个ACK万一B收不到呢则B会重新发一个“B不玩了”这个时候A已经跑路了的话B就再也收不到ACK了因而TCP协议要求A最后等待一段时间TIME_WAIT这个时间要足够长长到如果B没收到ACK的话“B说不玩了”会重发的A会重新发一个ACK并且足够时间到达B。

A直接跑路还有一个问题是A的端口就直接空出来了但是B不知道B原来发过的很多包很可能还在路上如果A的端口被一个新的应用占用了这个新的应用会收到上个连接中B发过来的包虽然序列号是重新生成的但是这里要上一个双保险防止产生混乱因而也需要等足够长的时间等到原来B发送的所有的包都死翘翘再空出端口来。

等待的时间设为2MSLMSLMaximum Segment Lifetime报文最大生存时间它是任何报文在网络上存在的最长时间超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的而IP头中有一个TTL域是IP数据报可以经过的最大路由数每经过一个处理他的路由器此值就减1当此值为0则数据报将被丢弃同时发送ICMP报文通知源主机。协议规定MSL为2分钟实际应用中常用的是30秒1分钟和2分钟等。

还有一个异常情况就是B超过了2MSL的时间依然没有收到它发的FIN的ACK怎么办呢按照TCP的原理B当然还会重发FIN这个时候A再收到这个包之后A就表示我已经在这里等了这么长时间了已经仁至义尽了之后的我就都不认了于是就直接发送RSTB就知道A早就跑了。

TCP状态机

将连接建立和连接断开的两个时序状态图综合起来就是这个著名的TCP的状态机。学习的时候比较建议将这个状态机和时序状态机对照着看不然容易晕。

在这个图中加黑加粗的部分是上面说到的主要流程其中阿拉伯数字的序号是连接过程中的顺序而大写中文数字的序号是连接断开过程中的顺序。加粗的实线是客户端A的状态变迁加粗的虚线是服务端B的状态变迁。

小结

好了,这一节就到这里了,我来做一个总结:

  • TCP包头很复杂但是主要关注五个问题顺序问题丢包问题连接维护流量控制拥塞控制

  • 连接的建立是经过三次握手,断开的时候四次挥手,一定要掌握的我画的那个状态图。

最后,给你留两个思考题。

  1. TCP的连接有这么多的状态你知道如何在系统中查看某个连接的状态吗

  2. 这一节仅仅讲了连接维护问题,其实为了维护连接的状态,还有其他的数据结构来处理其他的四个问题,那你知道是什么吗?

欢迎你留言和我讨论。趣谈网络协议,我们下期见!