中篇:TCP 为什么可靠又高效?滑动窗口、流量控制、拥塞控制与粘包全解析
TCP 既要保证可靠性,又要尽可能提高性能。
本篇围绕 TCP 的核心机制展开:滑动窗口、流量控制、拥塞控制、延迟应答、捎带应答、面向字节流与粘包问题。
一、滑动窗口:一次发送多条数据
如果每发一个数据段都要等 ACK,性能会较差,尤其是往返时间较长时。
于是 TCP 引入滑动窗口。
窗口大小指的是:无需等待确认应答而可以继续发送数据的最大值。
例如窗口大小为 4000 字节,即四个段。
发送前四个段时,不需要等待任何 ACK,直接发送。
收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。
操作系统内核为了维护滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。
只有确认应答过的数据,才能从缓冲区删掉。
窗口越大,网络的吞吐率就越高。
丢包如何处理?
分两种情况:
数据包已经抵达,ACK 被丢了
部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认。数据包直接丢了
发送方会触发超时重传;如果收到多个重复 ACK,也可能触发快速重传。
二、流量控制:别把接收端撑爆
接收端处理数据的速度是有限的。
如果发送端发得太快,接收端的缓冲区就会满,此时再发数据就会丢包。
TCP 支持根据接收端的处理能力,决定发送端的发送速度,这就是流量控制。
接收端如何把窗口大小告诉发送端?
TCP 首部中有一个 16 位窗口字段,就是存放窗口大小信息。
如果接收端缓冲区满了,就会将窗口置为 0。
这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
问题来了:16 位数字最大表示 65535,那么 TCP 窗口最大就是 65535 字节吗?
实际上,TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是窗口字段的值左移 M 位。
三、拥塞控制:别把网络压垮
虽然滑动窗口能高效可靠地发送大量数据,但如果刚开始就发送大量数据,仍然可能引发问题。
因为网络上有很多计算机,当前网络状态可能已经比较拥堵。
在不清楚网络状态的情况下贸然发送大量数据,很可能雪上加霜。
TCP 引入慢启动机制:先发少量数据,探探路,摸清当前网络拥堵状态,再决定按多大速度传输。
核心概念:拥塞窗口 cwnd。
- 发送开始时,定义拥塞窗口大小为 1。
- 每次收到一个 ACK 应答,拥塞窗口加 1。
- 每次发送数据包时,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小值作为实际发送窗口。
这种增长速度是指数级别的。“慢启动”只是指初始时慢,但增长速度非常快。
为了不增长那么快,引入慢启动阈值 ssthresh:
- 当拥塞窗口超过这个阈值时,不再按指数方式增长,而是按线性方式增长。
- TCP 开始启动时,慢启动阈值等于窗口最大值。
- 每次超时重发时,慢启动阈值会变成原来的一半,同时拥塞窗口置回 1。
少量丢包,仅触发超时重传;大量丢包,就认为网络拥塞。
当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降。
拥塞控制,归根结底是 TCP 协议想尽可能快地把数据传输给对方,但又要避免给网络造成太大压力的折中方案。
四、延迟应答与捎带应答
延迟应答
如果接收数据的主机立刻返回 ACK,这时候返回的窗口可能比较小。
例如:
- 接收端缓冲区为 1M。
- 一次收到了 500K 的数据。
- 如果立刻应答,返回的窗口就是 500K。
- 但实际上可能处理端处理速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了。
- 如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么返回的窗口大小就是 1M。
窗口越大,网络吞吐量越大,传输效率越高。
目标是在保证网络不拥塞的情况下尽量提高传输效率。
但并非所有包都可以延迟应答:
- 数量限制:每隔 N 个包就应答一次。
- 时间限制:超过最大延迟时间就应答一次。
- 一般 N 取 2,超时时间取 200ms,具体依操作系统不同而有差异。
捎带应答
在延迟应答的基础上,很多情况下客户端和服务器在应用层也是“发一收”的。
例如客户端说“How are you”,服务器回“Fine, thank you”。
这时 ACK 就可以搭顺风车,和服务器回应的数据一起回给客户端,这就是捎带应答。
五、面向字节流与粘包问题
创建一个 TCP 的 socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区。
- 调用
write时,数据会先写入发送缓冲区。 - 如果发送的字节数太长,会被拆分成多个 TCP 数据包发出。
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适时机再发送。
- 接收数据时,数据从网卡驱动程序到达内核的接收缓冲区。
- 应用程序调用
read从接收缓冲区拿数据。 - TCP 连接既有发送缓冲区,也有接收缓冲区,既可以读数据,也可以写数据,这叫全双工。
由于缓冲区的存在,TCP 程序的读和写不需要一一匹配。
写 100 字节可以一次write,也可以 100 次write每次 1 字节。
读 100 字节也可以一次read,或多次read。
粘包问题
粘包问题中的“包”,是指应用层的数据包。
在 TCP 协议头中,没有如同 UDP 一样的“报文长度”字段,但有序号字段。
站在传输层角度,TCP 是一个一个报文过来的,按序号排好序放在缓冲区中。
站在应用层角度,看到的只是一串连续的字节数据。
应用程序不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
如何避免粘包?归根结底一句话:明确两个包之间的边界。
- 对于定长的包,保证每次都按固定大小读取。
- 对于变长的包,可以在包头位置约定一个包总长度字段。
- 对于变长的包,还可以在包和包之间使用明确的分隔符。
思考:UDP 是否存在粘包问题?
对于 UDP,如果还没有上层交付数据,UDP 的报文长度仍然在。
同时,UDP 是一个一个把数据交付给应用层,有很明确的数据边界。
使用 UDP 时,要么收到完整的 UDP 报文,要么不收,不会出现“半个”的情况。
六、异常情况与 TCP/UDP 对比
异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没什么区别。
- 机器重启:和进程终止情况相同。
- 机器掉电 / 网线断开:接收端认为连接还在。一旦接收端有写入操作,发现连接已经不在了,就会进行 reset。即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
- 应用层协议也可能有检测机制,如 HTTP 长连接定期检测,QQ 断线后定期重连。
TCP 与 UDP 对比
TCP 用于可靠传输的情况,如文件传输、重要状态更新。
UDP 用于对高速传输和实时性要求较高的通信领域,如早期 QQ、视频传输,UDP 还可用于广播。
TCP 和 UDP 都是程序员的工具,具体怎么用,要根据需求场景判定。
用 UDP 实现可靠传输(经典面试题)
参考 TCP 的可靠性机制,在应用层实现类似逻辑:
- 引入序列号,保证数据顺序。
- 引入确认应答,确保对端收到了数据。
- 引入超时重传,如果隔一段时间没有应答,就重发数据。
- 还可以加入滑动窗口、流量控制、拥塞控制等思想。
中篇小结
本篇重点:
- 滑动窗口:提高吞吐量
- 流量控制:保护接收端
- 拥塞控制:保护网络
- 延迟应答、捎带应答:提高效率
- 面向字节流与粘包:明确应用层边界
- 异常情况与 TCP/UDP 对比
- UDP 实现可靠传输的思路
下一篇将下探到网络层、数据链路层和应用层,重点讲 IP、NAT、ARP、DNS 等关键协议。