目录
1.网络是不可靠的
计算机网络是不可靠的,存在 丢包、乱序、延时 。
这是众多 TCP 协议机制的设计出发点,万恶之源,将贯穿全文。
2.TCP 概念和特点
TCP 协议全称 传输控制协议
, 是一种 面向连接的、可靠的、面向字节流的 传输层通信协议。
TCP 在 TCP/IP 协议模型
中位于传输层。
TCP/IP 模型中各层数据包结构的关系如下图。
TCP 协议的主要特点:
- 面向连接,一对一通信。
- 可靠交付:保序、不重复、不丢失。
- 全双工通信:双方均可收发。
- 和上层应用进程的交互方式是 面向字节流的。
TCP 和上层应用进程的交互方式:
- 发送方可以发送不同大小的数据块到发送缓存。
- 先会对发送缓存内的数据分段,打包成 TCP 数据包再发送。
- 在接收一侧,数据包会先进入接收缓存区。
- 当一定数量的数据包全部到达,重排、组装,以数据流方式吐给接收方。
虽然 TCP 是面向字节流的,但是 TCP 所处理的数据单元却是面向报文段的, 也就是本文所说的数据包。
将看到,短短 可靠交付 四字背后并不简单。
3.可靠传输机制
若不止考虑 TCP 协议本身的实现,如何设计可靠的网络通信?
首先,由于 丢包的可能性,要实现可靠通信:
- 发送方要知道对方接收成功,因此需要接收方回复确认 即 ACK。
- 如果丢包发生,发送方需要重传。
一种触发重传的方式是,超时重传 (也有其他触发方法,见后续 TCP 重传机制 )。
网络延时发生时,重传可能会导致重复:
- 接收方会丢弃收到的重复数据包,但是仍然回复确认。
- 发送方会丢弃收到的重复确认包。
其次,对于如何发送确认包和重传包,有两种方式。
1.一问一答的方式
也叫做 停止并等待 ARQ 协议, 是指 发送方等到接收方的确认包后,再发下一个数据包 。
类似乒乓方式,具体来说:
- 如果时限内收到对方确认,才发送下一个数据包。
- 否则,重传当前数据包。
可以看到这种方式下,发送方大部分时间在等待,效率非常低。
2.流水线传输的方式
叫做 连续 ARQ 协议 , 是指 发送方会连续发送一组数据包,同时等待这些数据包的确认 。
具体来说:
- 发送方发送一批数据包。
- 同步地接收对方的确认包。
简单来说, 发送方不闲着,一边发送,一边等回复 。
可以看到这种方式相对一问一答的方式,效率要高。
如果发生丢包或延时,需要重传,有两种方式:
1.回退 N 重传
发送方每发送一个数据包,都会发起一个定时器。
一旦一个某个定时器触发,就会重传。
发送指针回退到未拿到确认的数据包处,以实现重传。
可以看到,此方法下, 会重传后面所有的数据包。
2.选择重传
同样,每发送一个数据包,都会发起一个定时器。
不同点仅在于, 只重传未拿到确认的数据包,不回退发送指针。
综合以上,得出的结论是,不考虑 TCP 协议的具体实现的话, 要实现可靠的网络通信,需要依赖确认和重传机制, 并且一个好的办法是采用流水线传输的方式。
而流水线传输方式,正是下一个部分 TCP 协议中的 滑动窗口机制 的引子。
4.TCP 滑动窗口机制
滑动窗口机制是 TCP 协议的精髓所在,它是 TCP 协议设计的基本框架 。
滑动窗口机制就是 流水线传输方式 在 TCP 协议中的细化设计, 发送方一边连续地发送数据包,一边等待接收方的确认。
滑动窗口分为两种:发送窗口 和 接收窗口。
由于 TCP 是全双工的, 所以通信的每一端都会同时维护两种窗口 。
数据包序号
在 可靠传输的基础机制 中, 接收的数据包可能是重复的、乱序的,因此 TCP 会对每一个数据包进行唯一标号, 叫做数据包的序号。
数据包的序号是 TCP 协议头
中的一个 32 比特大小的整数字段。
每次发送一个包,这个序号就会增加一。
TCP 是全双工的,两个通信端各自维护自己的序号。
因为 网络延时不可控, 如果两次连接建立时差很短、或者连接重建后老连接的数据包延迟到达, 会造成序号冲突。
所以,序号并非由固定数字初始化。可以综合时间、随机数来生成等。
确认号和累计确认
在 TCP 中,一个用以确认的回复包,会有确认号。
如果一个数据包同时也是一个确认包,那么它也会有确认号。
一个序号为 SEQ 的数据包,其确认包的确认号会是 SEQ+1 。
同样,确认号也是 TCP 协议头
中的一个 32 比特大小的整数字段。
可以理解为,接收方已收到序号为 SEQ 的数据包,期待发送方下一次给 SEQ+1 的包。
更广义的理解是, 确认号是接收方期望对方发送的下一个包的序号 。
下图中,发送方连续发送一组包,如果中间有丢包, 接收方则期待序号最小的丢失的包。当重传成功后, 接收方仍然期待下一个未拿到的数据包:
接收方所期待的是序号最小的没拿到的数据包 。
这种确认号的机制,即可实现累计确认机制:
接收方确认了标号为 SEQ 的数据包, 即代表确认了所有小于 SEQ 的数据包, 此时接收方给的确认号是 SEQ+1 。
累计确认其实是一种批量确认的机制,以减少确认包的数量。
此外,如果接收方恰好需要发送数据,确认号可以直接标在数据包上,即捎带确认。
一个问题是,如何控制累计确认的时机?
TCP 协议中的 Nagle
算法 给出的办法是 延迟确认。
其大概的原理是,未确认的包达到一定量、或者达到一个时间阈值,才回复一次确认。
所以说, 在默认的 TCP 协议中,确认不是立即回复的,而是延迟的 。
不过,接收方的延迟确认不应该过分延迟,否则会造成发送方的重传,浪费网络资源。
可以设置 TCP_NODELAY
选项 来禁用 Nagle 算法。
TCP 的累计确认机制,是累计确认和延迟确认两个策略的综合。
此外,TCP 协议还有另外一种确认机制,叫做 选择确认机制 ,将会后面讲到。
发送机制
已经讲过,TCP 协议默认的 Nagle
算法 采用了延迟确认的方法。
对应的,发送的时机如何确定?
办法是类似的,大概是,未发送的包达到一定量、或者达到一个时间阈值,才发送一次。
TCP 的发送机制,是累计发送和延迟发送两个策略的综合。
同样可以设置 TCP_NODELAY
选项 来关闭延迟发送的行为。
发送窗口
发送窗口的示意图如下,当收到对方的累计确认后,则向右滑动。
注意的是 窗口大小 是有限的(稍后将讨论它的受限情况), 发送方只能发送窗口内的数据包 。
接收窗口
接收窗口的示意图如下,当回复对方确认后,则向右滑动。
仍需注意 窗口大小 是有限的(稍后将讨论它的受限情况), 接收方只能接收窗口内的数据包 。
由于 网络数据包是乱序的 , 所以接收后的数据包会按照序号重新排序,才可以交付给应用程序。
窗口大小
窗口的大小是受限的,也是动态的 。
窗口大小是 TCP 协议头
中的一个 16 比特大小的整数字段。
首先,显然 接收窗口受限于接收缓存区的大小, 发送窗口受限于发送缓存区的大小 。
其次指出, 发送窗口大小也受限于接收窗口大小 。
这其实比较容易理解,发送方是生产者,接收方是消费者, 如果生产速度快而消费速度慢,则会导致接收方来不及接收。
所以,发送方只能发送窗口内的数据包 。
其实,这就是 流量控制机制 : 由接收方控制的、调节发送方生产速度的机制 , 其具体的实现方式,就是在回复时设置 TCP 协议头
中的窗口大小字段。
窗口的大小也是动态变化的, 因为两端接收和发送能力是动态变化的 :
- 接收能力的变化导致窗口大小的变化,即后面所讲的 TCP 流量控制机制。
- 发送能力的变化导致窗口大小的变化,即后面所讲的 TCP 拥塞控制机制。
可以看到,滑动窗口的大小是一个贯穿式的重要概念。
但是,如果窗口要缩小,窗口的前沿是不可以向前收缩的。
窗口收缩的方法则是,慢慢地,随着已发送的数据包得到确认, 保持窗口前沿不动,前移窗口后沿。
因此, 发送窗口也并不总和接收窗口一样大 。
窗口大小的初始化,是在 连接建立 过程中两端协商确定的, 在后续的传输阶段,它会因主动或被动的原因而动态变化。
5.TCP的建立和释放
1.建立连接-三次握手
首先,TCP 连接建立过程要解决的问题:
- 确定双方都可以正常收发 TCP 数据包。
- 双方协商一些参数和可选项(例如前面所讲的 选择确认 、窗口大小 等), 尤其是交换初始序号。
- 对资源进行分配,比如缓存区、端口号。
下面是 TCP 协议三次握手建立连接的过程示意图,其中
- SYN(synchronous) 是同步标志位 ,表示开始传输数据的意思。
- ACK(acknowledgement) 是确认标志位,表示是否启用确认机制。
- ack(小写)是前面所说的 确认号。
- seq 是前面所说的 序号 。
具体过程的描述如下:
- 客户端发送一个 TCP 数据包,主动发起连接。
设置 SYN=1 标志位,表示发起连接,想要传输数据。
客户端初始化自己的数据包序号 seq=x ,一同发送给对方。
- 服务端收到对方的连接请求,回复对方确认包。
设置回复标志位 ACK=1 ,表示确认。
根据 确认号的规则, 同时设 ack=x+1 表示期待对方下一次发过来的数据包序号为 x+1。
因为 TCP 是全双工的,服务端也会设置 SYN=1 , 并初始化自己的数据包序号 seq=y ,捎带回复给客户端。
此时,服务端可以确定: 客户端可以发送 TCP 数据包,自身可以接收 TCP 数据包 。
- 客户端收到对方的确认包,并回复对方确认。
同样,设置回复标志位 ACK=1,表示确认。
对方的序号是 y , 回复对方 ack=y+1 表示期待对方下一次发来的数据包序号为 y+1 。
同时,自身的数据包序号需要自增 seq=x+1 。
自己发送上一个数据包,对方收到了,并且收到了对方回复。
所以客户端可以确定: 服务端可以正常收发 TCP 数据包, 自身也可以正常收发 TCP 数据包 。
- 服务端收到客户端的确认,三次握手结束。
此时服务端可以进一步确定:
客户端端可以正常接收 TCP 数据包, 自身也可以正常发送 TCP 数据包 。
三次握手结束后,双方都可以确定对方是可以正常收发 TCP 数据的。
三次握手的必要性
一个经常出现的问题是: 两次握手可以吗?
当然是不可以的,可以分几个方面解释:
1.三次握手是为了确定双方的收发能力
如果缺少最后一次 ACK 的话,服务端就无法知道客户端是否可以接收 TCP 数据, 也无法知道自己的发送是否成功,就无法做到可靠传输。
2.序号同步的确定性
如果缺少最后一次 ACK 的话,服务端就无法确定对方有无收到自己的初始序号。
前面讲到,数据包的序号是 TCP 滑动窗口机制的基本字段,它如果是不可靠的, 后续的确认机制、重传机制等都无从谈起。
网络是不可靠的,存在丢包,所以连接建立时, 双方都必须确定初始序号交到了对方手中。
三次握手过程,也是可靠地交换初始序号的过程 。
3.历史失效连接请求的乱序问题
网络中,数据传输可能存在延迟、乱序。
如果一个老的失效的连接请求,延迟到达服务端,倘若缺少第三次握手, 服务端会建立一条新连接,直接进入 ESTABLISHED 状态。
客户端收到服务端的回复包时,可以根据序号是否过期判断是否是失效连接。
在三次握手的情况下,客户端就进一步可以发送 RST 标志,终止连接。
如果不是历史失效连接,客户端则发送 ACK 标志,正常建立连接。
4.三次握手中丢包的情况
另一个常见的问题是: 如果第三次 ACK 丢包呢?
无论哪一次通信发生丢包或延迟,都出发 TCP 的 重传机制 。
简单说,再发一次,如果达到一定次数,则放弃连接。
对于第三次 ACK 丢包的情况,重试一定次数后,服务端会关闭连接,回收资源。
不过,服务端单方面关闭连接,客户端并不知晓,它认为连接已经建立。
如果客户端向服务端发送数据,会被服务端打回 RST 报文, 借此客户端可知道连接失效。
5.数据传输的开始时机
上面的 三次握手图示 中,一个细节是:
- 客户端在两次握手之后就进入了 ESTABLISHED 状态。
- 服务端则是在三次握手之后才进入 ESTABLISHED 状态。
其原因在于, 在 第二次握手 后, 客户端已经可以确定双方都可以正常收发数据包 。
所以,客户端可以提前进入 ESTABLISHED 状态,分配端口,开始通信。
因此, 第三次 ACK 是可以捎带客户端的数据一并发送的 。
服务端直到 第三次握手 成功之后,才可以确定双方都正常, 所以它会稍晚进入 ESTABLISHED。
6.连接关闭 - 四次挥手
TCP 的连接释放过程,需要注意两个点:
- TCP 是全双工的。
- 双方都可以主动释放连接。
释放连接的过程有一个重要的目的:
双方都要确定地知道对方不再发送数据了,连接才正式关闭 。
假设客户端主动关闭,下面是 TCP 协议四次挥手释放连接的过程示意图。
其中 FIN(finish)是结束数据传输的标志位。
具体过程的描述如下:
- 客户端发送 FIN 数据包,不再发送数据。
设置 FIN=1 标志位,表示想要关闭连接,不再发送数据。
但是,此时服务端还可以继续发送数据 。
- 服务端收到 FIN 数据包,回复确认 。
服务端对于 FIN 包也会进行确认。
此时服务端明确知道,客户端不再发送数据了 。
- 服务端传输完剩余数据 。
由于 TCP 是全双工的, 连接关闭由客户端主动发起,并不意味着服务端的数据已传输完。
- 服务端需要把剩余数据传输完毕。
此时服务端知道,双方都不再发送数据了。 。
服务端发送 FIN 数据包,不再发送数据包 。
同样,设置 FIN=1 标志位,表示服务端不再发送数据。
- 客户端收到对方 FIN 包后,回复确认 。
任何一方结束数据发送,都要确保对方是知道的,就需要对方确认。
此时客户端知道,双方都不再发送数据了。
- 客户端等待 2MSL 时间等待后,四次挥手结束,释放所有资源
再次明确, FIN 标志位的意思是,不再发送数据 。
7.挥手四次的必要性
双方都必须对 FIN 包做确认, 这样对方才可以确定地知道自己不再发送数据。
TCP 连接的建立需要三次握手,是因为, 第二次握手过程服务端将设置 SYN 和 ACK 合并为一个数据包发送,所以是三次 。
而对于释放连接的情况, 无法将第二次和第三次合并, 因为中间还要传输数据, 所以是四次。
如果任何一个确认包发生丢包或延迟,主动设置 FIN 的一方会触发重传。
8.等待 2MSL 时间的原因
首先,MSL 是指报文在网络中最大生存时间。
2MSL 就是两倍的 MSL ,也被叫做 TIME_WAIT 时间, 在 linux 中常被设置为 2*60s 。
2MSL 的值一定需要大于重传超时阈值 。
为何要等待这么长时间呢?
- 如果最后一次 ACK 丢包呢?
前面有说过,任何一方的 FIN 包丢失后,如果超时未收到对方确认, 就会触发重传。
如果客户端在一定时间内未收到服务端的 FIN 包重传,说明对方已经收到 ACK 。
否则,如果收到服务端的 FIN 包重传,自然回复 ACK。
一次 ACK 包的发送时间,再算上重传 FIN 的时间,所以叫做 2MSL 。
不过,无论如何,2MSL 时间至少要比重传超时阈值长。
2. 防止新老连接数据包混乱
如果立即释放端口资源,此端口可能被一个新连接立即使用。
如此的话,假设网络中存在老数据包的延迟,那么老数据包会被新连接接收,造成混乱。
所以, 等待一定时间,使得网络中潜在的、可能延迟传输的数据包悉数殆尽, 才不会影响到后面的连接。
综合来看,还是因为网络是不可靠的。