TCP协议具有可靠性,从一个终端有序地发出多个数据包,经过一个复杂的网络环境,到达目的地的时候会变得无序,而可靠性要求数据恢复到原始的顺序。
TCP 是一个传输层协议,TCP 发送数据的时候,往往不会将数据一次性发送,而是将数据拆分成很多个部分,然后再逐个发送。同样的,在目的地,TCP 协议又需要逐个接收数据。
这里有很多原因,比如为了稳定性,一次发送的数据越多,出错的概率越大。再比如说为了效率,网络中有时候存在着并行的路径,拆分数据包就能更好地利用这些并行的路径。再有,比如发送和接收数据的时候,都存在着缓冲区。
缓冲区是在内存中开辟的一块区域,目的是缓冲。因为大量的应用频繁地通过网卡收发数据,这个时候,网卡只能一个一个处理应用的请求。当网卡忙不过来的时候,数据就需要排队,也就是将数据放入缓冲区。如果每个应用都随意发送很大的数据,可能导致其他应用实时性遭到破坏。
所以,TCP 协议,会将数据拆分成不超过缓冲区大小的一个个部分。每个部分有一个独特的名词,叫作 TCP 段(TCP Segment)。
在接收数据的时候,一个个 TCP 段又被重组成原来的数据。
拆包:
数据经过拆分,然后传输,然后在目的地重组。
粘包:
有时候,如果发往一个目的地的多个数据太小了,为了防止多次发送占用资源,TCP 协议有可能将它们合并成一个 TCP 段发送,在目的地再还原成多个数据。
粘包是将多个数据合并成一个 TCP 段发送
下图是一个 TCP 段的格式:
Source Port/Destination Port 描述的是发送端口号和目标端口号,代表发送数据的应用程序和接收数据的应用程序。比如 80 往往代表 HTTP 服务,22 往往是 SSH 服务……
Sequence Number 和 Achnowledgment Number 是保证可靠性的两个关键。
Data Offset 是一个偏移量。这个量存在的原因是 TCP Header 部分的长度是可变的,因此需要一个数值来描述数据从哪个字节开始。
Reserved 是很多协议设计会保留的一个区域,用于日后扩展能力。
URG/ACK/PSH/RST/SYN/FIN 是几个标志位,用于描述 TCP 段的行为。
URG 代表这是一个紧急数据,比如远程操作的时候,用户按下了 Ctrl+C,要求终止程序,这种请求需要紧急处理。
ACK 代表响应,我们在“02 | 传输层协议 TCP:TCP 为什么握手是 3 次、挥手是 4 次?”讲到过,所有的消息都必须有 ACK,这是 TCP 协议确保稳定性的一环。
PSH 代表数据推送,也就是在传输数据的意思。
SYN 同步请求,也就是申请握手。
FIN 终止请求,也就是挥手。
以上这 5 个标志位,每个占了一个比特,可以混合使用。比如 ACK 和 SYN 同时为 1,代表同步请求和 响应被合并了。这也是 TCP 协议,为什么是三次握手的原因之一。
Window 也是 TCP 保证稳定性并进行流量控制的工具
Checksum 是校验和,用于校验 TCP 段有没有损坏
Urgent Pointer 指向最后一个紧急数据的序号(Sequence Number)。它存在的原因是:有时候紧急数据是连续的很多个段,所以需要提前告诉接收方进行准备 。
Options 中存储了一些可选字段,比如 MSS(Maximun Segment Size)。
Padding 存在的意义是因为 Options 的长度不固定,需要 Pading 进行对齐。
Sequence Number 和 Achnowledgment Number 是保证可靠性的两个关键。
稳定性要求数据无损地传输,也就是说拆包获得数据,又需要恢复到原来的样子。而在复杂的网络环境当中,即便所有的段是顺序发出的,也不能保证它们顺序到达,因此,发出的每一个 TCP 段都需要有序号。这个序号,就是 Sequence Number(Seq)
但是这样又会产生一个新的问题——接收方如果要回复发送方,也需要这个 Seq。而网络的两个终端,去同步一个自增的序号是非常困难的。因为任何两个网络主体间,时间都不能做到完全同步,又没有公共的存储空间,无法共享数据,更别说实现一个分布式的自增序号了。
其实这个问题的本质就好像两个人在说话一样,我们要确保他们说出去的话,和回答之间的顺序。因为 TCP 是一个双工的协议,两边可能会同时说话。所以聪明的科学家想到了确定一句话的顺序,需要两个值去描述——也就是发送的字节数和接收的字节数。
我们重新定义一下 Seq(如上图所示),对于任何一个接收方,如果知道了发送者发送某个 TCP 段时,已经发送了多少字节的数据,那么就可以确定发送者发送数据的顺序。
但是这里有一个问题。如果接收方也向发送者发送了数据请求(或者说双方在对话),接收方就不知道发送者发送的数据到底对应哪一条自己发送的数据了。
举个例子:下面 A 和 B 的对话中,我们可以确定他们彼此之间接收数据的顺序。但是无法确定数据之间的关联关系,所以只有 Sequence Number 是不够的。
A:今天天气好吗?
A:今天你开心吗?
B:开心
B:天气不好
人类很容易理解这几句话的顺序,但是对于机器来说就需要特别的标注。因此我们还需要另一个数据,就是每个 TCP 段发送时,发送方已经接收了多少数据。用 Acknowledgement Number 表示,下面简写为 ACK。
下图中,终端发送了三条数据,并且接收到四条数据,通过观察,根据接收到的数据中的 Seq 和 ACK,将发送和接收的数据进行排序。
例如上图中,发送方发送了 100 字节的数据,而接收到的(Seq = 0 和 Seq =100)的两个封包,都是针对发送方(Seq = 0)这个封包的。发送 100 个字节,所以接收到的 ACK 刚好是 100。说明(Seq= 0 和 Seq= 100)这两个封包是针对接收到第 100 个字节数据后,发送回来的。这样就确定了整体的顺序。
注意,无论 Seq 还是 ACK,都是针对“对方”而言的。是对方发送的数据和对方接收到的数据
思考这样一个问题,你和小明聊天, 你会发现,你发出的消息,你可以确定顺序。因为用了聊天工具,接收到消息的时间不确定,你需要理解内容才能确认小明具体那句话回复的是你的哪句话。那么如果是机器,如何确定小明哪句话是针对哪句话回复呢?那就需要小明的回复带上(Seq, Ack),看到小明的Ack,就知道小明是收到多少自己发送的消息后才进行的回复。通过ACK,你知道小明回复的是你的第1条?第2条?第3条?……还是第k条信息。ACK是小明累计收到的信息数。 如果你要针对小明的回复(Seq, ACK, 内容)进行回复,你就把ACK设置成小明的Seq。
MSS是一个 TCP Header 中的可选项(Options),这个可选项控制了 TCP 段的大小,它是一个协商字段(Negotiate)。协议是双方都要遵循的标准,因此配置往往不能由单方决定,需要双方协商。
TCP 段的大小(MSS)涉及发送、接收缓冲区的大小设置,双方实际发送接收封包的大小,对拆包和粘包的过程有指导作用,因此需要双方去协商。
如果这个字段设置得非常大,就会带来一些影响。
首先对方可能会拒绝,作为服务的提供方,你可能不会愿意接收太大的 TCP 段。因为大的 TCP 段,会降低性能,比如内存使用的性能。
还有就是资源的占用。一个用户占用服务器太多的资源,意味着其他的用户就需要等待或者降低他们的服务质量。
其次,支持 TCP 协议工作的 IP 协议,工作效率会下降。TCP 协议不肯拆包,IP 协议就需要拆出大量的包。那么 IP 协议为什么需要拆包呢?这是因为在网络中,每次能够传输的数据不可能太大,这受限于具体的网络传输设备,也就是物理特性。但是 IP 协议,拆分太多的封包并没有意义。因为可能会导致属于同个 TCP 段的封包被不同的网络路线传输,这会加大延迟。同时,拆包,还需要消耗硬件和计算资源。
那是不是 MSS 越小越好呢?MSS 太小的情况下,会浪费传输资源(降低吞吐量)。因为数据被拆分之后,每一份数据都要增加一个头部。如果 MSS 太小,那头部的数据占比会上升,这让吞吐量成为一个灾难。所以在使用的过程当中,MSS 的配置,往往都是一个折中的方案。