0%

前言

说起TCP协议相信大家都不陌生,作为互联网的主要的传输协议,其重要性自不必说。
计算机网络体系十分庞大而复杂,TCP协议也不例外。TCP协议从发布至今也经历过无数次的修订和迭代,所以本篇也只是对其中的核心点分析一二。
若有谬误之处,还请指正。

TCP报文结构分析

传输控制协议(Transmission Control Protocol),简称TCP。

  • 提供一种面向连接的、可靠的、基于字节流的服务。

  • 位于OSI网络模型的传输层。

  • 全双工通信。

官方文档:https://tools.ietf.org/html/rfc793

OSI网络模型

image.png

TCP报文结构如下图所示:

image.png

TCP报文字段含义说明:

  • 源端口:占2字节;用于识别连接的发送端口。

  • 目的端口:占2字节;用于识别连接的接收端口。

  • 序列号:seq,占4字节; 如果SYN标记有效,则此为最初的序列号;如果SYN标记无效,则此为当次发送的数据部分的第一个字节编号。

  • 确认号:ack,占4字节;期望对方下一次传过来的TCP数据部分的第一个字节的编号。

  • 数据偏移:占4位;取值范围0x0101~0x1111;乘以4之后的值表示TCP首部总长度。

  • 保留:占6位;目前的值全为0。

  • 窗口:占2字节;进行流量控制,告知对方下一次允许发送的数据大小。(字节为单位)

  • 检验和:占2字节;检验和的计算内容:伪首部 + 首部 + 数据。

  • 选项字段:最多40字节,用于扩展TCP功能。

  • 标志位

    1
    2
    3
    4
    5
    6
    URG(Urgent):当URG=1时,紧急指针字段才有效。表明当前报文段中有紧急数据,应优先尽快传送。
    ACK(Acknowledgment):当ACK=1时,确认号字段才有效。
    PSH(Push):当PSH=1时,接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
    RST(Reset):当RST=1时,表明连接中出现严重差错,必须释放连接,然后再重新建立连接。也可以用于拒绝非法数据和拒绝连接请求。
    SYN(Synchronization):当SYN=1、ACK=0时,表明这是一个建立连接的请求。
    FIN(Finish):当FIN=1时,表明数据已经发送完毕,要求释放连接。

TCP的运行方式及特性

TCP通信的数据封装解封过程,如下图所示:

image.png

  • 按照OSI网络模型的分层设计,数据封装时从应用层到数据链路层每一层接收上层传递过来的数据,并加上当前层的头部,直至传递到物理层;数据经物理层负责运转,之后再反过来逐层解封,交给应用层处理。

TCP运行的三个主要阶段

  • TCP协议的运行主要划分为三个阶段:创建连接、传送数据、终止连接。

我们可以看到,TCP通信需要通过连接完成,那么什么是连接?

连接的定义

  • 在官方文档中,对连接有着明确的规定:
    image.png
  • 总结来说,连接就是用于保证可靠性和流控制机制而维护的数据流状态信息,由Socket、序列号以及窗口大小组成的组合。

创建连接之三次握手

image.png

三次握手协议的过程详解:

  • 一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端(服务器端)打开一个套接字(socket)然后监听来自另一方(客户端)的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,客户端就能开始创建主动打开(active open)。

  • 服务器端执行了listen函数后,就在服务器上创建起两个队列:

1
2
SYN队列:存放完成了二次握手的结果。 队列长度由listen函数的参数backlog指定。
ACCEPT队列:存放完成了三次握手的结果。队列长度由listen函数的参数backlog指定。
  • 客户端(通过执行connect函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数x作为消息序列号。

  • 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为x+1,SYN/ACK包本身携带一个随机产生的序号y。

  • 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为x+1,而ACK的确认码则为y+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新创建一个新的用于双向通信的sockfd(本次socket连接的文件句柄),并返回。

为什么需要三次握手?

TCP官方文档中明确指出了 TCP 连接使用三次握手的首要原因;
image.png

  • 即为了阻止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立了错误的连接。
1
2
3
4
5
6
7
8
9
10
11
试想一下,如果通信双方的通信次数只有两次,那么发送方一旦发出建立连接的请求之后它就没有办法撤回这一次请求,
如果在网络状况复杂或者较差的网络中,发送方连续发送多次建立连接的请求,
如果 TCP 建立连接只能通信两次,那么接收方只能选择接受或者拒绝发送方发起的请求,它并不清楚这一次请求是不是由于网络拥堵而早早过期的连接。

所以,TCP 选择使用三次握手来建立连接并在连接引入了 RST 这一控制消息,接收方当收到请求时会将发送方发来的 SEQ+1 发送给对方,
这时由发送方来判断当前连接是否是历史连接:

如果当前连接是历史连接,即 SEQ 过期或者超时,那么发送方就会直接发送 RST 控制消息中止这一次连接;
如果当前连接不是历史连接,那么发送方就会发送 ACK 控制消息,通信双方就会成功建立连接;
使用三次握手和 RST 控制消息将是否建立连接的最终控制权交给了发送方,因为只有发送方有足够的上下文来判断当前连接是否是错误的或者过期的,
这也是 TCP 使用三次握手建立连接的最主要原因。

传送数据

  • 可靠传输

  • 流量控制

  • 拥塞控制

可靠传输

可靠传输 - 停止等待ARQ协议

  • ARQ(Automatic Repeat–reQuest),自动重传请求。
    image.png
    image.png

  • 所谓的停止等待,也就是在发生数据传输错误时(上面所示的几种情况下),TCP启动超时重传机制,但是这会出现严重的效率问题,当上一个数据传送成功之后,才能开始传送下一个数据,可以类比于串行队列。

为了解决这个问题,也就发明出了连续ARQ协议与滑动窗口协议。

可靠传输 - 连续ARQ协议+滑动窗口协议

image.png

  • 在连续ARQ协议+滑动窗口协议的共同协作下,使TCP一个数据段内同时发送多条数据成为可能。

  • 发送字节编号连续的一个数据段,接收方以“最后一个字节编号+1”作为确认号,不需要每个字节都确认一遍,提升了传输效率和性能。

  • 滑动窗口协议:发送方有发送窗口,接收方有接收窗口,在数据发送过程中,接收方在报文中携带窗口大小,在网络情况较差或者恢复的情况下,动态的调整发送方窗口大小,避免因网络因素引起的传输瘫痪。

可靠传输 - SACK(选择性确认)

  • 问题场景
    在TCP通信过程中,如果发送序列中间某个数据包丢失(比如1、2、3、4、5中的3丢失了)。
    TCP会通过重传最后确认的分组后续的分组(最后确认的是2,会重传3、4、5)。
    这样原先已经正确传输的分组也可能重复发送(比如4、5),降低了TCP性能。

  • 解决方案
    为改善上述情况,发展出了SACK(Selective acknowledgment,选择性确认)技术。
    告诉发送方哪些数据丢失,哪些数据已经提前收到。
    使TCP只重新发送丢失的包(比如3),不用发送后续所有的分组(比如4、5)。

流量控制

  • 问题场景
    如果接收方的缓存区满了,发送方还在疯狂着发送数据。接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源,所以要进行流量控制。

  • 什么是流量控制?
    让发送方的发送速率不要太快,让接收方来得及接收处理。

  • 原理
    通过确认报文中窗口字段来控制发送方的发送速率。
    发送方的发送窗口大小不能超过接收方给出窗口大小。
    当发送方收到接收窗口的大小为0时,发送方就会停止发送数据。

流量控制 - 特殊情况

  • 有一种特殊情况
    一开始,接收方给发送方发送了0窗口的报文段。
    后面,接收方又有了一些存储空间,给发送方发送的非0窗口的报文段丢失了。
    发送方的发送窗口一直为零,双方陷入僵局。
  • 解决方案
    当发送方收到0窗口通知时,这时发送方停止发送报文。
    并且同时开启一个定时器,隔一段时间就发个测试报文去询问接收方最新的窗口大小。
    如果接收的窗口大小还是为0,则发送方再次刷新启动定时器。

拥塞控制

  • 防止过多的数据注入到网络中
    避免网络中的路由器或链路过载。

  • 拥塞控制是一个全局性的过程
    涉及到所有的主机、路由器等。
    以及与降低网络传输性能有关的所有因素。

  • 相比而言,流量控制是点对点通信的控制。

网络拥塞示意图:

image.png

  • 在理想情况下,输入的负载与链路吞吐量成正相关,但是实际值与理论值存在一定的偏差,当总带宽1000M/s,实际负载到达700M/s附近可能就发生了轻度拥塞,之后增加负载,拥塞就更加严重,最后造成整个网络瘫痪,这时也就彰显了拥塞控制的必要性。

拥塞控制 – 方法

TCP的拥塞控制具有一定的方法,具体如下:

  • 慢开始或称慢启动(slow start)

  • 拥塞避免(congestion avoidance)

  • 快速重传(fast retransmit)

  • 快速恢复(fast recovery)

  • 先来了解几个基础概念

    1
    2
    3
    4
    5
    MSS(Maximum Segment Size):每个段最大的数据部分大小;在建立连接时确定。
    cwnd(congestion window):拥塞窗口
    rwnd(receive window):接收窗口
    swnd(send window):发送窗口
    swnd = min(cwnd, rwnd)

拥塞控制 – 方法 – 慢开始

  • 当MSS=100,且rwnd=3000时,发送方的发送窗口以2倍的速度逐级试探性增长,知道达到接收窗口的阈值为止。

如下图所示:

image.png

拥塞控制 – 方法 – 拥塞避免

  • ssthresh(slow start threshold):慢开始阈值,cwnd达到阈值后,以线性方式增加。

  • 拥塞避免(加法增大):拥塞窗口缓慢增大,以防止网络过早出现拥塞。

  • 乘法减小:只要网络出现拥塞,把ssthresh减为拥塞峰值的一半,同时执行慢开始算法(cwnd又恢复到初始值)。

  • 当网络出现频繁拥塞时,ssthresh值就下降的很快。

如下图所示:
image.png

拥塞控制 – 方法 – 快重传

  • 接收方
    每收到一个失序的分组后就立即发出重复确认。
    使发送方及时知道有分组没有到达。
    而不要等待自己发送数据时才进行确认。

  • 发送方
    只要连续收到三个重复确认(总共4个相同的确认),就应当立即重传对方尚未收到的报文段。
    而不必继续等待重传计时器到期后再重传。

如下图所示:

image.png

拥塞控制 – 方法 – 快恢复

  • 当发送方连续收到三个重复确认,说明网络出现拥塞。

  • 就执行“乘法减小”算法,把ssthresh减为拥塞峰值的一半。

  • 与慢开始不同之处是现在不执行慢开始算法,即cwnd现在不恢复到初始值。
    而是把cwnd值设置为新的ssthresh值(减小后的值)。

  • 然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

如下图所示:
image.png

终止连接之四次挥手

image.png

四次挥手过程分析

四次挥手过程详解:
  • 客户端发送断开连接请求,FIN = 1,seq = u,客户端进入FIN-WAIT-1状态。

  • 服务端收到断开请求, 发送确认 ACK=1,ack=u+1,,同时发送一个验证包 seq = v,进入 CLOSE-WAIT状态,客户端收到请求确认,进入 FIN-WAIT-2 状态,等待服务器发送断开请求。

  • 服务器传输完成,发送断开请求 FIN = 1,ACK = 1,seq = w,ack = u + 1,服务端进入 LAST-ACK 状态。

  • 客户端收到断开请求,向服务端发出确认 ACK = 1,seq = u + 1,ack = w + 1,客户端进入TIME-WAIT状态, 服务端立即进入CLOSED状态,客户端在 TIME-WAIT 状态结束后(2MSL),进入 CLOSED 状态。

为什么释放连接的时候,要进行4次挥手?
TCP是全双工通信模式。
  • 第1次挥手:当主机1发出FIN报文段时
    表示主机1告诉主机2,主机1已经没有数据要发送了,但是,此时主机1还是可以接受来自主机2的数据。

  • 第2次挥手:当主机2返回ACK报文段时。
    表示主机2已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的。

  • 第3次挥手:当主机2也发送了FIN报文段时
    表示主机2告诉主机1,主机2已经没有数据要发送了。

  • 第4次挥手:当主机1返回ACK报文段时
    表示主机1已经知道主机2没有数据发送了。随后正式断开整个TCP连接。

TCP性能问题

TCP协议可以说是互联网的基石,作为可靠传输协议,在当今几乎所有的数据都会通过TCP协议传输,但是也会存在一些严重的性能问题。
自从RFC 793发布至今已经走过了40年时间。
但在TCP设计之初没有考虑到现今复杂的网络环境,当我们弱网情况下被断断续续的网络折磨时,我们可能都不知道这一切可能都是TCP协议造成的。

造成性能问题的原因

  • TCP设计时还处于有线网时代,丢包的概率相对较小,所以TCP认为丢包就发生了拥塞,但是随着网络发展,丢包意味着网络拥塞这一假设在很多时候已经不成立了,这也是问题的根源。

  • TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差。

  • TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟。

  • TCP默认由操作系统内核实现,升级迭代很麻烦,发展缓慢。

扩展

QUIC

  • QUIC(读作“quick”)是一个通用的传输层网络协议,最初由Google的Jim Roskind设计,2012年实现并部署,目前还处于草稿阶段。
  • QUIC旨在提供几乎等同于TCP连接的可靠性,但延迟大大减少。

QUIC的特点

  • 协议变更,基于UDP,减少了TCP三次握手及TLS握手时间。

  • 拥塞控制,QUIC将拥塞控制策略从操作系统内核转交给应用层来控制,更新拥塞控制算法不需要停机升级,使得在某些场景下可更有效地改变拥塞策略,达到更优的效果。

  • 连接管理,TCP 使用四元组 (源 IP,源端口,目的 IP,目的端口) 标识连接,网络切换时会导致重连,而 QUIC 使用自定义的 Connection ID 作为链接标识,只要客户端使用的 Connection ID 不变,即使网络切换也能保证链接不中断。

  • 多路复用。

总结

到此为止,我们分析了TCP的一些比较核心的特性,但是TCP的复杂性远超我们本文所说,所以有兴趣的同学可以自行仔细研究一番。

参考资料

目录

1、Widget、Element、RenderObject

2、Flutter中树结构的创建、更新、复用

3、DOM、Virtual DOM、diff

4、Key的分类及使用

5、参考资料


1、Widget、Element、RenderObject

Widget

  • Flutter的基石,用户界面的不可变的一种描述;会频繁创建、销毁。
  • 作用类似于HTML中的标签。

image.png

Element

  • Widget的实例化对象,在树中详细的位置。
  • 利用Widget作为配置,用于管理Widget的生命周期、UI的更新。

image.png

RenderObject

  • 由Element的子类RenderObjectElement创建,负责UI的布局、绘制、事件响应。
  • 开发复杂视图时,可以自定义绘制功能。

image.png

Widget、Element、RenderObject之间的关系

  • Element同时持有对Widget和RenderObject的引用。
  • 源码如图所示:

image.png


2、Flutter中树结构的创建、更新、复用

Flutter中的树结构

image.png

为什么创建3棵树?

  • 提高性能
    1
    2
    尽可能的复用Element和RenderObject,因为Widget可能会频繁的创建销毁,
    因此WidgetTree是非常不稳定的,如果每次直接根据WidgetTree重新创建RenderObjectTree会极大的消耗性能。
  • 便于访问状态、树节点之间的结构信息等数据
    1
    2
    3
    例如:
    StatelessElement内部存储了_widget、 _renderObject;
    对于StatefulElement其内部还存储了_state信息。

树的创建过程

整体流程图

image.png

1、Flutter主入口,main()函数。

image.png

2、初始化Flutter功能组件。

image.png

3.1、创建根Widget(即RenderObjectToWidgetAdapter)

  • 内部创建了RenderObjectToWidgetAdapter ,并将我们传入的自定义Widget(即runApp)做为其child;
  • RenderObjectToWidgetAdapter本身是一个RenderObjectWidget,是RenderObject和Element之间的桥梁。

image.png

3.2、创建根Element(即RenderObjectToWidgetElement)

  • 接着执行attachToRenderTree()方法,创建根Element,并调用mount()方法,继续创建根RenderObject。

image.png

3.3、创建根RenderObject

  • 在根Element中调用了mount()方法后,会调用super.mount()方法,即RenderObjectElement.mount(),创建根RenderObject,并将其挂载。

image.png

4、调用scheduleWarmUpFrame方法。

  • 此时WidgetTree、ElementTree、RenderObjectTree对应的结构都已经初步建立,Flutter准备界面渲染和显示。

image.png

Flutter中树的更新

更新规则

image.png

问题:在更新过程中,如何知道Widget能够复用Element呢?

答案:Widget提供了一个核心方法canUpdate,如源码所示:

image.png

  • 默认情况下:Widget的Key == null。
1
当没有给Widget设置Key的时候,Flutter会根据Widget的runtimeType和显示顺序是否相同来判断Widget是否有变化。(runtimeType即Widget的类型)
  • 当给Widget设置了Key时。
1
当给Widget设置了Key时,Flutter根据runtimeType和Key两个条件来判断Widget是否有变化。

Flutter中树的复用


3、DOM、Virtual DOM、diff

DOM是什么?

  • 文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展置标语言的标准编程接口。
  • DOM提供了对整个文档的访问模型,将文档抽象成一个倒立生长的树形结构,树的每个节点表示了一种对象类型,节点之间存在父子、兄弟关系。
  • DOM是一种与平台和语言无关的应用程序接口(API),可以动态地访问程序和脚本,更新其内容、结构。
  • 常见的形式主要有:HTML-DOM、XML-DOM。

常见的HTML-DOM结构

image.png

Virtual DOM

  • Flutter的很多灵感来自于React,比如: Virtual DOM、diff算法、状态管理等。

  • Virtual DOM的本质

1
2
3
对应于Flutter,就是在Widget和RenderObject之间做了一个缓存。
可以类比CPU和硬盘,硬盘速度很慢,所以在两者之间加入内存缓存。
既然直接操纵RenderObject很慢并且消耗性能,就可以在Widget和RenderObject之间加个缓存(Element)。
  • Virtual DOM算法主要步骤
1
2
3
1、用对象结构作为DOM树结构的映射,然后用这个树构建一个真正的DOM树,插到上下文中。
2、当状态发生变更时,重新构造一颗新的对象树,然后用新树和旧树进行比较,记录两棵树的差异,即diff的过程。
3、把第2步所记录的差异,应用到真正的DOM树上(即patch),进行视图更新。

diff

  • diff作为Virtual DOM算法的核心,具有一定的规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、深度优先遍历新旧2棵Virtual DOM树,给每个节点设定唯一的标记。
遍历的同时,将旧树的节点与新树的节点进行比较,将差异依次记录到一个对象中。

例如:
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的差异

2、定义差异类型。
2.1、替换掉原来的节点。例如:把Row、改为Column。
2.2、移动、删除、新增子节点。例如:把多子Widget的children换位置。
2.3、修改了节点的属性。例如:颜色变化。
2.4、文本节点的内容修改。例如:Text(“123”) 改为 Text(“ABC”)。

3、把差异应用到真正的DOM树上,即patch的过程。
对应Flutter中,也就是更新RenderObjectTree,重新渲染页面。

问题:对于多个相同TagName或相同runtimeType的DOM节点,如何复用?

答案:给Widget设置Key,保证唯一性。


4、Key的分类及使用

Key的定义

  • 官方定义如下:
    1
    2
    3
    Key是Widget、Element、SemanticsNode(语义节点)的标识符。
    只有当新的Widget的Key与当前Element中Widget的Key相同时,它才会被用来更新现有的Element。
    Key在具有相同父级的Element之间必须是唯一的。
  • Key的作用:diff算法的关键。

Key的分类

image.png

  • LocalKey
1
2
LocalKey直接继承自抽象类Key ,应用于拥有相同父级Element的Widget进行比较的场景。
例如:有一个多子Widget中需要对它的子Widget进行移动操作,此时可以使用LocalKey,提高Element复用率,提高性能。
  • GlobalKey
1
2
3
4
5
GlobalKey直接继承自抽象类Key,内部使用了一个静态常量Map来保存它对应的Element。
可以通过GlobalKey找到持有该GlobalKey的Widget、Element和State。
GlobalKey可以在多个页面或者层级复用。

注意:GlobalKey比较昂贵,需要谨慎使用。

GlobalKey的源码如下:

image.png

Key的使用


5、参考资料