UDP & TCP介绍
- UDP
- 报文格式
- 报文内容介绍
- 端口号
- 报文长度
- 校验和
- 载荷
- TCP
- 报文格式
- 初步了解
- TCP机制
- 确认应答
- 超时重传
- 连接管理
- 滑动窗口
- 流量控制
- 拥塞控制
- 紧急传输
- 数据推送
- 延时应答
- 捎带应答
- 面向字节流
- 异常处理
- 心跳机制
- UDP 和 TCP 的区别
UDP
报文格式
对于网络协议, 本质上就是就是定义了一个数据的组织格式, 因此我们先来看一下UDP的报文格式, 随后根据其报文格式去了解它的具体功能.
下面是 UDP 协议一个报文的基本格式
下面我们就来简单介绍一下其中的报文格式
报文内容介绍
端口号
首先 UDP 作为一个传输层的协议, 那么就会有端口号信息, 从报文格式中我们也可以看出, 刚开始就直接提供了源端口和目的端口的信息, 这个并没有什么好介绍的, 我们直接看下一个内容
报文长度
报文长度, 顾名思义就是去描述 UDP 的报文多长的. 那既然要描述一个东西有多长, 自然它的单位就是非常重要的, 比如我描述一条路有多长, 那此时如果我说 1 肯定是不行的, 我肯定要说例如 1m, 1km这样的.
那么 UDP 这里的报文长度, 则是以字节为单位的, 那么两个字节, 也就是 16bit, 也就是能描述 2 ^ 16 - 1 个字节, 即 65535 个字节. 那此时有人就要说了, 才这么一丁点, 一次传输才能传 64KB.
这也是 UDP 的局限性, 只能用于传输比较小的数据, 虽然对于当初计算机刚刚发展起来的时候, 64KB 已经非常够用了, 但是在当今这个时代, 肯定是不太够的.
那此时有人就要问了: 那假如我想要使用 UDP 来传输一个比较大的数据, 那么能不能实现呢?
实际上是可以的, 但是成本会比较高. 例如我们可以将数据进行拆分, 分批次发送, 但是如果要拆分数据发送, 那么我们势必就要考虑一些问题, 例如:
- 数据的先后顺序问题: 网络中是存在后发先置的问题的, 也就是说后面发的数据反而可能先到了. 那么此时就会有数据先后顺序问题
- 数据如何拆分和拼接: 既然要拆分, 我们就要规定拆分的规则, 同时规定对应的合并规则
- 中间的某些数据丢了: 网络传输过程中, 是可能会发生丢包的情况的, 那么此时如果中间的数据丢了怎么办?
可以看到, 此时要考虑这些问题, 还是比较麻烦的, 因此这里实际上有一个更简单的方式去传输大的数据, 就是直接使用 TCP 协议传输. 那为什么 TCP 可以传输? 它不会有上面的这些问题吗? 这些问题就留到我们学习 TCP 的过程中解答.
校验和
校验和, 从它的名字里面也可以看出, 它是用于校验的, 那它是用于校验什么的? 怎么校验的?
在网络传输的过程中, 难免会涉及到一些外界条件的影响, 那此时可能就会导致我们传输的数据发生一些损坏, 使其不完整或者是直接就没了.
其中数据没了, 我们直接都收不到自然就不用校验, 我们校验的主要是数据是否损坏这个问题. 那如何去检验这个数据是否完整呢. 此时就是需要靠这个校验和来实现
校验和会采取一些算法, 使其按照相同数据计算出的校验和一定是相同的. 那么假如我接收到数据后, 发现我算一下数据, 校验和不一样了, 此时就认为数据发生了损坏, 不会使用了.
那此时自然就有一个问题: 如果我的校验和在不同数据上算出的长一样怎么办?
这个实际上主要就是涉及到校验和的算法选择问题, 如果选择的算法合适, 那么此时发生这样事件的概率就是比较低的. 其中 UDP 采取的算法叫做循环冗余算法, 简单地说就是把数据里面的每一个字节进行累加, 加到溢出也不管, 加到最后就是我们的校验和, 很明显这个算法并不是非常的靠谱, 冲突的概率还是比较高的.
那此时可能有人要问了: 那如果我的校验和坏了呢? 我怎么检验呢?
一般来说, 如果你校验和都坏了, 此时证明你的数据就是损失过的, 那么此时自然对不上才是最好的, 因此这个并不会引发什么问题.
载荷
载荷实际上就是用于装载数据的部分, 这部分其实没有什么可以详细介绍的. 我们主要需要知道的是, UDP 是面向数据报来传输数据的, 也就是说 UDP 在你传输数据前, 要求你要先把数据进行打包, 然后才能传输.
TCP
了解了 UDP 之后, 我们发现 UDP 在某些方面似乎并不是非常的好用, 因此在日常使用中, 我通常都会采用 TCP 来进行传输. 而对于 TCP 协议, 有一个非常重要的特点, 即可靠传输. 可靠传输可以说是 TCP 的初心, 下面我们了解 TCP 机制的过程中, 也可以发现 TCP 实际上就是围绕着可靠传输来设计的.
报文格式
首先我们依旧是来看 TCP 的报文格式, 其基本报文格式如下
当然, 这个报文格式可能不是最新的, 但是基本上核心的部分都涉及到了, 如果要参考最新的标准, 那么推荐阅读 RFC 标准文档里对于 TCP 的描述, RFC 标准文档就是定制了一系列的网络等方面的标准, 如果想要获取最准确的信息, 那么一定是推荐去阅读这个文档的. 文档链接: Transmission Control Protocol (TCP)
接下来我们先初步了解TCP中一些比较容易理解的部分, 剩余的部分我们会在介绍 TCP 机制的过程中进行介绍
初步了解
-
源/目的端口号: 传输层协议存储的端口信息
-
四位首部长度:
用于描述下面的这个选项位置是否需要
此时我们可以看出, TCP 的报头是可变长的
-
保留位: 保留下来的可以用于拓展的位置
-
校验和:
用于校验数据完整性的, TCP采用的是反码求和算法, 算法原理如下:
-
将 TCP 的报文划分为一个一个的 16 位二进制
-
把这些二进制进行相加, 如果进位放到下一次计算中一起加
-
对二进制相加的结果进行取反, 此时得到校验和
然后检验过程如下:
-
把数据按照相同的方式进行二进制相加
-
把加出来的总和和校验和进行相加, 此时相当于原码和反码相加, 正常来说会得到全 1 的二进制, 如果得到全 1, 证明数据没有损坏
-
剩余的部分, 我们会在了解 TCP 机制的过程中了解到具体功能, 这里就不介绍了.
TCP机制
确认应答
序号和确认序号这两个位置, 实际上是为 TCP 的确认应答机制而设计的.
确认应答指的就是, TCP 的消息接收方, 会在收到消息后, 返回一个 ACK 的报文, 告诉消息的发送方已经收到数据了. 此时这个 ACK 报文主要由这个 ACK 标志位表示. 如果 ACK 位是 1, 那么就证明这是一个 ACK 数据报, 而如果是 0, 则证明不是.
那此时就可能有人要问了: 那这两个序号又和确认应答有什么关系呢?
实际上, 由于网络通信过于复杂, 有时候会出现一些 ACK 和对应发送的信息对不上的情况发生, 举一个例子来看.
假如张三和李四分别是发送端和接收端, 发生了这样的对话
那么很明显, 此时的这个好
对应的就是要不要一起吃饭
, 而这个滚
则是对应你可不可以转我一百万
的. 但是由于网络的复杂性, 此时可能又会产生下面的这个情况
可以看到, 此时李四后面回的消息, 反而先到达了, 此时就可能会产生歧义, 张三此时就会想: 这李四居然不想和我吃饭, 而是想转我一百万, 真是我的好兄弟啊. 但是实际上并不是这样的
此时的这种情况就被称作为是"后发先置", 这种情况在网络中是非常常见的, 因为在网络的传输是有非常多路径可以走的, 而数据的传输又不是每一次都会找到最优解. 就好比我从北京到上海, 我可以绕中国一大圈, 也可以直接走直线距离.
而 TCP 为了防止这种情况的发生, 就引入了序号和确认序号, 在每次发送 ACK 的时候都会确认和刚刚拿到的数据对的上号, 不要产生歧义. 那么此时即使出现了后发先至的情况, 也会自动调整为正确的顺序.
但是由于 TCP 是根据字节流来传输信息的, 此时这两个序号就会根据字节信息来进行编号
首先发送的时候, 序号会指向需要读取的第一个字节数, 然后要读多少会根据载荷自动计算, 然后将下一次的起始点通过应答报文中的确认序号进行返回
例如我们现在要传输 1000 个字节, 起点是 1, 发送端就会在序号中放一个 1, 然后发送给接收端, 接收端从 1 开始读, 读完之后检测到 1000 是结尾了, 所以把下一次要读的起点 1001 通过 ACK 中的确认序号返回给发送端
我们上面说过 TCP 的初心就是实现可靠传输, 而这个确认应答机制就是可靠传输的核心.
常见问题: TCP 是如何实现可靠传输的?
TCP 是以确认应答为核心, 搭配其他的一些机制实现的可靠传输.
超时重传
确认应答是以一个理想情况来考虑的, 即发送的数据一定会到达对端. 但是在错综复杂的网络通信中, 数据是可能会在传输路径的某一个位置发生丢包的, 此时数据就会丢失, 导致接受的一方接不到数据.
由于网络传输过程中的路线是错综复杂的, 我们无法确定什么时候什么地方会发生丢包, 也不知道丢失的是哪一段数据. 因此丢包是一种随机事件, 并且还是一种很正常的现象
在TCP中, 丢包会有两种情况: 1. 发送的数据报丢了 2. 返回的 ACK 丢了
但是由于发送方只能通过 ACK 来判断自己有没有发送完数据, 因为它也不知道, 我是没传出去, 还是传成功了但是对面返回的 ACK 丢了
因此发送方唯一的解决方式就是: 重传一次看看. 由于正常情况下, 丢包的概率是很低的, 因此发生连续丢包的概率是极低的, 所以重传操作能够大幅度提升数据传输成功的概率
同时, 也不能无间断连续疯狂重传, 因此会有设定超时时间的操作, 但是时间是不确定的.
因为首先这个时间是可以配置的, 并且不同的系统上都可能不一样, 其次是也可以通过修改底层内核的参数来改变. 并且等待的时间也会动态变化, 每一次重传后, 等待时间都会变长, 如果重传次数再多一点, 此时会直接重置连接, 放弃这个连接.
可能此时要有人问了: 为啥你这重传还一次次变长而不是变短呢, 难道不应该很着急把数据传过去吗?
虽然确实, 我们想把数据赶紧传过去, 但是由于发生连续丢包的概率是很低的, 因此如果连续几次都无法成功传输数据, 则大概率证明网络/对端出现了严重错误, 此时再重传也没有用了, 因此时间间隔会越来越长.
举个例子, 假如我们丢包率是10%(正常情况下不会这么高, 一般就大概是 1% 左右, 甚至基本上都是 0%), 那么此时连续两次丢包的概率就是1%, 已经非常低了, 再传一次还失败的概率就更低了. 因此如果多次重传还一直丢包, 证明这个连接可能有出现了重大问题, 丢包率变得很高, 此时不如随便隔很长一段时间传一下看看, 或者直接重置连接.
上面我们讲解了以发送方视角来看, 接下来我们以接收方视角来看
假设发送的数据没丢, 而是 ACK 丢了, 导致发送端触发了重传, 那么此时接收方就是接收了两个同样的数据, 那这个时候咋办?
实际上, TCP 内部非常贴心的帮我们解决了这个问题, 它规定了一块接收缓冲区, 用来保存当前已经收到了的数据, 并且还有编号, 如果接收数据时发现缓冲区有一样的数据, 那么就会把后来的这个数据直接丢弃.
并且接收缓冲区不仅仅能保存接收的数据并且进行去重, 还能对其接受的数据进行排序, 保证我们读的数据顺序是的正确的.
连接管理
这个连接管理机制主要是处理连接的建立和断开的过程, 也就是著名的三次握手和四次挥手操作.
三次握手是什么意思呢? 实际上, 握手是英文将 handshake 直译过来的说法, 它也有更加明了的一种解释方式, 就是打招呼. 因此三次握手简单的说就是让发送端和接收端打招呼. 日常生活中, 打招呼大部分时候并没有实际的意义, 只是为了引起对方注意, 方便后续的交谈. TCP 也是一样, 就是给对端传输一个没有业务数据的数据报, 让对端注意到自己要和它建立连接了, 从而方便后续的操作. 此时的这个数据报被称作为同步数据报, 这个数据报没有载荷, 只有一个报头, 并且在报头中也有一个它的标志位置.
如果这个 SYN 是 1, 证明这是一个同步数据报, 如果是 0 则不是
TCP 的三次握手实际上就是让两端存储对方的信息, 从而完成建立连接的过程: 发送端先发送一个 SYN (发送自己的信息), 然后接收端根据 SYN 返回一个 ACK, 同时接收端也反向发送一个 SYN (发送自己信息的同时告诉发送端信息接收到了), 最后发送端返回一个ACK (告诉接收端这边也收到了信息), 此时通信就算建立完成
此时有人问: 你这不是四下吗? 怎么不叫四次握手?
实际上中间接收端返回 ACK 和 SYN 的时候是可以合并为同一次进行的, 那么这样就变成了三次握手.
那么为什么要进行三次握手呢? 这三次握手有什么用呢?
首先, 依旧是回到初心, TCP 的初心是保证可靠传输, 但是我们上面的两个机制都是建立在连接成功建立的条件下, 但是假如我连接都建立不了, 那么你就算机制再优秀也没用, 而三次握手首先就是为了保证连接是可以正常建立的.
其次, 连接正常建立有两个条件, 一个是中间的网络正常运行, 另外一个就是发送端和接收端也能正常发送和接收数据, 假如两边有任意一边不能正常接收或者发送数据, 那连接从何谈起?
因此三次握手的第二个作用就是, 保证发送方和接收方都能正常接收和发送数据.
还有一个作用就是让连接双方针对一些重要的参数进行协商, 比如我们的序号是从哪里开始的. 因为这里的序号通常在两次建立连接的时候, 可能会有前面连接的残留信息, 因此为了防止前面遗留的数据影响当下的通信, 就需要重新确认一下.
例如因为网络不太好, 网络重连了, 此时建立了第二次连接, 此时第一次连接传的一个数据才刚到. 那此时我都建立第二次连接了, 你这个第一次连接的数据肯定就不要了. 那如何区分这个数据是不是上一次的残留数据呢? 实际上就是通过这个序号的差异来区分, 假如这个序号差异很大, 那你这个数据肯定就不是我们当前连接传的数据, 直接丢掉就好了
常见问题: 三次握手的作用? 两次行不行? 四次行不行?
核心考察三次握手的过程和作用, 根据上面的点回答即可.
过程:
- 发送端发送 SYN
- 接收端返回 ACK, 同时发送 SYN, 此时可以合并一起发送
- 发送端返回 ACK
作用:
- 保证网络状况可以用于建立连接
- 保证发送端和接收端都有发送消息和接受消息的能力
- 对重要参数进行协商, 例如序号
三次握手中, TCP会有几种状态, 需要我们了解一下
LISTEN
: 表示接收端可以进行连接, 正在监听是否有连接请求
SYN_SEN
: 全称SYN_SEND
, 表示当前正在发送 SYN, 此时会开始等待 ACK
SYN_RCVD
: 全称SYN_RECEIVED
, 表示接收到了 SYN, 此时会返回一个 SYN 并且等待 ACK
ESTABLISHED
: 接收到 ACK 后的状态, 表示连接已经建好, 当两方都是这个状态时就可以开始通信了
下面是一个图片表示各个状态
四次挥手也很好理解, 就是用于告别的, 告诉对面我要走了. 与三次牵手对应, 四次挥手就是连接断开的过程. 但是和三次挥手有些不同的是, 三次挥手大多数情况下都是由客户端发起的, 而四次挥手则是两端都可以主动发起的
实际上四次挥手的流程和三次牵手也很类似, 我们直接看图
挥手操作主要就是靠这个FIN来进行, 这个FIN被称作结束报文段, 同样的在TCP报文中也有一个它的标志位
此时可能有人问: 你这个第二次挥手和第三次挥手为什么不和三次握手那一样合并起来?
是因为这个能不能合并, 并不是我们说了算, 而是代码说了算.
首先我们要知道, ACK 的触发机制和 FIN 是不同的, ACK 是由系统内核, 接收到一个数据报后直接自动返回的.而 FIN 是我们的网络连接关闭的时候才会发送 FIN.
那假如发送端给接收端传了一个 FIN, 然后此时接收端需要立刻返回一个 ACK, 但是假如我们的服务器没直接关连接, 还要进行一些善后处理, 例如关闭一些资源什么的这样的工作, 或者发一些残留的数据发送完. 那么此时 FIN 就无法和 ACK 同时发送出去了.
当然, 假如说这个时候接收端不整其他操作, 直接把连接给关了, 那么也是可以同时发送的.
前面的三次握手, 其中的 SYN 和 ACK 都是由系统内核自动发送的, 因此就可以同时发送. 而四次挥手这里 FIN 是我们手动通过代码来触发, 和 ACK 的触发时机不相同, 因此很难合并
此时可能还有人问: 那假如我的代码有问题, 那第二个 FIN 岂不是可能一直发不出去?
确实有这个可能, 断开连接经历四次挥手, 那就属于是正常断连. 假如挥手少于四次, 虽然也可能会断连但是那就属于是异常断连了
另外这里也有几个关于四次挥手的TCP状态, 我们来看图
其中我们最需要关注的是TIME_WAIT
这个状态, 可以发现我们先发送 FIN 的这一端没有直接CLOSED
, 而是进入了这个状态, 过了一会才关闭. 这是为什么呢?
实际上我们这些状态都是为了防止数据在中间丢失而设置的, 这样两端就可以通过自己的状态知道自己要重传什么样的数据报. 举一个例子, 假如我们这里的 A 处于FIN_WAIT_1
状态, B 现在处于CLOSE_WAIT
状态, 但是此时 B 发出的 ACK 丢了, 此时 A 作为发送端, 发现好像不对劲, 我的ACK呢? 此时就会看看自己的状态是啥, 发现是FIN_WAIT_1
, 于是选择重新发送一次 FIN.
那么下面的这个TIME_WAIT
也是同理的, 这个TIME_WAIT
状态是为了防止A发出的最后一个 ACK 丢失. 我们现在假设 A 不会进入TIME_WAIT
而是直接进入CLOSED
, 也就是直接断开连接. 那假如此时 A 最后发出来的 ACK 丢了, B 依旧处于LAST_ACK
状态, 过了一段时间 B 发现不对劲, 怎么没有 ACK? 于是超时重传, 但是此时 A 已经关了, 没人能再发一次这个 ACK 了. 那么此时 B 就永远都无法接收到 ACK 了
因此TIME_WAIT
就是为了能够成功将最后一个 ACK 送达而设立的一个状态, 那么这个TIME_WAIT
持续多久呢?
这里假设网络上两个节点的进行传输的最大消耗时间为 MSL, 那么这个等待的时间就是 2MSL, 对于使得 ACK 重传成功这件事情属于是绰绰有余
滑动窗口
TCP的可靠传输是肯定会影响效率的, 因为发送端需要等待接收端返回的ACK, 因此不能一下发很多数据. 而这个滑动窗口就是为了能够让TCP在保证可靠传输的情况下, 加快一点传输的效率.
我们刚刚也说过, TCP传输最影响效率的一点就是每一次发送都要等待ACK, 也就是确认应答机制. 而这个滑动窗口就是用来缩短这个确认应答机制的等待时间的, 先来看一个例子
假如正常情况下, 数据的发送是如图这样发送的
那么引入滑动窗口后, 数据就可能是这样发送的
实际上就是发送端直接不等待ACK了, 直接开始批量发送后面的数据, 但是这也不是无限制的传输, 达到上限之后会一起等待ACK, 直到收到下一个ACK的时候在继续发送. 而这个上限的数据量就被称作为窗口大小
我们以发送端的视角来看一下上面这个图的过程
假设A现在开始发送数据, 窗口大小如下图
当发完2001~3000时, 此时到达上限, 发送端开始等待, 直到收到第一个ACK后, 窗口往后移一位, 开始发下一条数据(也就是3001~4001)
每当接收到一个ACK, 那么就能证明前面的那个数据成功发送了, 那么就不用管了, 直接往后移发送后面的数据就行
窗口越大, 那么一次性能够传输的数据就越多, 此时传输效率就越高
但是上面所说的都是建立在正常传输的情况下, 那假如我传输过程中发生了丢包, 那咋办?
这里的重传和前面的超时重传, 也有一些变化, 依旧是分两种情况: 1. ACK 丢了 2. 发送的数据报丢了
我们先看第一个情况, 假如 ACK 丢了, 实际上这个时候大多数情况是不用管的, 因为我们可以根据后面的ACK来确认前面的数据是否送达. 比如下面的这个图
可以看到, 假如我们这 1001 的这个 ACK 丢了, 但是后面 2001 的 ACK 发过来了, 那么就可以知道 2001 前的数据全都读完了, 也就是前面是没有问题的, 所以可以不需要进行任何重传
有人可能感觉很奇怪: 你 2001 和我 1001 有什么关系?
我们要知道这个返回的ACK中的确认序号, 是为了告诉发送端, 这前面的数据我都读了, 你之后就从这为起点读就行. 那么这个 2001 的情况就已经涵盖了 1001 的情况了, 也就是说不用管也可以
举一个例子, 别人问你, 你高中毕业了没? 你此时直接和他说我已经读大学了. 此时虽然看似是答非所问, 但是实际上大学的这个情况就已经涵盖了高中毕业的这个情况, 因此即便不直接回答我高中毕业了, 对面也能知道你确实高中毕业了.
那么接下来我们就看数据报丢失的情况, 这个情况就不太一样了, 直接看图
我们上面也说过, ACK 的确认序号指向的是接收端要发送端读的数据, 而这里也很能体现这一点, 当发送端发出的数据报丢失后, 接收端即使接收了后面的数据, 也持续的返回 ACK 指向数据丢失的那一段. 而一旦发送端发现, 接收端一直索要这一段(3次重复的ACK), 那么就会重传这一段, 成功重传后, 返回的ACK中的确认序号又会指向后面需要的数据
上述的重传操作, 并没有任何冗余操作, 哪个数据丢了就重传哪个, 整个操作是比较快速的, 因此这个操作就被称作为快速重传, 是滑动窗口机制下, 超时重传的一个变种
滑动窗口机制, 是在检测到传输的数据量变大后自动启用的. 假设通信双方此时传输的数据量很小, 也不频繁, 那么依旧是按照普通的确认应答和超时重传机制来进行数据的传输. 但是反之, 如果通信双方传输的数据量很大, 也比较频繁, 那么就会自动进入滑动窗口模式来提高通信效率
上面有说过一个点, 滑动窗口的窗口越大, 传输效率就越快. 那么此时就有一个问题: 这个滑动窗口是越大越好吗?
答案是: 并非如此, 如果真的越大越好, 那还不如直接一开始全传了得了
如果窗口大小过大, 那么此时发送的数据就可能会超过接收端的处理能力, 此时处理方不仅处理不过来, 同时还容易发生丢包, 可以说是得不偿失
TCP 的初心就是可靠传输, 所有的机制都必须在保证可靠性的前提下运行, 因此窗口过大是不可取的, 那么此时自然就引出了一个问题: 如何确定这个窗口的大小呢?
流量控制
实际上窗口的大小并不是固定的, 而是会根据接收方的处理能力动态变化的, 这个机制就被称作是流量控制. 那么发送方又要如何知道接收方的处理能力从而调整自己的窗口大小呢? 这首先要从接收方如何处理这些数据的说起.
发送方发送过来的数据, 会被接收方先放到一个叫做接收缓冲区的地方, 然后这些数据就会存放在这, 等待接收方的应用程序来进行读取. 一旦被读取了, 那么这个存在于缓冲区的数据就会被删除. 这实际上也是一个生产者消费者模型, 生产者是发送方, 消费者是接收方, 中间场所是接收方的接收缓冲区.
如何判断接收方的处理能力是否足够, 最简单的方法就是看这个缓冲区的空位. 如果剩余空间很大, 证明接收方还有余力, 如果剩余空间很小甚至没了, 那就证明接收方要应付不过来了
就和一个经典的防水加水问题一样, 往一个池子里加水和放水, 如果池子要满了, 证明水放的有点慢以及加水加快了, 如果池子很空, 证明水放的很快以及加水加慢了
为了告诉发送方应该如何调整发送数据的速度, 接收方就会通过 ACK 中的窗口大小部分来告诉发送方自己接收缓冲区的剩余空间
此时有人可能会问, 这个用来表示接收缓冲区的窗口大小只有两字节, 那么是不是最大只能表示64kb的数据呢?
不一定, 还记得我们说过报头中有一个可选的东西叫做选项吗? 实际上在选项中就有一部分可以拓展这个窗口大小的选项, 称为"窗口拓展因子", 通过扩展因子, 就可以让窗口大小表示一个更大的值
下面我们来看一个流量控制的例子, 如下图
我们一点一点来看, 首先是正常发一个数据报, 然后返回的ACK发现剩余的空间还有3000, 因此发送方此时调整窗口大小, 直接一轮发送3000数据, 此时ACK返回后发现缓冲区空间为0了, 那么发送方此时就会等待.
等待到一定时间后, 如果发送方还没接到窗口更新的通知, 发送方会自动去探测一下, 它会发送一个没有任何内容的数据报, 来获取一个 ACK 看看窗口大小怎么样, 如果还是 0, 那么就继续等待, 如果不是 0, 则开始发送新的数据.
假如接收方处理完数据后, 那么就会发送一个窗口更新通知, 告诉发送方: 我这边数据已经弄完一些了, 你再搞点来. 然后发送方收到这个信息后, 就会开始传数据. 假如这里这个窗口更新通知丢了, 那么还可以通过发送方的自动检测来检测窗口的变化.
拥塞控制
流量控制, 考虑的是接收方的处理能力. 但是在网络中, 数据的传输不仅仅是涉及接收方本身, 还有中间的通信路径是否能够传输这样的问题.
假如中间路都不通, 窄窄的一条, 即使发送方传再多, 你接收方再能处理, 也没有任何作用. 网络通信是要经过很多节点的, 如果我们传输的数据过多, 一旦中间有哪个节点传不了这么多, 那么此时就会直接把数据丢掉, 导致丢包, 影响可靠传输
而这里的拥塞控制, 就是为了考虑中间节点的情况而诞生的机制, 但是又有一个问题, 中间的节点那么多, 并且每个节点还不一样, 可能用的是什么不同的路由器交换机啥的, 那这我们如何知道它能传输的数据的量, 又从何控制数据发送的量?
由于网络路径中的节点情况复杂, 因此很难直接得到量化的数据, 因此 TCP 采用的是"探测实验"的方式来寻找一个合适的值.
探测实现, 主要就是让发送端先慢慢的发数据, 同时增加窗口大小, 加快自己数据的发送速度. 假如一直没有发生丢包问题, 那么就持续加速, 但是一旦加速到一定速度后, 中间的节点顶不住了, 发生了丢包, 那么此时就会把窗口大小缩小, 减慢自己的数据发送数据
后面就是一直按照这个逻辑进行调整, 最后达到一种动态平衡的效果
这张图可以简单的描述拥塞控制的调整逻辑, 在刚开始的时候增长速度还是比较慢的, 然后会指数增长一段时间, 直到达到一个阈值, 此时增长变为线性增长, 假如此时突然遇到丢包, 那么此时窗口大小就会回到初识状态, 然后阈值也会下调, 之后依旧是按照这个规律一直进行
但是上面这个实际上是经典版本的拥塞控制, 后续的TCP版本中, 对遇到丢包后的处理又进行了一步优化, 如下图所示
优化后的拥塞控制, 就是让传输速率不要直接回到最小值, 而是直接降到阈值直接开始线性增长, 也就是直接忽略了后面发生丢包后, 指数增长的过程
上面介绍的两个机制都是为了控制窗口大小, 防止传输过快导致无法保证可靠性的问题. 在最终传输时, 取得窗口大小, 是上面两个机制中计算出的两个窗口大小的较小值(可靠性优先)
紧急传输
上面我们说到, TCP 提供了一个缓冲区, 让数据能够存储在那里慢慢按照顺序处理. 但是有些时候, 假如突然有了一个紧急情况, 我想要对方能够赶紧处理一个特殊数据怎么办呢? 比如我想要立刻就把连接断开.
此时 TCP 就提供了一个紧急传输的机制, 我们可以通过发送一个 URG 包, 告诉接收端, 你赶紧处理一下这个数据. 这个 URG 同样在标志位中也有一个位置来进行表示
同时, 既然发送端告诉接收端要赶紧处理数据, 那要处理什么数据, 那部分的数据? 此时就是通过紧急指针来告诉接收端的.
当接收端接收到一个 URG 包后, 他就会看看这个紧急指针的值是多少, 然后迅速处理掉这些数据. 其中序号和紧急指针区间内的数据就是紧急数据.
例如序号是 5, 紧急指针指向 10, 那么此时 5 到 10 的部分就是紧急数据, 需要尽快处理.
数据推送
由于 TCP 的缓冲区存在, 有些时候数据会存储在缓冲区里, 慢慢等待处理, 但此时有一些数据, 我希望你能够立马处理, 而不是在缓冲区里面等着. 此时就可以借助 TCP 的数据推送机制
发送端可以通过发送一个 PSH 包告诉接收端, 你不要缓冲了, 直接把数据传上去就行. 同理, 这个 PSH 也是在标志位中有一个位置的.
那此时可能有人就要问了: 你这个缓冲区, 不是用于缓存数据的吗? 那难道不是程序要就会去读? 你这样丢上去不会导致程序处理不过来吗?
实际上这就涉及到了不同场景的问题, 有些时候程序并不会主动的要数据, 而是要 TCP 传上去, 但 TCP 也并不是说收到了就一定会直接传上去的, 它也可能会对缓冲区中的数据进行一些聚合, 后面再一起发送. 此时就可以通过推送操作直接把数据推上去
延时应答
延时应答, 顾名思义就是接收端等一段时间再给发送端返回ACK, 那么为什么要延时发送ACK呢?
实际上也是为了提高传输效率, 我们知道发送方的窗口大小, 就是传输效率的关键影响因素, 假如我缓冲区没空间了, 返回的ACK里窗口大小是0, 你的ACK返回再快发送端也不会发送任何数据, 并且后面还要消耗发送端探测或者接收端返回更新通知的时间
那么不如我的接收端就等一会, 等自己先处理一部分数据后, 此时自己的接收缓冲区就有空位了, 那么此时再去发送一个ACK告诉发送端: 我现在有空了, 你赶紧发送点数据过来
捎带应答
捎带应答, 是延时应答的进一步优化. 在网络通信中, 大部分情况都是"一问一答"的情况, 也就是发送端发送了请求并且由接收端确认接受后, 接收端还要再返回一个响应. 那么这个捎带应答就是让这个要返回的响应带着确认收到的ACK一起发出去.
这个和延时应答的区别就是, 延时应答只是延长了ACK返回的时间, 没有涉及到一起发的操作, 而捎带应答则是响应带着ACK一起发
例如我们之前说过的四次挥手是可以变成三次挥手的, 此时有一种可能就是触发了捎带应答
当 ACK 触发了延时应答, 没有立即返回. 过了一会后 FIN 要返回了, 发现这个 ACK 还在这等着, 于是就让这个ACK 搭个便车, 一次性一起返回了, 此时就变成了三次挥手
面向字节流
面向字节流这个特性在刚开始讲解确认应答的时候就提到了, 主要指的就是通过字节流去传输数据, 其优点就是能够分批次的读取. 但是同时, 只要是面向字节流的东西, 都会有一个缺点, 这个缺点就是粘包问题
什么是粘包问题, 实际上就是我们分批次发送的数据包, 读取的一方并不知道你哪些是一批数据包, 此时就可能会发生读取问题
为了解决读取问题, 常见的有两个解决方案: 1. 数据前面加一个数字表明要读几个字节 2. 采用分割符
第一个方法虽然可行, 但是也有问题, 假如我传的数据里面就有数字怎么办?
因此还有一种方式就是采用分隔符来解决问题, 例如传输数据的时候可以添加一些特殊字符, 例如\n
, \r\n
这样的, 随后读取的时候就按照这个分割符来读
异常处理
在通信过程中, 通信双方难免会发生一些异常情况. 下面就介绍一些异常情况发生后, TCP会进行的处理方式
- 进程崩溃
进程崩溃, 相当于进程直接结束, 相当于直接执行断开连接操作, 此时就是正常的执行四次挥手的流程
此时可能有人感觉很奇怪: 我进程都关了,你怎么还能挥手?
实际上我们的 TCP 连接和进程是有一定的独立性的, 进程结束 TCP 连接不一定就会直接断开.
- 主机关机
主机执行关机操作的时候, 是会在关机前强制关闭所有进程的操作的. 因此刚开始的时候还是和进程崩溃差不多的, FIN 还是可以正常发送出去的. 但是对方的ACK和FIN返回的时候, 那就有点不一样了
假如对面返回 ACK 和 FIN 的时候, 主机还没完全关闭, 此时就还是可以进行正常的四次挥手的流程的. 但是假如当对面的 ACK 和 FIN 传回来的时候, 这边的主机已经关机了, 那自然也就没有人来接受这个 FIN 和 ACK 了. 但是此时对端也不知道是什么情况, 此时就会尝试重传 FIN, 然后一次次重传后发现一直没有反应, 就会自己放弃连接(删除对端信息)
- 主机断电
主机断电那就是一个完全不同的情况了, 主机断电则相当于所有的进程和设备一瞬间结束运行, 那么这边自然也就是没有时间来发送FIN的.
此时就只能依靠另外一端来断开这个连接, 此时也分两种情况
- 断电的这一方是数据的接收方: 此时发送方发送了数据发现一直没有 ACK 回来, 那么就会触发超时重传. 重传一定次数后, 还没有响应, 就会触发 TCP 的连接重置功能, 发送一个复位报文段, 如果此时这个报文还是没有 ACK, 那么此时发送方就会释放这个连接. 其中复位报文段就是报头中RST标志位为1的报文
- 断电的这一方是数据的发送方: 如果数据的接受方发现一直没有数据过来, 此时它也不知道是真的没有数据过来还是发送方没了. 但是接收方也不会死等, TCP 中提供了一个心跳机制, 这个机制让数据的接收方也可以定期的发送一个特殊的, 不含任何业务数据的数据报, 并且期望对方返回一个ACK. 但是假设反复多次后都没有回复, 此时接收方就会认定为数据的发送方没了, 就会单方面的释放连接
- 网线断开
网线断开实际上和刚刚的主机断电非常类似, 因为都导致了两方一瞬间开始后无法进行任何通信
那么此时也是类似的运行模式:
- 假设发送端已经发送数据 -> 网线断开 -> 发送端发现一段时间后没有ACK, 触发超时重传 -> 多次重传一直没有ACK, 释放连接
- 假设发送端没有发送数据 -> 网线断开 -> 接收端发现一段时间后没有数据, 触发心跳机制 -> 多次发送一直没有应答, 释放连接
心跳机制
心跳机制实际上是日常开发中比较常见的一种机制, 在那种有很多服务器的场景中, 为了能够确认一个机器是否挂了, 那么就通过心跳机制来检测.
只不过一般不会使用TCP的心跳机制, 因为TCP的心跳机制触发时间比较长, 需要分钟级别, 而有时候我们希望能够在秒级甚至毫秒级知道这个机器挂了没, 此时就需要自己在应用层中自己实现.
UDP 和 TCP 的区别
TCP 的初心就是可靠传输, 由于在大部分情况下, 可靠性都是优先的, 因此 TCP 是可以用在绝大多数的场景下的. 而 UDP 则是更加追求效率, 但是可靠性就没有那么高了, 因此更加适用于一些"可靠性要求不高, 但是性能要求高"的情境下. 例如同一个路由器的局域网下, 因为在同一个小的局域网下, 网络结构简单, 宽带充足, 那么就不容易发生丢包.
如果是要传输比较大的数据报, 那么则是 TCP 优先, 因为 UDP 天然有着 64kb 的限制.
如果要进行广播传输, 则 UDP 优先, 因为 UDP 自带这个机制, 而 TCP 不支持这个操作, 需要自己实现.
此时可能就会有人问了: 广播传输是什么东西呢?
实际上广播传输就是将数据发送给局域网中的所有设备. 这就是广播传输. 举一个现实中的例子, 我们手机假如想要投屏, 手机就会先向局域网进行广播传输, 询问一下局域网中的哪个设备支持投屏, 此时支持投屏的设备收到了这个数据报就会回应, 然后把自己的信息传回去, 最后手机就可以进行投屏了
常见问题: 如何基于 UDP 实现可靠传输?
答案: 本质上考察 TCP 机制, 直接照搬 TCP 机制即可
下面是偏总结方面的区别
- TCP 是有连接的, UDP 是无连接的
有无连接指的是通信双方是否保存对端的信息, 假如 TCP 想要发送数据, 则要先请求建立连接, 然后拿到对方信息了才会发送, 但是假如此时对方拒绝了, 那么此时通信就无法完成. 而 UDP 则是什么都不管, 只管发数据. 并且由于 UDP 数据中不保存对端信息, 所以使用 UDP 的时候是需要我们手动去指定数据发哪去, 从哪里接
- TCP 是可靠传输的, UPD 是不可靠传输的
TCP能够知道自己发出的信息是否成功到达, 因此可以根据到达情况来选择应对措施. 而UDP依旧是只管发, 其余什么都不管
- TCP 是面向字节流的, UDP是面向数据报的
很容易理解, TCP 是通过字节流来传输数据, 而 UDP 传输数据前需要我们将其打包为数据报后才能发送
- TCP 和 UDP 都是全双工的
全双工指的是能够双向通信, 两端可以互相发送信息