一、互联网和传输层协议
01 | 互联网的构成
1.1 | 网络的组成
整个世界可以看作一张巨大的、立体的网。可以想象,用无数个节点构成一个个小型网络,再用小型网络组成中型网络,再组成大型网络,以此类推,最后组成完整的一个如星河般的世界。
1.2 | 公司内网
公司网络从本地网络服务提供商 (Internet Service Provider) 接入,然后内部再分成一个个子网。上图中看到的线路,也被称作通信链路(Communication Link),用于传输网络信号。你可以观察到,有的网络节点,同时接入了 2 条以上的链路,这个时候因为路径发生了分叉,数据传输到这些节点需要选择方向,因此我们在这些节点需要进行交换(Switch)。数据发生交换的时候,会先从一条链路进入交换设备,然后缓存下来,再转发(切换)到另一条路径,如下图所示:
交换技术的本质,就是让数据切换路径。因为,网络中的数据是以分组或封包(Packet)的形式传输,因此这个技术也称作封包交换技术(Packet Switch)。
比如要传递一首 2Mb 的 MP3 的歌曲,歌曲本身不是一次传输完成的,而是被拆分成很多个封包。每个封包只有歌曲中的一部分数据,而封包一旦遇到岔路口,就需要封包交换技术帮助每个封包选择最合理的路径。
网络中,常见的具有交换能力的设备是路由器(Router)和链路层交换机(Link-Layer Switch)。通常情况下,两个网络可以通过路由器进行连接,多台设备可以通过交换机进行连接。但是路由器通常也具有交换机的功能。
1.3 | 移动网络
网络传输需要通信链路(Communication Link),而通信链路是一个抽象概念。
这里说的抽象,就是面向对象中抽象类和继承类的关系,比如同轴电缆是通信链路,无线信号的发送接收器可以构成通信链路,蓝牙信道也可以构成通信链路。
移动网络中,无线信号构成了通信链路。在移动网络的设计中,通信的核心被称作蜂窝塔(Cellular Tower),有时候也称作基站(BaseStation)。之所以有这样的名称,是因为每个蜂窝塔只覆盖一个六边形的范围,如果要覆盖一个很大的区域就需要很多的蜂窝塔(六边形)排列在一起,像极了蜜蜂的巢穴。这种六边形的结构,可以让信号无死角地覆盖。想象一下,如果是圆形结构,那么圆和圆之间就会有间隙,造成一部分无法覆盖的信号死角,而六边形就完美地解决了这个问题。
对于构成移动网络最小的网络结构——蜂窝网络来说
国家或全球网络提供商将网络供给处于蜂窝网络边缘的路由器,路由器连接蜂窝塔,再通过蜂窝塔(基站)提供给处于六边形地区中的设备。通常是*别的网络服务提供商负责部署基站,比如中国电信、中国联通。将网络提供给一个子网的行为,通常称为网络提供(Network Provider),反过来,对一个子网连接提供商的网络,称为网络接入(Network Access)。
随着移动网络的发展,一个蜂窝网格中的设备越来越多,也出现了基站覆盖有重叠关系的网格
这样设计的好处是,当一个基站过载、出现故障,或者用户设备周边信号出现不稳定,就可以切换到另一个基站的网络,不影响用户继续使用网络服务。
另一方面,在一定范围内的区域,离用户较近的地方还可以部署服务器,帮助用户完成计算。这相当于计算资源的下沉,称为边缘计算。相比中心化的计算,边缘计算延迟低、链路短,能够将更好的体验带给距离边缘计算集群最近的节点。从而让用户享受到更优质、延迟更低、算力更强的服务。
1.4 | 家用网络
家用网络现在已经发展成一种网格状的连接。一方面家用网络会通过路由器接入本地 ISP 提供的网络服务。另一方面,一些设备,比如电脑、笔记本、手机、冰箱等都可以直接和路由器连接。路由器也承担了链路层网关的作用,作为家用电器之间信息的交换设备。
还有一些家用设备,比如说 10 多块钱的灯泡,不太适合内部再嵌入一个几十块钱可以接收 WI-FI 的芯片,这个时候就可以考虑用蓝牙控制电灯。路由器提供蓝牙不现实,因此一些家用电器也承担了蓝牙设备的控制器——比如说智能音箱。上图中的智能音箱把家用网络带向了一个网格状,有的设备会同时连接路由器(WI-FI)和智能音箱,这样手机和音箱都可以直接控制这些设备。这样的设计,即便网络断开,仍然可以控制这些家用设备。
1.5 | 整体关系
最顶部的全球或国家大型的 ISP 之间联网,构成了网络的主干。然后区域性的 ISP 承接主干网络,在这个基础之上再向家庭和公司提供接入服务。移动蜂窝网络因为部署复杂,往往也是由大型 ISP 直接提供。
1.6 | 数据的传输
为了传递数据,在网络中有几个特别重要的抽象。
- 最终提供服务或者享受服务的设备,称为终端(Terminal),或者端系统(End System),有时候简单称为主机(Host)
- 可以把网络传输分成两类,一类是端到端(Host-to-Host)的能力,由 TCP/IP 协议群提供。还有一类是广播的能力,是一对多、多对多的能力,可以看作是端到端(Host-to-Host)能力的延伸。
比如说:电脑、手机、冰箱、汽车等,我们都可以看作是一个主机(Host)。
可以思考一下,一个北京的主机(Host)向一个深圳的主机(Host)发送消息。那么,中间会穿越大量的网络节点,这些节点可以是路由器、交换机、基站等。在发送消息的过程中,可能跨越很多网络、通过很多边缘,也可能会通过不同的网络提供商提供的网络……而且,传输过程中,可能会使用不同材质的通信链路(Communication Link),比如同轴电缆、双绞线、光纤,或者通过无线传输的 WI-FI、卫星等。
网络基础设施往往不能一次性传输太大的数据量,因此通常会将数据分片传输。
比如传输一个 MP3,我们会将 MP3 内容切分成很多个组,每个组也称作一个封包,英文都是 Packet。这样,如果一个封包损坏,只需要重发损坏的封包,而不需要重发所有数据。
另一方面,网络中两点间的路径非常多,如果一条路径阻塞了,部分封包可以考虑走其他路径。发送端将数据拆分成封包(Packet),封包在网络中遇到岔路,由交换器和路由器节点决定走向.下图是对封包交换技术的一个演示。
[========]
02 | 传输层协议TCP
TCP 和 UDP 是今天应用最广泛的传输层协议,拥有最核心的垄断地位。今天互联网的整个传输层,几乎都是基于这两个协议打造的。无论是应用开发、框架设计选型、做底层和优化,还是定位线上问题,只要碰到网络,就逃不开 TCP 协议相关的知识。
2.1 | TCP协议
TCP(Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议
2.2 | 主机到主机(Host-To-Host)
TCP 提供的是 Host-To-Host 传输,一台主机通过 TCP 发送数据给另一台主机。
这里的主机(Host)是一个抽象的概念,可以是手机、平板、手表等。收发数据的设备都是主机,所以双方是平等的。
TCP 协议往上是应用到应用(Application-To-Application)的协议。
什么是应用到应用的协议呢?比如你用微信发信息给张三,你的微信客户端、微信聊天服务都是应用。微信有自己的聊天协议,微信的聊天协议是应用到应用的协议;如果微信的聊天协议想要工作,就需要一个主机到主机的协议帮助它实现通信。
而 TCP 上层有太多的应用,不仅仅有微信,还有原神、抖音、网易云音乐……因此 TCP 上层的应用层协议使用 TCP 能力的时候,需要告知 TCP 是哪个应用——这就是端口号。端口号用于区分应用.
TCP 要实现主机到主机通信,就需要知道主机们的网络地址(IP 地址),但是 TCP 不负责实际地址到地址(Address-To-Address)的传输,因此 TCP 协议把 IP 地址给底层的互联网层处理。
互联网层,也叫网络层(Network Layer),提供地址到地址的通信,IP 协议就在这一层工作。互联网层解决地址到地址的通信,但是不负责信号在具体两个设备间传递。因此,网络层会调用下方的链路层在两个相邻设备间传递信息。当信号在两个设备间传递的时候,科学家又设计出了物理层封装最底层的物理设备、传输介质等,由最下方的物理层提供最底层的传输能力。
以上的 5 层架构,我们称为互联网协议群,也称作 TCP/IP 协议群。主机到主机(Host-To-Host)为应用提供应用间通信的能力。
2.3 | 连接和会话
连接(Connection)——连接是数据传输双方的契约。
连接是通信双方的一个约定,目标是让两个在通信的程序之间产生一个默契,保证两个程序都在线,而且尽快地响应对方的请求,这就是连接(Connection)。
设计上,连接是一种传输数据的行为。传输之前,建立一个连接。具体来说,数据收发双方的内存中都建立一个用于维护数据传输状态的对象,比如双方 IP 和端口是多少?现在发送了多少数据了?状态健康吗?传输速度如何?
所以,连接是网络行为状态的记录。
与连接相关的是会话(Session),会话是应用的行为。
比如微信里张三和你聊天,那么张三和你建立一个会话。你要和张三聊天,你们创建一个聊天窗口,这个就是会话。你开始 Typing,开始传输数据,你和微信服务器间建立一个连接。如果你们聊一段时间,各自休息了,约定先不要关微信,1 个小时后再回来。那么连接会断开,因为聊天窗口没关,所以会话还在。
有些系统设计中,会话会自动重连(也就是重新创建连接),或者帮助创建连接。 此外,会话也负责在多次连接中保存状态,比如 HTTP Session 在多次 HTTP 请求(连接)间保持状态(如用户信息)。
总结下,会话是应用层的概念,连接是传输层的概念。
2.4 | 双工/单工问题
在任何一个时刻,如果数据只能单向发送,就是单工,所以单工需要至少一条线路。如果在某个时刻数据可以向一个方向传输,也可以向另一个方向反方向传输,而且交替进行,叫作半双工;半双工需要至少 1 条线路。最后,如果任何时刻数据都可以双向收发,这就是全双工,全双工需要大于 1 条线路。当然这里的线路,是一个抽象概念,你可以并发地处理信号,达到模拟双工的目的。
TCP 是一个双工协议,数据任何时候都可以双向传输。这就意味着客户端和服务端可以平等地发送、接收信息。正因为如此,客户端和服务端在 TCP 协议中有一个平等的名词——Host(主机)。
2.5 | 可靠性
可靠性指数据保证无损传输。如果发送方按照顺序发送,然后数据无序地在网络间传递,就必须有一种算法在接收方将数据恢复原有的顺序。另外,如果发送方同时要把消息发送给多个接收方,这种情况叫作多播,可靠性要求每个接收方都无损收到相同的副本。多播情况还有强可靠性,就是如果有一个消息到达任何一个接收者,那么所有接受者都必须收到这个消息。
2.6 | TCP的握手和挥手
TCP 是一个连接导向的协议,设计有建立连接(握手)和断开连接(挥手)的过程。TCP 没有设计会话(Session),因为会话通常是一个应用的行为。
2.7 | TCP协议的基本操作
TCP 协议有这样几个基本操作:
- 如果一个 Host 主动向另一个 Host 发起连接,称为 SYN(Synchronization),请求同步;
- 如果一个 Host 主动断开请求,称为 FIN(Finish),请求完成;
- 如果一个 Host 给另一个 Host 发送数据,称为 PSH(Push),数据推送。
以上 3 种情况,接收方收到数据后,都需要给发送方一个 ACK(Acknowledgement)响应。
请求/响应的模型是可靠性的要求,如果一个请求没有响应,发送方可能会认为自己需要重发这个请求。
2.8 | 建立连接的过程(3次握手)
因为要保持连接和可靠性约束,TCP 协议要保证每一条发出的数据必须给返回,返回数据叫作 ACK(也就是响应)。
- 客户端发消息给服务端(SYN)
- 服务端准备好进行连接
- 服务端针对客户端的 SYN 给一个 ACK
你可能觉得,2 次握手就足够了。但其实不是,因为服务端还没有确定客户端是否准备好了。比如步骤 3 之后,服务端马上给客户端发送数据,这个时候客户端可能还没有准备好接收数据。因此还需要增加一个过程。
接下来还会发生以下操作:
- 服务端发送一个 SYN 给客户端
- 客户端准备就绪
- 客户端给服务端发送一个 ACK
虽然是6步操作,但其实是三次握手:
- 步骤 1 是 1 次握手;
- 步骤 2 是服务端的准备,不是数据传输,因此不算握手;
- 步骤 3 和步骤 4,因为是同时发生的,可以合并成一个 SYN-ACK 响应,作为一条数据传递给客户端,因此是第 2 次握手;
- 步骤 5 不算握手;
- 步骤 6 是第 3 次握手。
为了方便理解步骤 3 和步骤 4,这里我画了一张图。可以看到下图中 SYN 和 ACK 被合并了,因此建立连接一共需要 3 次握手
2.9 | 断开连接的过程(4次挥手)
- 客户端要求断开连接,发送一个断开的请求,这个叫作(FIN)。
- 服务端收到请求,然后给客户端一个 ACK,作为 FIN 的响应。
- 这里你需要思考一个问题,可不可以像握手那样马上传 FIN 回去?
其实这个时候服务端不能马上传 FIN,因为断开连接要处理的问题比较多,比如说服务端可能还有发送出去的消息没有得到 ACK;也有可能服务端自己有资源要释放。因此断开连接不能像握手那样操作——将两条消息合并。所以,服务端经过一个等待,确定可以关闭连接了,再发一条 FIN 给客户端。 - 客户端收到服务端的 FIN,同时客户端也可能有自己的事情需要处理完,比如客户端有发送给服务端没有收到 ACK 的请求,客户端自己处理完成后,再给服务端发送一个 ACK。
2.10 | 总结
在学习 3 次握手、4 次挥手时,一定要理解为什么这么设计。
- TCP 提供连接(Connection),让双方的传输更加稳定、安全。
- TCP 没有直接提供会话,因为应用对会话的需求多种多样,比如聊天程序会话在保持双方的聊天记录,电商程序会话在保持购物车、订单一致,所以会话通常在 TCP 连接上进一步封装,在应用层提供。
- TCP 是一个面向连接的协议(Connection -oriented Protocol),说的就是 TCP 协议参与的双方(Host)在收发数据之前会先建立连接。后面我们还会学习 UDP 协议,UDP 是一个面向报文(Datagram-oriented)的协议——协议双方不需要建立连接,直接传送报文(数据)。
- 最后,连接需要消耗更多的资源。比如说,在传输数据前,必须先协商建立连接。因此,不是每种场景都应该用连接导向的协议。像视频播放的场景,如果使用连接导向的协议,服务端每向客户端推送一帧视频,客户端都要给服务端一次响应,这是不合理的。
最后:TCP 为什么是 3 次握手,4 次挥手?
【解析】TCP 是一个双工协议,为了让双方都保证,建立连接的时候,连接双方都需要向对方发送 SYC(同步请求)和 ACK(响应)。
握手阶段双方都没有烦琐的工作,因此一方向另一方发起同步(SYN)之后,另一方可以将自己的 ACK 和 SYN 打包作为一条消息回复,因此是 3 次握手——需要 3 次数据传输。
到了挥手阶段,双方都可能有未完成的工作。收到挥手请求的一方,必须马上响应(ACK),表示接收到了挥手请求。类比现实世界中,你收到一个 Offer,出于礼貌你先回复考虑一下,然后思考一段时间再回复 HR 最后的结果。最后等所有工作结束,再发送请求中断连接(FIN),因此是 4 次挥手。
[========]
03 | TCP封包格式
本节从稳定性角度深挖 TCP 协议的运作机制。
简单来说,可靠性就是让数据无损送达。但若是考虑到成本,就会变得非常复杂——因为还需要尽可能地提升吞吐量、降低延迟、减少丢包率。
TCP 协议具有很强的实用性,而可靠性又是 TCP 最核心的能力,所以理所当然成为面试官们津津乐道的问题。具体来说,从一个终端有序地发出多个数据包,经过一个复杂的网络环境,到达目的地的时候会变得无序,而可靠性要求数据恢复到原始的顺序。这里先提出两个问题:
- TCP 协议是如何恢复数据的顺序的?
- 拆包和粘包的作用是什么?
3.1 | TCP的拆包和粘包
TCP 发送数据的时候,往往不会将数据一次性发送;而是将数据拆分成很多个部分,然后再逐个发送。像下图这样:
同样的,在目的地,TCP 协议又需要逐个接收数据。
TCP之所以不一次性发送完所有数据,有很多原因:
- 为了稳定性,一次发送的数据越多,出错的概率越大。
- 为了效率,网络中有时候存在着并行的路径,拆分数据包就能更好地利用这些并行的路径。
- 发送和接收数据的时候,都存在着缓冲区。
- 如果数据的大小超过一个页表,可能会存在页面置换问题,造成性能的损失。
缓冲区是在内存中开辟的一块区域,目的是缓冲。因为大量的应用频繁地通过网卡收发数据,这个时候,网卡只能一个一个处理应用的请求。当网卡忙不过来的时候,数据就需要排队,也就是将数据放入缓冲区。如果每个应用都随意发送很大的数据,可能导致其他应用实时性遭到破坏。
总之,方方面面的原因:在传输层封包不能太大。这种限制,往往是以缓冲区大小为单位的。
也就是 TCP 协议,会将数据拆分成不超过缓冲区大小的一个个部分。每个部分有一个独特的名词,叫作 TCP 段(TCP Segment)。
在接收数据的时候,一个个 TCP 段又被重组成原来的数据。
- 拆包:数据经过拆分,然后传输,然后在目的地重组。拆包是将数据拆分成多个 TCP 段传输。
- 粘包:将多个数据合并成一个 TCP 段发送。如果发往一个目的地的多个数据太小了,为了防止多次发送占用资源,TCP 协议有可能将它们合并成一个 TCP 段发送,在目的地再还原成多个数据。
3.2 | TCP Segment
TCP段格式如下:
如图所示,TCP 的很多配置选项和数据粘在了一起,作为一个 TCP 段。
TCP 协议就是依靠每一个 TCP 段工作的,所以每认识一个 TCP 的能力,几乎都会找到在 TCP Segment 中与之对应的字段。
- Source Port/Destination Port 描述的是发送端口号和目标端口号,代表发送数据的应用程序和接收数据的应用程序。比如 80 往往代表 HTTP 服务,22 往往是 SSH 服务……
- Sequence Number 和 Achnowledgment Number 是保证可靠性的两个关键。具体见下文的讨论。
- Data Offset 是一个偏移量。这个量存在的原因是 TCP Header 部分的长度是可变的,因此需要一个数值来描述数据从哪个字节开始。
- Reserved 是很多协议设计会保留的一个区域,用于日后扩展能力。
- URG/ACK/PSH/RST/SYN/FIN 是几个标志位,用于描述 TCP 段的行为。也就是一个 TCP 封包到底是做什么用的
1)URG 代表这是一个紧急数据,比如远程操作的时候,用户按下了 Ctrl+C,要求终止程序,这种请求需要紧急处理。
2)ACK 代表响应,所有的消息都必须有 ACK,这是 TCP 协议确保稳定性的一环。
3)PSH 代表数据推送,也就是在传输数据的意思。
4)SYN 同步请求,也就是申请握手。
5)FIN 终止请求,也就是挥手。
特别说明:以上这 5 个标志位,每个占了一个比特,可以混合使用。比如 ACK 和 SYN 同时为 1,代表同步请求和响应被合并了。这也是 TCP 协议,为什么是三次握手的原因之一。
6) Window 也是 TCP 保证稳定性并进行流量控制的工具.
7)Checksum 是校验和,用于校验 TCP 段有没有损坏。
8)Urgent Pointer 指向最后一个紧急数据的序号(Sequence Number)。它存在的原因是:有时候紧急数据是连续的很多个段,所以需要提前告诉接收方进行准备。
9)Options 中存储了一些可选字段,比如接下来要讨论的 MSS(Maximun Segment Size)。
10)Padding 存在的意义是因为 Options 的长度不固定,需要 Pading 进行对齐。
3.3 | Sequence Number 和 Acknowledge Number
这里思考一个问题:稳定性要求数据无损地传输,也就是说拆包获得数据,又需要恢复到原来的样子。而在复杂的网络环境当中,即便所有的段是顺序发出的,也不能保证它们顺序到达,因此,发出的每一个 TCP 段都需要有序号。这个序号,就是 Sequence Number(Seq)。
- 发送数据的时候,为每一个 TCP 段分配一个自增的 Sequence Number。
- 接收数据的时候,虽然得到的是乱序的 TCP 段,但是可以通过 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,都是针对“对方”而言的。是对方发送的数据和对方接收到的数据。
3.4 | MSS(Maximum Segment Size)
MSS,也是面试经常会问到的一个 TCP Header 中的可选项(Options),这个可选项控制了 TCP 段的大小,它是一个协商字段(Negotiate)。协议是双方都要遵循的标准,因此配置往往不能由单方决定,需要双方协商。
TCP 段的大小(MSS)涉及发送、接收缓冲区的大小设置,双方实际发送接收封包的大小,对拆包和粘包的过程有指导作用,因此需要双方去协商。
如果MSS设置得非常大,就会带来一些影响。
- 对方可能会拒绝。作为服务的提供方,你可能不会愿意接收太大的 TCP 段。因为大的 TCP 段,会降低性能,比如内存使用的性能。另外,一个用户占用服务器太多的资源,意味着其他的用户就需要等待或者降低他们的服务质量。
- 支持 TCP 协议工作的 IP 协议,工作效率会下降。TCP 协议不肯拆包,IP 协议就需要拆出大量的包。那么 IP 协议为什么需要拆包呢?这是因为在网络中,每次能够传输的数据不可能太大,这受限于具体的网络传输设备,也就是物理特性。但是 IP 协议,拆分太多的封包并没有意义。因为可能会导致属于同个 TCP 段的封包被不同的网络路线传输,这会加大延迟。同时,拆包,还需要消耗硬件和计算资源。
MSS太小的情况下,会浪费传输资源(降低吞吐量)。因为数据被拆分之后,每一份数据都要增加一个头部。如果 MSS 太小,那头部的数据占比会上升,这让吞吐量成为一个灾难。所以在使用的过程当中,MSS 的配置,往往都是一个折中的方案。
最后,TCP 协议是如何恢复数据的顺序的,TCP 拆包和粘包的作用是什么?
【解析】TCP 拆包的作用是将任务拆分处理,降低整体任务出错的概率,以及减小底层网络处理的压力。拆包过程需要保证数据经过网络的传输,又能恢复到原始的顺序。这中间,需要数学提供保证顺序的理论依据。TCP 利用(发送字节数、接收字节数)的唯一性来确定封包之间的顺序关系。
[========]
04 | TCP稳定性:滑动窗口与流量控制
TCP 作为一个传输层协议,最核心的能力是传输。传输需要保证可靠性,还需要控制流速,这两个核心能力均由滑动窗口提供。
4.1 | 请求/响应模型
TCP 中每个发送的请求都需要响应。如果一个请求没有收到响应,发送方就会认为这次发送出现了故障,会触发重发。
大体的模型,和下图很像。但是如果完全和下图一样,每一个请求收到响应之后,再发送下一个请求,吞吐量会很低。因为这样的设计,会产生网络的空闲时间,说白了,就是浪费带宽。带宽没有用满,意味着可以同时发送更多的请求,接收更多的响应。
一种改进的方式,就是让发送方有请求就发送出去,而不是等待响应。通过这样的处理方式,发送的数据连在了一起,响应的数据也连在了一起,吞吐量就提升了。
但是如果可以同时发送的数据真的非常多呢?比如成百上千个 TCP 段都需要发送,这个时候带宽可能会不足。像下图这样,很多个数据封包都需要发送,该如何处理呢?
4.3 | 排队模型
此种情况下,通常我们会考虑排队(Queuing)机制。
考虑这样一个模型,如上图所示,在 TCP 层实现一个队列。新元素从队列的一端(左侧)排队,作为一个未发送的数据封包。开始发送的数据封包,从队列的右侧离开。
这样做就需要多个队列,我们要将未发送的数据从队列中取出,加入发送中的队列。然后再将发送中的数据,收到 ACK 的部分取出,放入已接收的队列。而发送中的封包,何时收到 ACK 是一件不确定的事情,这样使用队列似乎也有一定的问题。
4.4 | 滑动窗口(Sliding Window)
这里其实应该用一种叫作滑动窗口的数据结构去实现
如上图所示:
- 深绿色代表已经收到 ACK 的段
- 浅绿色代表发送了,但是没有收到 ACK 的段
- 白色代表没有发送的段
- 紫色代表暂时不能发送的段
下面重新设计一不同类型封包的顺序,将已发送的数据放到最左边,发送中的数据放到中间,未发送的数据放到右边。假设我们最多同时发送 5 个封包,也就是窗口大小 = 5。窗口中的数据被同时发送出去,然后等待 ACK。如果一个封包 ACK 到达,我们就将它标记为已接收(深绿色)。
如下图所示,有两个封包的 ACK 到达,因此标记为绿色。
这个时候滑动窗口可以向右滑动
4.5 | 重传
发送过程中,部分数据没能收到 ACK 时可能发生重传。
如果发生下图这样的情况,段 4 迟迟没有收到 ACK。
这个时候滑动窗口只能右移一个位置,
这个过程中,如果后来段 4 重传成功(接收到 ACK),那么窗口就会继续右移。如果段 4 发送失败,还是没能收到 ACK,那么接收方也会抛弃段 5、段 6、段 7。这样从段 4 开始之后的数据都需要重发。
4.6 | 快速重传
在 TCP 协议中,如果接收方想丢弃某个段,可以选择不发 ACK。发送端超时后,会重发这个 TCP 段。而有时候,接收方希望催促发送方尽快补发某个 TCP 段,这个时候可以使用快速重传能力。
例如段 1、段 2、段 4 到了,但是段 3 没有到。 接收方可以发送多次段 3 的 ACK。如果发送方收到多个段 3 的 ACK,就会重发段 3。这个机制称为快速重传。这和超时重发不同,是一种催促的机制。
为了不让发送方误以为段 3 已经收到了,在快速重传的情况下,接收方即便收到发来的段 4,依然会发段 3 的 ACK(不发段 4 的 ACK),直到发送方把段 3 重传。
窗口大小的单位是多少呢?在上面所有的图片中,窗口大小是 TCP 段的数量。实际操作中,每个 TCP 段的大小不同,限制数量会让接收方的缓冲区不好操作,因此实际操作中窗口大小单位是字节数。
4.7 | 流速控制
发送、接收窗口的大小可以用来控制 TCP 协议的流速。
窗口越大,同时可以发送、接收的数据就越多,支持的吞吐量也就越大。当然,窗口越大,如果数据发生错误,损失也就越大,因为需要重传越多的数据。
举个例子:我们用 RTT 表示 Round Trip Time,就是消息一去一回的时间。
假设 RTT = 1ms,带宽是 1mb/s。如果窗口大小为 1kb,那么 1ms 可以发送一个 1kb 的数据(含 TCP 头),1s 就可以发送 1mb 的数据,刚好可以将带宽用满。如果 RTT 再慢一些,比如 RTT = 10ms,那么这样的设计就只能用完 1/10 的带宽。 当然你可以提高窗口大小提高吞吐量,但是实际的模型会比这个复杂,因为还存在重传、快速重传、丢包等因素。
而实际操作中,也不可以真的把带宽用完,所以最终我们会使用折中的方案,在延迟、丢包率、吞吐量中进行选择,毕竟鱼和熊掌不可兼得。
4.8 | 总结
为了提高传输速率,TCP 协议选择将多个段同时发送,为了让这些段不至于被接收方拒绝服务,在发送前,双方要协商好发送的速率。但是我们不可能完全确定网速,所以协商的方式,就变成确定窗口大小。
有了窗口,发送方利用滑动窗口算法发送消息;接收方构造缓冲区接收消息,并给发送方 ACK。滑动窗口的实现只需要数组和少量的指针即可,是一个非常高效的算法。像这种算法,简单又实用,比如求一个数组中最大的连续 k 项和,就可以使用滑动窗口算法。如果你对这个问题感兴趣,不妨用你最熟悉的语言尝试解决一下。
最后,滑动窗口和流速控制是怎么回事?
【解析】滑动窗口是 TCP 协议控制可靠性的核心。发送方将数据拆包,变成多个分组。然后将数据放入一个拥有滑动窗口的数组,依次发出,仍然遵循先入先出(FIFO)的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到 ACK,窗口就会发生滑动;如果最小序号的分组长时间没有收到 ACK,就会触发整个窗口的数据重新发送。
另一方面,在多次传输中,网络的平均延迟往往是相对固定的,这样 TCP 协议可以通过双方协商窗口大小控制流速。补充下,上面我们说的分组和 TCP 段是一个意思。
05 | UDP 协议
TCP 和 UDP 是今天应用最广泛的传输层协议,拥有最核心的垄断地位。TCP 最核心的价值是提供了可靠性,而 UDP 最核心的价值是灵活,你几乎可以用它来做任何事情。例如:HTTP 协议 1.1 和 2.0 都基于 TCP,而到了 HTTP 3.0 就开始用 UDP 了。
5.1 | UDP协议
UDP(User Datagram Protocol),目标是在传输层提供直接发送报文(Datagram)的能力。
- Datagram 是数据传输的最小单位。
- UDP 协议不会帮助拆分数据,它的目标只有一个,就是发送报文。
为什么不直接调用 IP 协议呢? 如果裸发数据,IP 协议不香吗?
这是因为传输层协议在承接上方应用层的调用,需要提供应用到应用的通信——因此要附上端口号。每个端口,代表不同的应用。传输层下层的 IP 协议,承接传输层的调用,将数据从主机传输到主机。IP 层不能区分应用,导致哪怕是在 IP 协议上进行简单封装,也需要单独一个协议。这就构成了 UDP 协议的市场空间。
5.2 | UDP的封包格式
设计目标:就是在允许用户直接发送报文的情况下,最大限度地简化应用的设计。
下图是 UDP 的报文格式。
UDP 的报文非常简化,只有 5 个部分:
- Source Port 是源端口号。因为 UDP 协议的特性(不需要 ACK),因此这个字段是可以省略的。但有时候对于防火墙、代理来说,Source Port 有很重要的意义,它们需要用这个字段行过滤和路由。
- Destination Port 是目标端口号(这个字段不可以省略)。
- Length 是消息体长度。
- Checksum 是校验和,作用是检查封包是否出错。
- Data octets 就是一个字节一个字节的数据,Octet 是 8 位。
其中,校验和(Checksum)机制,在很多的网络协议中都会存在,因为校验数据在传输过程中有没有丢失、损坏是一个普遍需求。在一次网络会话中,我们传输的内容可能是:“你好!”,但事实上传输的是 01 组成的二进制。请你思考这样一个算法,我们把数据分成一个一个 byte,然后将所有 byte 相加,再将最终的结果取反。
比如现在数据有 4 个 byte:a,b,c,d,那么一种最简单的校验和就是:
checksum=(a+b+c+d) ^ 0xff
如果发送方用上述方式计算出 Checksum,并将 a,b,c,d 和 Checksum 一起发送给接收方,接收方就可以用同样的算法再计算一遍,这样就可以确定数据有没有发生损坏(变化)。
当然 Checksum 的做法,只适用于数据发生少量变化的情况。如果数据发生较大的变动,校验和也可能发生碰撞。
UDP 的可靠性保证仅仅就是 Checksum 一种。
如果一个数据封包 Datagram 发生了数据损坏,UDP 可以通过 Checksum 纠错或者修复。 但是 UDP 没有提供再多的任何机制,比如 ACK、顺序保证以及流控等。
5.3 | UDP与TCP的区别
-
目的差异
- TCP 协议的核心目标是提供可靠的网络传输
- UDP 的目标是在提供报文交换能力基础上尽可能地简化协议轻装上阵。
-
可靠性差异
TCP 核心是要在保证可靠性基础上提供更好的服务。TCP 会有握手的过程,需要建立连接,保证双方同时在线。而且TCP 有时间窗口持续收集无序的数据,直到这一批数据都可以合理地排序组成连续的结果。
UDP 并不具备以上这些特性,它只管发送数据封包,而且 UDP 不需要 ACK,这意味着消息发送出去成功与否 UDP 是不管的。
-
连接 vs 无连接
- TCP 是一个面向连接的协议(Connection-oriented Protocol),传输数据必须先建立连接。
- UDP 是一个无连接协议(Connection-less Protocol),数据随时都可以发送,只提供发送封包(Datagram)的能力。
-
流控技术(Flow Control)
TCP 使用了流控技术来确保发送方不会因为一次发送过多的数据包而使接收方不堪重负。TCP 在发送缓冲区中存储数据,并在接收缓冲区中接收数据。当应用程序准备就绪时,它将从接收缓冲区读取数据。如果接收缓冲区已满,接收方将无法处理更多数据,并将其丢弃。UDP 没有提供类似的能力。 -
传输速度
UDP 协议简化,封包小,没有连接、可靠性检查等,因此单纯从传输速度上讲,UDP 更快。 -
场景差异
TCP 每个数据封包都需要确认,因此天然不适应高速数据传输场景,比如观看视频(流媒体应用)、网络游戏(TCP 有延迟)等。具体来说,如果网络游戏用 TCP,每个封包都需要确认,可能会造成一定的延迟;再比如音、视频传输天生就允许一定的丢包率;Ping 和 DNSLookup,这类型的操作只需要一次简单的请求/返回,不需要建立连接,用 UDP 就足够了。
近些年有一个趋势,TCP/UDP 的边界逐渐变得模糊,UDP 应用越来越多。比如传输文件,如果考虑希望文件无损到达,可以用 TCP。如果考虑希望传输足够块,就可能会用 UDP。再比如 HTTP 协议,如果考虑请求/返回的可靠性,用 TCP 比较合适。但是像 HTTP 3.0 这类应用层协议,从功能性上思考,暂时没有找到太多的优化点,但是想要把网络优化到极致,就会用 UDP 作为底层技术,然后在 UDP 基础上解决可靠性。
所以理论上,任何一个用 TCP 协议构造的成熟应用层协议,都可以用 UDP 重构。这就好比,本来用一个工具可以解决所有问题,但是如果某一类问题体量非常大,就会专门为这类问题创造工具。因此,UDP 非常适合需要定制工具的场景。
第一类:TCP场景
- 远程控制(SSH)
- File Transfer Protocol(FTP)
- 邮件(SMTP、IMAP)等
- 点对点文件传出(微信等
第二类:UDP 场景
- 网络游戏
- 音视频传输
- DNS
- Ping
- 直播
第三类:模糊地带
- HTTP(目前以 TCP 为主)
- 文件传输
UDP 不提供可靠性,不代表我们不能解决可靠性。UDP 的核心价值是灵活、轻量,构造了最小版本的传输层协议。在这个之上,还可以实现连接(Connection),实现会话(Session),实现可靠性(Reliability)……
最后,TCP 协议和 UDP 协议的优势和劣势?
【解析】TCP 最核心的价值就是提供封装好的一套解决可靠性的优秀方案。TCP 帮助我们在确保吞吐量、延迟、丢包率的基础上,保证可靠性。
历史上 TCP 也是靠可靠性起家的,有一次著名的实验,TCP 协议的设计者做了一次演示——利用 TCP 协议将数据在卫星和地面之间传播了很多次,没有发生任何数据损坏。从那个时候开始,研发人员开始大量选择 TCP 协议。然后随着生态的发展,逐渐提供了流控等能力。TCP 的成功在于它给人们提供了很多现成、好用的能力。
UDP 则不同,UDP 提供了最小版的实现,只支持 Checksum。UDP 最核心的价值是灵活、轻量、传输速度快。考虑到不同应用的特性,如果不使用一个大而全的方案,为自己的应用特性量身定做,可能会做得更好。比如网络游戏中游戏客户端不断向服务端发送玩家的位置,如果某一次消息丢失了,只要这个消息不影响最终的游戏结果,就可以只看下一个消息。不同应用有不同的特性,需要的可靠性级别不一样,这就是越来越多的应用开始使用 UDP 的原因之一。
其实对于我们来说,TCP 协议和 UDP 协议根本不存在什么优势和劣势,只不过是场景不同,选择不同而已。最后还有一个非常重要的考虑因素就是成本,如果没有足够专业的团队解决网络问题,TCP 无疑会是更好的选择。