计算机网络
[TOC]
参考资料
https://www.xiaolincoding.com/network/ 2.1、4.1、4.2、4.6、4.7章节
./计算机网络笔记
学习时间:2024年1月7日
学习大纲
-
☑ 2.1 TCP/IP 网络模型有哪几层?
-
☑ 4.1 TCP 三次握手与四次挥手面试题
- ☑ TCP 基本认知
- ☑ TCP 连接建立
- ☑ TCP 连接断开
- ☐
Socket 编程
这个 socket 比较具体的我还是没什么具体的理解
-
☑ 4.2
- ☑ TCP 重传
- ☑ 滑动窗口
- ☑ 流量控制
- ☑ 拥塞控制
-
☑ 4.6 如何理解是 TCP 面向字节流协议?
-
☑ 4.7 为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?
学习笔记
TCP/IP 网络模型有哪几层?
经典TCP/IP模型:
- 应用层
- HTTP、FTP、Telnet、DNS、SMTP
- 只需要打包数据, 其他传输方面的不用管
- 应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。
- 传输层
- TCP/UDP
- TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。
- TCP 分段为
segment
传输 - 只有发件人和收件人信息
- 网络层
- IP 协议
- IP 协议分割为包
packet
进行网络发包 - 主要是网络路径选择的问题, 考虑各个网路节点的选择问题
- **
子网掩码
**将 IP 地址分成两种意义:- 一个是网络号,负责标识该 IP 地址是属于哪个「子网」的;
- 一个是主机号,负责标识同一「子网」下的不同主机;
- 链路层
- 路由协议, 转换成比特流, 差错检测, 纠错 (IP <==> MAC 地址)
- 物理层
- 物理信号
TCP 基本认识
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小
。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
- ACK:该位为
1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
。 - RST:该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接。 - SYN:该位为
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。 - FIN:该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位为 1 的 TCP 段。
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
- 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
- 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
连接一个 TCP/IP 需要
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
TCP 连接建立
为什么要三次握手: 连接时由于网络状态不可控, 各自发送探测数据包, 保证一对一确认, 从而确保网络是正常的, 连接是稳定的
避免历史连接
三次握手: SYN --> SYN-ACK --> ACK
-
只有 SYN 会被重传, 每次丢失超时重传, 超时重传时间翻倍
-
最后一次 ACK 不会重传, 没收到默认重新建立连接
-
三次握手: 防止历史连接, 保证 TCP 连接是连续性的
-
SYN 攻击 ==> 理解为一个 DDoS 攻击
TCP 连接断开
四次挥手: 互相 FIN --> ACK
为什么需要 4 次挥手:
FIN 表示不再接受信息, ACK 表示了解应答
客户端 FIN 之后, 服务端可能还有信息要发, 最后服务端没有信息发送的时候, 服务端发出 FIN 等待客户端应答 ACK
整个流程为连接断开流程
2MSL: 防止 ACK 没有传到, 给一次再发的机会
TCP 重传机制
超时重传: 非常好理解
快速重传: 当接受到三个相同的 ACK 就会触发快速重传, 这个好处就是快, 避免了 ACK 的等待时间 但是有一个问题: 对于丢失的包的序列 (比如连续三次受到的是 ACK3 ,表示 SEQ3 丢失), 是只重传这一个, 还是后面的包也重传?
- 如果只选择重传 Seq3 一个报文,那么重传的效率很低。因为如果丢失的报文还包括后续的 Seq4 报文,还得在后续收到三个重复的 ACK4 才能触发重传。
- 如果后面的都重传, 那么没丢的包又重传了一遍, 其实导致严重的资源浪费
SACK 重传 (选择性确认)
: 对于快速重传问题的补充
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
Duplicate SACK:
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了, 而不是丢失了
D-SACK 信号的显著特点: ACK 号 >> SACK 序列号
- 情景一: 接收方发的 ACK 丢失了, 触发发送方超时重传历史的包, 导致重复
- 情景二: 历史连接的包被网络延迟后重复到达 (这个序列的包已经触发重传传了一次了)
滑动窗口
一问一答效率过低, 有了窗口之后,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
固定窗口也有资源浪费的问题, 引入**滑动窗口
**:
- 对于
发送方
:
- 对于
接受方
:
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
流量控制 ==> 对发送方的处理能力的利用
TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
双方根据自己的窗口大小动态调整发送的数据大小并通知对方下一次还可以发的数据大小
TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。
为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
- 窗口关闭
有潜在死锁风险
接收方向发送方通告窗口大小时,是通过 ACK
报文来通告的。
当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
- 糊涂窗口综合症
如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
// Nagle 伪代码
if 有数据要发送 {
if 可用窗口大小 >= MSS and 可发送的数据 >= MSS {
立刻发送MSS大小的数据
} else {
if 有未确认的数据 {
将数据放入缓存等待接收ACK
} else {
立刻发送数据
}
}
}
拥塞控制 ==> 对网络带宽的利用
尽可能利用网络带宽
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
cwnd 表示这一次可以比上一次多发的数据大小
拥塞控制主要是四个算法:
有一个叫慢启动门限
ssthresh
(slow start threshold)状态变量。
- 当
cwnd
<ssthresh
时,使用慢启动算法。- 当
cwnd
>=ssthresh
时,就会使用「拥塞避免算法」。
-
慢启动
- 当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
-
拥塞避免
- 每当收到一个 ACK 时,cwnd 增加 1/cwnd。
-
拥塞发生 --> 重传计时超时 --> 触发重传算法
-
超时重传
-
ssthresh
设为cwnd/2
,cwnd
重置为1
(是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)
-
-
快速重传 (
能收到三个 ACK 的时候网络也没那么糟糕
)-
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;进入快速恢复算法
-
-
-
快速恢复
-
拥塞窗口
cwnd = ssthresh + 3
(加 3 代表快速重传时已经确认接收到了 3 个重复的数据包); -
重传丢失的数据包;
-
如果再收到重复的 ACK,那么 cwnd 增加 1;
这个过程的目的是尽快将丢失的数据包发给目标。
-
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值 (
恢复结束
)
-
首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。
其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。
TCP是面向字节流的协议
之所以会说 TCP 是面向字节流的协议,UDP 是面向报文的协议,是因为操作系统对 TCP 和 UDP 协议的发送方的机制不同,也就是问题原因在发送方
。
发送 UDP 时系统不会对消息进行拆分, 每个 UDP 就是一个用户消息的边界, UDP 队列里的每一个元素就是一个 UDP 报文
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。因此,我们不能认为一个用户消息对应一个 TCP 报文
,正因为这样,所以 TCP 是面向字节流的协议。
- 粘包
当**两个消息
的某个部分内容
被分到同一个 TCP 报文
**时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。
需要划分用户消息
的边界 (不是TCP报文的边界, TCP的报文边界由 TCP 套接字做好了)
一般有三种方式分包的方式:
- 固定长度的消息 ==> 不灵活没人用
- 特殊字符作为边界 ==> HTTP 协议中用回车和换行表示一个消息边界
- 自定义消息结构: 自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?
主要原因是为了防止历史报文被下一个相同四元组的连接接收。
初始序列号的算法 ISN: ISN = M [计时器] + F(localhost, localport, remotehost, remoteport) [hash算法] 最大程度保证 ISN 不会随机到同一个
- 回绕问题: SEQ 只是一个 32bit 的数, 超过 4G 就会回绕, 所以 TCP 可以追加一个时间戳 (默认 32bit), 它有两个好处,一个是便于精确计算 RTT ,另一个是能防止序列号回绕(PAWS)。