第11讲 | TCP协议(上):因性恶而复杂,先恶后善反轻松

第11讲 | 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。我们也常称为

“请求 -> 应答 -> 应答之应答”的三个回合。

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

  tcp是全双工,如果没有第三次的握手,服务端不能确认客户端是否ready,不知道什么时候可以往客户端发数据包。三次的握手刚好两边都互相确认对方已经ready。

三次握手确认两件事: 1. 各自确认对方的存在 2. 约定初始的数据包的序列号

链接建立后,每次要发送端和服务端都要重新商定序号

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

 

双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,

在连接建立的过程中,双方的状态变化时序图就像这样。

 

TCP 四次挥手

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 发送的所有的包都死翘翘,再空出端口来。

等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

  因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

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

 

TCP 状态机

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

 

小结

我来做一个总结:

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

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

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

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

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

对于三次握手的理解:

信息论中,有个很重要的思想:要想消除信息的不确定性,就得引入信息。将这个思想应用到TCP中,很容易理解TCP的三次握手和四次挥手的必要性:它们的存在以及复杂度,就是为了消除不确定性,这里我们叫「不可靠性」吧。拿三次握手举例:
为了描述方便,将通信的两端用字母A和B替代。A要往B发数据,A要确定两件事:
1. B在“那儿”,并且能接受数据 —— B确实存在,并且是个“活人”,能听得见
2. B能回应 —— B能发数据,能说话
为了消除这两个不确定性,所以必须有前两次握手,即A发送了数据,B收到了,并且能回应——“ACK”

同样的,对于B来说,它也要消除以上两个不确定性,通过前两次握手,B知道了A能说,但是不能确定A能听,这就是第三次握手的必要性。

当然你可能会问,增加第四次握手有没有必要?从信息论的角度来说,已经不需要了,因为它的增加也无法再提高「确定性」

 

之前对于TCP的感知就是简单的“三次握手”,“四次挥手”,觉得自己掌握了精髓,但随便一个问题就懵了,比如,
- 客户端什么时候建立连接?
> 根据以前的认知,会以为是“三次握手”后,双方同时建立连接。很显然是做不到的,客户端不知道“应答的应答”有没有到达,以及什么时候到达。。。
- 客户端什么时候断开连接?
> 不仔细思考的话,就会说“四次挥手”之后喽,但事实上,客户端发出最后的应答(第四次“挥手”)后,永远无法知道有没有到达。于是有了2MSL的等待,在不确定的网络中,把问题最大程度地解决。

TCP的状态机,以及很多的设计细节,都是为了解决不稳定的网络问题,让我们看到了在无法改变不稳定的底层网络时,人类的智慧是如果建立一个基本可靠稳定的网络的。

posted @ 2021-05-31 09:38  wang-sir  阅读(65)  评论(0)    收藏  举报