文章目录
- TCP核心机制
- 1. 确认应答
- 2. 超时重传
- 3. 连接管理
- 三次握手
- 四次挥手
- 4. 滑动窗口
- 5. 流量控制
- 6. 拥塞控制
- 7. 延时应答
- 8. 捎带应答
- 9. 粘包问题
- 10. 异常情况
TCP核心机制
1. 确认应答
(上篇)
2. 超时重传
(上篇)
3. 连接管理
建立连接的流程: 三次握手
断开连接的流程: 四次挥手
握手和挥手, 是打招呼的过程, 此时两个主机并没有进行实质上的数据交互, 只是为了建立连接或断开连接而打招呼
三次握手
建立连接, 就是通信双方, 各自保存对端的信息
据图完成上述过程, 需要经过三次网络交互
第一次: 三次握手的第一次, 一定是客户端先发起的
SYN数据报, 是标志位中的第五位
SYN, synchronize, 同步, 在TCP中, 同步 则是希望 服务器和客户端之间, 达成某种配合的关系, 达成某种有关联得状态
这个数据报, 不携带任何业务数据, 载荷部分是空的, 只有TCP报头
第二次: 服务器收到SYN, 需要返回ACK
然后还要给客户端发送SYN
这两次交互ACK 和 SYN 可以合并成一次网络通信, 将第二位和第五位标志位设为1即可
这么做的目的实际上是为了提高效率
第三次: 客户端收到SYN, 返回ACK
- 三次握手的意义:
- 1) 三次握手, 相当于"投石问路", 在证书传输业务数据之前, 先确认一下通信链路是否畅通
也相当于是TCP可靠传输的一种保证(辅助机制) - 2) 通过三次握手, 来确认通信双方, 发送能力和接收能力都是正常的
举例:
此时, 只有通过三次交流, 才能完全确认发送方和接收方各自的发送能力和接收能力 - 3) 三次握手, 还需要协商一些必要的参数, 例如数据的开始序号
序号往往不是从 0 / 1 开始的, 而是三次握手的时候, 通信双方协商出一个数字
考虑一下场景:
第一次建立连接, 我们称"前朝", 第二次建立连接. 称"本朝"
如果前朝, 某个数据报, 在网络通信的过程中, 迷路了, 走了一个非常远的路线(没有丢包)
等待这个数据报饶了半天终于到达服务器的时候, 之前的连接已经断开了, 现在是新的连接了
那么服务器如何判断这个数据报是前朝还是当朝的呢?
就是根据序号来区分了
每次建立连接, 都是从一个新的数字, 作为起始序号的
当前本朝的数据, 序号一定是沿着我们其实序号往下的数字, 不会相差很多
如果收到一个数据报, 序号和其实序号差别很大, 就可以任务是前朝的数据报了, 此时就可以直接丢弃了
连接状态:
掌握两个即可:
listen: 是服务器出现的状态, 当服务器绑定端口成功之后, 机会进入到listen状态
established: 建立完成, 可以随时进行后续通信了
四次挥手
四次挥手, 客户端和服务器都可以主动发起断开连接
第一次: 发送方发送FIN 结束报文
FIN finish 结束报文, 标志位中的最后一位
第二次: 接收方收到FIN, 立刻返回ACK
第三次: 接收方的应用程序代码中, 调用close的时候, 才会触发FIN报文
意味着, 当收到发送方的FIN结束报文时, 返回ACK 和 发送FIN 并不是同一时刻发生的,
可能过了好久才执行close, 所以第二次和第三次挥手, 在正常情况下, 是不能合并的
但是, 在特殊情况下, 两次挥手可以合并
在TCP中, 有一个机制"延时应答", 回复ACK不是马上回复, 这个情况下, 就可以合并
第四次: A收到FIN, 返回ACK
只有当双方都收到了ACK, 连接才会断开, 双方才会删除对方的信息, 上述挥手过程只是在通知对方
状态:
CLOSE_WAIT: 被动一方的状态, 在接收FIN报文后, 等待代码调用close
代码中调用close越及时, 这里的状态就越不容易看到
如果有的时候, 在服务器看到大量的CLOSE_WAIT, 说明大概率代码忘记调用close了
TIME_WAIT: 主动一方的状态, 存在的意义, 就是为了应对最后一个ACK丢包这样的场景
主动方在收到被动方返回的FIN后, 不能立即释放TCP连接, 如果立即释放, 后续一旦对端重传了FIN, 此时, 主动方就无法应对了, 无法返回ACK了
但是, TINE_WAIT状态不是持续的, 而是有一定时间的, 在一定时间内, 如果没有收到重传的FIN,
意思就是最后一个ACK对方已经收到了, 此时TIME_WAIT就可以释放了
TIME_WAIT等待的时间, 一般是2MSL
MSL 理论上, 表示网络中两个结点之间传输数据的最大时间, 这个数值通常是1min
那么TIME_WAIT意味着2min后, 没有收到重传FIN, 就直接释放
4. 滑动窗口
滑动窗口, 是提高效率的一种机制
不引入滑动窗口:
数据传输过程, A收到一个ACK, 才会发送下一个数据, 比较低效
引入滑动窗口:
从一条一条发送, 到批量发送, 把等待时间重叠了, 可以提高效率
不等待ACK, 批量发送多少数据, 这个数据量就表示窗口大小, 上图的窗⼝⼤⼩就是4000个字节(四个段).
发送前四个段的时候, 不需要等待任何ACK, 直接发送;
收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推
滑动窗口, 如果出现丢包怎么办?
情况一: 数据报已经抵达, ACK丢了
此时不需要做任何处理
1001的ACK丢包, 但是后面的2001的ACK并没有丢包, 此时主机B已经接收到了1-2000的字节数据, 那么返回的ACK就包含了前面的效果
同理, 5001返回的ACK, 也包含了前面的效果
情况二: 数据包丢了
1001-2000的数据包丢了, 接收方按理来说应该按顺序接收字节数据, 此时他没有收到1001-2000的数据, 那么接下来不管发送什么数据, 都返回下一个应该是1001的ACK
那么, 当A连续收到若干个这样的ACK就会明白, 1001-2000这个数据包丢了, 所以就重新发送
当B收到这个数据包, 就会结合之前收到的所有数据包, 索要下一个数据包
当某处存在缺口时, 返回的ACK, 确认序号都是在索要这个缺口的数据
一旦缺口补充上, 接下来就可以从队列中最后一个数据的序号继续往后索要
上述这个, 在滑动窗口下, 搭配的丢包重传处理机制, 称为"快速重传"
如果TCP传输的数据比较少, 不频繁, 此时就不会触发滑动窗口, 这时仍然按照超时重传的方式解决丢包问题 如果TCP短时间传输大量的数据, 此时才会触发滑动窗口, 按照快速重传的方式解决丢包问题
滑动窗口虽能够提升效率, 但速度是不可能比UDP这种没有可靠机制的协议更快的
5. 流量控制
滑动窗口机制, 窗口大小是可变的, 可以通过窗口大小, 来控制发送方的发送速度
窗口越大, 发送数据越快, 但可能接收方处理不过来, 发生丢包
窗口越小, 发送速度越慢
那么此时, 最合理的做法, 就是接收方根据自身能力, 反向制约发送发的速度, 是双方达到一种平衡, 这样的机制, 就叫"流量控制"
接收方, 有一个接收缓冲区, 以未使用的空间大小作为发送方发送的窗口大小
在返回ACK的时候, 在TCP报文中有一个字段, 表示上述空闲空间大小
16位窗口大小, 这个字段只在ACK报文中生效, 含义就是接收方接收缓冲区的空闲空间的大小
16位, 能够表示64KB, 如果此时空闲空间大于64KB, 那么在选项中, 包含了一种特殊属性"窗口扩展因子"
最终的空闲空间大小为: 窗口大小 <<(左移) 窗口扩展因子
如果此时窗口大小为64KB, 窗口扩展因子为2, 那么实际上的空闲空间是64KB<<2, 相当于*4 = 256KB
发送方就可以根据这个来控制窗口大小
6. 拥塞控制
拥塞控制, 也是控制窗口大小的
上述的流量控制, 我们只考虑了接收方, 但是网络通信, 是要经过很多的交换机/路由器的, 中间的设备, 我们也不能忽视
那么我们可以根据实验的方式, 找到一个合适的窗口大小
刚开始按照小的窗口来发送数据, 如果没有出现丢包, 说明中间链路畅通, 就可以增加速度, 增加窗口大小
如果增加到一定程度, 出现丢包了, 此时发送方立即减小窗口大小, 继续发送看是否还会丢包
如果不丢包, 继续加
如果丢包, 继续减
这样就能找到一个合适的窗口大小完成运输了, 上述过程就称为"拥塞控制"
举例:
①刚开始, 以比较小的窗口来传输数据
②如果没有出现丢包, 按照指数方式扩大窗口
③指数增长过程中, 达到某个阈值, 就要变成线性增长
④增长到一定程度, 出现了丢包, 此时立刻把窗口变小
⑤缩小有两种方式:
1)直接缩到底, 回到最初慢启动 — 已经废弃
2)缩到出现丢包时窗口大小的一半 — 当前的方式
由于拥塞控制和流量控制都是控制窗口大小, 谁窗口小我们就听谁的!
7. 延时应答
延时应答, 本质上也是为了提高传输的效率
通过延时, 就可以使窗口大小得到提升, 从而提高效率
延长的时间, 有两种方式决定:
1)按照一定时间来延时
2)按照收到的数据量来延时
这两种策略是结合使用的
8. 捎带应答
建立在延时应答的基础上, 进一步提高效率
正常来说, ack是内核收到请求, 自动返回的
响应数据, 则是应用程序代码, 是执行了一系列逻辑之后, 返回的
由于延时应答, ack不一定会立即返回, 在ack等待的过程中, 正好要返回响应数据, 那么响应数据就会捎带着, 在TCP的报头中, 将确认序号/窗口大小都设置上, 就把两次传输合并成了一次传输, 这样的策略就叫"捎带应答"
9. 粘包问题
⾸先要明确, 粘包问题中的 “包” , 是指的应⽤层的数据包.
在TCP的协议头中, 没有如同UDP⼀样的 “报⽂⻓度” 这样的字段, 但是有⼀个序号这样的字段.
站在传输层的⻆度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.
站在应⽤层的⻆度, 看到的只是⼀串连续的字节数据.
那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应
⽤层数据包
此时, 服务器就无法区分哪些是完整的应用层数据包
解决办法:
1)使用分隔符
2) 约定包的长度
思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
• 对于UDP, 如果还没有上层交付数据, UDP的报⽂⻓度仍然在. 同时, UDP是⼀个⼀个把数据交付给应
⽤层. 就有很明确的数据边界.
• 站在应⽤层的站在应⽤层的⻆度, 使⽤UDP的时候, 要么收到完整的UDP报⽂, 要么不收. 不会出
现"半个"的情况.
10. 异常情况
情况一: 其中一个进程崩溃了
强制杀死进程和进程崩溃, 其实是同样的效果, 操作系统, 都能够回收释放对应的PCB, 可以释放里面的文件描述符表, 就相当于调用close
此时, 仍然可以正常进行四次挥手, 并且能够挥完
情况二: 某个主机被关机(正常流程的关机)
对于正常流程的关机, 操作系统挥尝试强制介乎所有用户进程, 然后再进入关机流程
那么在结束进程之后, 也会进行四次挥手
但是可能在关机之间挥完了, 也可能没挥完
情况三: 某个主机电源掉电
如果A和B通信, A突然掉电了
此时A无法做出任何反应, 就关了
1)如果B是发送数据的一方:
B接下来发的数据, 都不会有ACK, B就会触发超时重传, 重传急促, 发送复位报文(RST), 也没有响应
此时B就单方面删除保存的A的信息
2)如果B是接收方
B再一定时间内没有收到A的数据, 就会触发心跳包
连续发送几个心跳包, A都没有回应, 这时B认为A挂了 于是单方面释放连接
心跳包, 只是一个没有载荷的数据包, 如果A正常, 就能回应ACK; 如果A挂了, B不会收到任何回应
TCP虽然内置了心跳包, 但是这个心跳包, 周期比较长, 指望通过这个心跳发现对端挂了, 往往需要分钟级别的时间, 在实际开发中, 经常会在应用层实现心跳包, 频率更高, 周期更短, 例如ping-pong
情况四: 网线断开
本质上就是第三种情况
比如A是发送方, B是接收方
A的角度, 就会触发超时重传, 触发RST, 单方面删除信息
B的角度, 就会触发心跳包, 对方无响应, 单方面删除信息