文章目录
- 基于UDP的可靠传输协议KCP/QUIC
- 1 KCP基本原理
- 1.1 如何做到可靠传输
- 1.2 TCP和UDP如何选择
- 1.3 ARQ协议
- 1.3.1 停止等待ARQ
- 1.3.2 回退nARQ
- 1.3.3 选择重传ARQ
- 1.4 RTT和RTO
- 1.5 流量控制——滑动窗口
- 1.6 拥塞控制
- 1.7 KCP协议的优势
- 1.7.1 RTO翻倍 vs 不翻倍
- 1.7.2 选择重传 vs 全部重传
- 1.7.3 快速重传
- 1.7.4 延迟ACK vs 非延迟ACK
- 1.7.5 UNA vs ACK + UNA
- 1.7.6 非退让流控
- 2. KCP源码分析和使用
- 2.1 名词说明
- 2.2 kcp协议头
- 2.2.1 ikcp中的主要数据结构
- 2.2 KCP的使用方式
- 2.2.1 生成会话ID
- 2.2.2 流程
- 2.2.3 KCP配置模式
- 3. 重点问题
- 4. QUIC简介
- 4.1 QUIC 的核心特点
- 学习参考
基于UDP的可靠传输协议KCP/QUIC
本文详细介绍了KCP协议基本原理,并简要介绍了KCP的使用方式以及QUIC协议。
1 KCP基本原理
1.1 如何做到可靠传输
可靠传输最主要是依赖于ARQ协议,即自动重复请求协议,它的基本功能是在丢包时请求重传,它的如下机制保证了可靠的传输:
-
ACK确认机制,确保数据已被接收。
-
重传机制,当超时或者判断丢包时重传数据。
-
序号机制,检测是否有数据丢失和是否有序。
-
重排机制,使乱序到达的数据重新有序。
1.2 TCP和UDP如何选择
以下是一个总结UDP和TCP之间主要区别的表格:
特性 | TCP | UDP |
---|---|---|
连接方式 | 面向连接(建立连接) | 无连接(无需建立连接) |
传输方式 | 面向字节流 | 面向报文 |
可靠性 | 提供可靠的数据传输(数据包顺序、重传机制) | 不保证可靠性(可能丢失数据包) |
数据顺序 | 确保数据包按顺序到达 | 不保证数据包顺序 |
流控制 | 提供流控制(使用滑动窗口协议) | 不提供流控制 |
拥塞控制 | 提供拥塞控制机制 | 不提供拥塞控制 |
适用场景 | 适合需要可靠传输的应用(如网页、文件传输) | 适合实时应用(如视频、语音);游戏行业 |
传输速率 | 相对较慢(由于连接管理、错误校正) | 相对较快(无连接管理、简单) |
应用协议 | HTTP, FTP, SMTP 等 | DNS, DHCP, VoIP 等 |
字节流: 连续、有序。
报文:离散,无序。
1.3 ARQ协议
ARQ(Automatic Repeat reQuest),即自动重复请求,是一种确保可靠传输的机制。可以参考这篇博文详细了解。
1.3.1 停止等待ARQ
很少被采用。
1.3.2 回退nARQ
TCP采用这种重传机制。ARQ
1.3.3 选择重传ARQ
KCP采用这种方式。
1.4 RTT和RTO
RTO(Retransmission TimeOut)即重传超时时间。
RTT(Round-Trip Time)即往返时延。
1.5 流量控制——滑动窗口
为什么要进行流量控制:发送方的发送速率与接收方的接受速率存在差异。
- 如果发送方速率大于接收方速率,接收方就不得不丢弃很多数据包,导致传输效率下降。
- 如果发送方速率小于接收方速率,会浪费带宽。
怎么进行流量控制:滑动窗口
接收方会告诉发送方自己的接收窗口的大小,还能够接收多少数据,这样发送方知道能够发送多少数据。
其他小问题总结:
- 接收窗口大小固定吗?不固定,需要根据网络情况动态调整。
- 接收窗口越大越好吗?不是,接收窗口过大容易导致乱序和丢包。
- 发送窗口和接收窗口相等吗?一般接收窗口>=发送窗口。
1.6 拥塞控制
主要由四个算法组成:
- 慢启动
- 拥塞避免
- 快速恢复 (TCP Reno版本开始使用)
- 快速重传
简要了解可以参考我的博文,详细了解可以参考这篇博文。
1.7 KCP协议的优势
在不稳定的网络中,KCP以10%-20%带宽浪费的代价,换取比TCP快30%-40%的传输速度
在网络通畅的情况下,文件传输速度上,kcp < tcp。
KCP的传输速度优势本质上来自于其实现的ARQ协议的重传策略。它通过以下机制实现了这样的目标:
- 尽量减少重传超时等待的时间(即RTO)。
- 尽量减少丢包的成本(得益于选择重传ARQ)。
尽管实现这样的目标是以消耗的网络带宽增加为代价的。
1.7.1 RTO翻倍 vs 不翻倍
TCP的超时时长计算策略是翻倍,而KCP启动快速模式后是乘以1.5,提高了传输速度。
KCP还可以定制重传策略、定制丢包策略。
1.7.2 选择重传 vs 全部重传
KCP使用选择拒绝自动重复请求,只重传那些已经丢失的包。
KCP使用快速重传,KCP的快速重传是指当发现某个数据段被跳过确认多次后,不必等待RTO而直接重传,大大改善了丢包时的重传速度。
1.7.3 快速重传
发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。
1.7.4 延迟ACK vs 非延迟ACK
TCP为了充分利用带宽,选择延迟发送ACK,这样超时计算会算出较大的RTT时间,从而延长了RTO时间。而KCP的ACK是否延迟可以调节。
在TCP中,一个ACK可以确认多个数据段,因此采用延迟ACK可以减少网络包数量。
1.7.5 UNA vs ACK + UNA
ARQ的响应模式有两种,UNA(Unacknowledged Number Acknowledged)和ACK,UNA模式下确认序号指的是下一个期待收到的序号,ACK模式下确认序号指的是已经收到的数据段的序号。只用UNA会导致太多重传,只用ACK会导致丢失成本太高(ACK包丢失可能会导致不必要的重传)。在KCP协议中,除去单独的ACK包外,所有的包都有UNA信息。
1.7.6 非退让流控
KCP正常模式下通TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端窗口大小、丢包退让、拥塞窗口这四个因素决定。当采用非退让流控时,就不考虑后两个因素。
2. KCP源码分析和使用
2.1 名词说明
MTU: 最大传输单元,是数据链路层的概念,以太网为1500字节。实际在传输层考虑到PDU的消耗,使用1400字节比较合适。
cwnd:拥塞窗口大小
rwnd:接收方窗口大小
snd_queue:待发送KCP数据包的队列
snd_buf:发送缓冲区
snd_nxt:下一个即将发送的kcp数据包序列号
snd_una:下一个待确认的序列号
2.2 kcp协议头
0 4 6 8 (BYTE)
+----------------------------+---------------+-----------+
| conv | cmd | frg | wnd | 8
+----------------------------+---------------+-----------+
| ts | sn | 16
+----------------------------+---------------------------+
| una | len | 24
+----------------------------+---------------------------+
| DATA (optional) |
+--------------------------------------------------------+
- conv: 会话标识
- cmd: 命令,如IKCP_CMD_ACK
- frg: 分片标识
- wnd: 接收窗口大小
- ts: 时间序列
- sn: 序列号
- una: 下一个期待的数据序列号
- len: 数据长度
- data: 数据
2.2.1 ikcp中的主要数据结构
- ikcp控制块,类似与tcp中的tcp控制块,保存每个kcp会话的核心数据。
struct IKCPCB
{
/* 会话状态信息 */
IUINT32 conv; // 标识会话
IUINT32 mtu; // 最大传输单元,默认数据为1400,最小为50
IUINT32 mss; // 最大分片大小,不大于mtu
IUINT32 state; // 连接状态(0xffffffff表示断开连接)
int nocwnd; // 取消拥塞控制
int stream; // 是否采用流传输模式
int logmask; // 日志的类型,如IKCP_LOG_IN_DATA,方便调试
/* 用于ARQ的字段 */
IUINT32 snd_una; // 第一个未确认的包
IUINT32 snd_nxt; // 下一个待分配包的序号
IUINT32 rcv_nxt; // 待接收消息序号
IUINT32 ts_recent; // 最近收到的数据的时间
IUINT32 ts_lastack; // 上一个收到的ACK的时间
IINT32 rx_rttval; // RTT的变化量,代表连接的抖动情况
IINT32 rx_srtt; // smoothed round trip time,平滑后的RTT;
IINT32 rx_rto; // 收ACK接收延迟计算出来的重传超时时间
IINT32 rx_minrto; // 最小重传超时时间
IUINT32 *acklist; //待发送的ack的列表。当收到一个数据报文时,将其对应的ACK报文的 sn 号以及时间戳 ts 同时加入到acklist 中,即形成如 [sn1, ts1, sn2, ts2 …] 的列表
/* 滑动窗口 */
struct IQUEUEHEAD snd_queue; //发送消息的队列
struct IQUEUEHEAD rcv_queue; //接收消息的队列, 是已经确认可以供用户读取的数据
struct IQUEUEHEAD snd_buf; //发送消息的缓存 和snd_queue有什么区别
struct IQUEUEHEAD rcv_buf; //接收消息的缓存, 还不能直接供用户读取的数据
/* 拥塞控制 */
IUINT32 ssthresh; // 拥塞窗口的阈值
IUINT32 cwnd; // 拥塞窗口大小, 动态变化
...;
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);//发送消息的回调函数
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); // 写日志的回调函数
}
- ikcp每个分片的数据结构
struct IKCPSEG
{
struct IQUEUEHEAD node;
IUINT32 conv; // 会话编号,和TCP的con一样,确保双方需保证conv相同,相互的数据包才能被接收.conv唯一标识一个会话
IUINT32 cmd; // 区分不同的分片.IKCP_CMD_PUSH数据分片;IKCP_CMD_ACK:ack分片;IKCP_CMD_WASK:请求告知窗口大小;IKCP_CMD_WINS:告知窗口大小
IUINT32 frg; // 标识segment分片ID,用户数据可能被分成多个kcp包发送, 为0时代表
IUINT32 wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小),发送方的发送窗口不能超过接收方给出的数值
IUINT32 ts; // 发送时刻的时间戳
IUINT32 sn; // 分片segment的序号,按1累加递增
IUINT32 una; // 待接收消息序号(接收滑动窗口左侧).对于未丢包的网络来说,una是下一个可接收的序号,如收到sn=10的包,una为11
IUINT32 len; // 数据长度
IUINT32 resendts; // 下次超时重传时间戳
IUINT32 rto; //该分片的超时等待时间,其计算方法同TCP
IUINT32 fastack; // 收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传
IUINT32 xmit; // 发送分片的次数,每发一次加1.发送的次数对RTO的计算有影响,但是比TCP来说,影响会小一些.
char data[1];
};
2.2 KCP的使用方式
2.2.1 生成会话ID
会话ID用来标识客户端与服务器端的一条逻辑连接。
两种方式:
- 客户端使用随机数产生uuid。
- 服务器端产生唯一id然后通过http协议等传给客户端。
ikcp中的实现:
// read conv 获取会话id
IUINT32 ikcp_getconv(const void *ptr)
{
IUINT32 conv;
ikcp_decode32u((const char*)ptr, &conv);
return conv;
}
/* decode 32 bits unsigned int (lsb) */
static inline const char *ikcp_decode32u(const char *p, IUINT32 *l)
{
#if IWORDS_BIG_ENDIAN || IWORDS_MUST_ALIGN
*l = *(const unsigned char*)(p + 3);
*l = *(const unsigned char*)(p + 2) + (*l << 8);
*l = *(const unsigned char*)(p + 1) + (*l << 8);
*l = *(const unsigned char*)(p + 0) + (*l << 8);
#else
memcpy(l, p, 4);
#endif
p += 4;
return p;
}
每个会话都对应一个kcp控制块,会话在构造时也会设置ikcpcb中的output回调函数,这样只需要封装号session类,用户就只需要与session打交道了。
2.2.2 流程
- 创建KCP对象
ikcpcb *kcp = ikcp_create(conv, user);
- 设置发送回调函数(如UDP的send函数)
kcp->output = udp_output;
- 循环调用update
ikcp_update(kcp, millisec); // 在一个线程、定时器5ms/10ms做调度
- 输入一个应用层数据包(如UDP收到的数据包)
ikcp_input(kcp, received_udp_packet, received_udp, size);
- 发送数据
ikcp_send(kcp1, buffer, 8);
- 接收数据
hr = ikcp_recv(kcp2, buffer, 10);
需要注意,接收数据时需要用户先用UDP socket的API读取出数据,然后调用ikcp_input(),然后再调用ikcp_recv()。
2.2.3 KCP配置模式
工作模式:
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
- nodelay: 是否开启ACK延迟确认,0不启用,1启用
- interval: 协议内部工作工作的interval,单位ms,默认10ms
- resend:是否开启快速重传模式,默认不开启
- nc:是否关闭流控,默认不关闭
最大窗口:
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd); // 默认32
最大传输单元:
int ikcp_setmtu(ikcpcb *kcp, int mtu); // 默认1400
最小RTO:
kcp->rx_minrto = 10; // 快速模式下为30ms,可以手动更改
3. 重点问题
- tcp为什么可靠?
tcp的可靠性来自于滑动窗口和ARQ协议,它们保证了数据不丢失、不乱序。
- kcp为什么牺牲带宽换取速度?
kcp适用于对实时通讯要求较高的场景,例如直播、网络游戏等领域。它基于UDP协议,省去了三次握手的过程。kcp使用了自定义重传机制(如RTO、快速重传)、自定义是否启用拥塞控制,选择决绝ARQ也降低了重传成本。因此综合来看,kcp增加了网络中数据包的数量,但是提高了数据包的实时性。在不稳定的网络环境下,kcp的优势更为明显。
- udp怎么实现客户端和服务端编程?服务端怎么维护和客户端的逻辑连接?
UDP不能像tcp那样建立连接并长时间维持上下文信息,而只能通过每个数据包来源的目的地址和端口识别对方。为了维护逻辑连接,通常服务端可以使用一个具有唯一ID的会话保存特定客户端的信息。这个会话机制包括:
- 客户端标识
- 状态管理
- 心跳机制
4. QUIC简介
QUIC(Quick UDP Internet Connections)是一种基于 UDP 的传输层协议,由 Google 开发,旨在为网络通信提供更快、更可靠的体验。QUIC 的设计目标是在保持低延迟的同时,提供与 TCP 相似的可靠性和拥塞控制,并解决一些传统 TCP 和 TLS 协议的缺点,比如慢启动、连接延迟高等问题。
4.1 QUIC 的核心特点
- 低延迟连接建立
QUIC 通过将握手和加密合并到一个过程,使客户端和服务端能够在一次往返(1-RTT)中完成握手。对于已经建立过连接的客户端,QUIC 还支持 0-RTT 握手,这意味着客户端可以在发送初始请求的同时发送数据,极大地减少了连接延迟。
- 集成的 TLS 加密
QUIC 将 TLS 1.3 协议集成在其协议栈中,从而在连接开始时即提供加密通信。这种方式不仅能提高连接安全性,还避免了传统 TCP 和 TLS 分别握手带来的延迟。
- 多路复用
传统 TCP 实现 HTTP/2 的多路复用时,存在着“队头阻塞”问题(某个流的丢包会阻塞其他流的数据传输)。QUIC 通过流的独立处理机制,使每个流都可以独立地进行数据传输,避免了队头阻塞的情况。
- 灵活的拥塞控制
QUIC 的拥塞控制算法可由实现方选择或配置,这使得它更具灵活性,可以根据网络情况灵活调整,进一步提高传输效率和稳定性。此外,QUIC 的流量控制机制可以控制流级别的数据量,以防止客户端或服务端被大量数据淹没。
- 基于 UDP
QUIC 通过 UDP 实现,不受操作系统内核中 TCP 堆栈限制的影响,便于快速更新和改进协议,特别适合现代互联网的需求。
学习参考
学习更多相关知识请参考零声 github。