名词解释

ARQ(Automatic Repeat reQuest,自动重传请求)

ARQ 是一种用于数据通信中的错误控制方法,旨在保证数据传输的可靠性。ARQ 协议通过在发送端和接收端之间引入确认机制来检测并纠正传输过程中出现的数据丢失或损坏问题。

  • 停等式 ARQ(Stop-and-Wait ARQ):最简单的形式,每发送一个数据包后就停止并等待确认。只有在收到 ACK 之后才会发送下一个数据包。这种方法效率较低,因为发送方在等待确认时处于空闲状态。
  • 回退 N 步 ARQ(Go-Back-N ARQ):允许发送方连续发送多个数据包而不需要立即等待每个数据包的确认。如果某个数据包未被正确接收,发送方需要重传从那个数据包开始到当前的所有后续数据包。
  • 选择性重传 ARQ(Selective Repeat ARQ):类似于回退 N 步 ARQ,但更高效。它只需要重传那些确实丢失或损坏的数据包,而不是从出错点开始的所有后续数据包。这减少了不必要的重传,提高了效率。

TCP

三次握手与四次挥手

滑动窗口确认机制

拥塞控制算法

  • 慢启动(Slow Start):在连接开始时或网络出现丢包后,发送方以指数增长的方式增加拥塞窗口大小。
  • 拥塞避免(Congestion Avoidance):当拥塞窗口达到某个阈值后,采用线性增长方式缓慢增加拥塞窗口大小。
  • 快速重传(Fast Retransmit):一旦接收方检测到丢失了一个分组,它会立即发送多次重复的 ACK 给发送方,促使发送方在未等到超时的情况下就进行重传。
  • 快速恢复(Fast Recovery):与快速重传配合使用,在检测到丢包时不立即进入慢启动状态,而是尝试通过调整拥塞窗口和阈值来快速恢复正常的数据传输。

Reno 和 Tahoe

这些是 TCP 拥塞控制机制的具体实现版本,其中 Reno 对快速重传和快速恢复机制进行了改进。

New Reno

进一步改进了 Reno,使得在多个分组丢失的情况下能更有效地恢复。

CUBIC

一种广泛使用的 TCP 拥塞控制算法,旨在提供更好的带宽利用率,尤其是在高带宽 - 延迟产品网络环境中。

BBR (Bottleneck Bandwidth and RTT)

由 Google 提出的一种新型拥塞控制算法,它试图找到并利用瓶颈带宽,从而最大化吞吐量,减少排队延迟。

KCP

KCP 是一个快速可靠协议,能以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如 UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback 的方式提供给 KCP。连时钟都需要外部传递进来,内部不会有任何一次系统调用。

整个协议只有 ikcp.h, ikcp.c 两个源文件,可以方便的集成到用户自己的协议栈中。也许你实现了一个 P2P,或者某个基于 UDP 的协议,而缺乏一套完善的 ARQ 可靠协议实现,那么简单的拷贝这两个文件到现有项目中,稍微编写两行代码,即可使用。

skywind3000/kcp

主要是因为原神用了这个协议,笔者知道了所以记录一下。

KCP 虽然说是协议,但更像是某种拥塞控制算法 + ARQ + 必要的胶水代码,因此也可以简单的视作某种拥塞控制算法。由于当前(2025/03/25)TCP 协议是各种意义上的传输层标准,所谓的拥塞控制算法都是要考虑 TCP 的,因此只能把 KCP 放在这个小节了。

KCP 采取了激进的重传策略,然而,所谓的激进是基于这样一个客观现实——绝大部分系统仍在使用 TCP 的 CUBIC 算法。

CUBIC 相对保守(在乎传输公平性,君子协议),而 KCP 舍弃了传输公平性则相当于侵犯了这部分公共空间。假如大家都使用 KCP,那么大家都不会变快,只有在大部分人用 CUBIC 的时候,使用 KCP 的人才能得到收益。

KCP 协议除了游戏也多用在翻墙上,所以 wall 也是会对这个流量进行管控的。
KCP 现状

HTTP/2 协议

Http/3(QUIC)协议

Tencent/tquic

gRPC 与 thrift 协议

面经问答:

假设在无线网条件下,网络比较拥堵,你会选择 TCP 还是 UDP 通信

没什么标准答案,主要还是看应用。说到底不重要的消息丢了也就丢了,而重要的消息肯定不能丢,哪一个跟网络环境都没关系,总不能网络环境变差了,以前不重要的消息现在就变重要了,或者相反。

epoll 相关

有意思的问题与回答:

… … 读 redis 的话,即使 redis 和请求程序同一台机器上,也需要通过读写套接字来完成,这个流程包括

1.首先会涉及操作系统内核协议栈的处理,就 linux 来说,如果 redis 服务在本机,写套接字这个过程会在到达协议栈传输层时,发现目的 ip 来自本机,然后返回。redis 读取请求,通过套接字把结果返回。

2.整个过程过程会有四次用户态和内核态的交互,其中两次还包括结果数据拷贝。

3.Redis 底层通过 epoll 函数来监测是否有请求到来,如果不想单核 CPU 占用率到 100%,那么 epoll 的 idle time 这个参数最低只能配置成 1ms,这意味着响应的平均耗时应该在 500us。

批判另一位答主测试程序的问题.jpg

如果把这个测试 demo 修改下,每隔 10ms 读写一次(显然这种方式更贴近业务场景),那耗时就会暴增,理由上面说了,Redis 底层用 epoll 来监测套接字请求,同一个套接字短时间内发起大量读写,Redis 的 epoll 函数会批量读取请求然后去处理(设置的触发方式肯定是水平触发,意味着只需要和内核态交互一次就可以读取一个 fd 的多个 Req),导致平均耗时看起来不高,可一旦进入业务场景,零散读写 Redis,耗时就会大幅增加。

游戏服务器缓存为什么一般不直接 Redis,而是自己写代码写入计算机内存中呢?