Linux C/C++ 学习日记(27):KCP协议(三):源码分析与使用示例

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

源码见:Linux C/C++ 学习日记(26):KCP协议(二):kcp源码分享-CSDN博客

一、关键结构体

1.struct IKCPSEG(KCP 数据分片,传输的基本单元)

关键参数(并非全部)

成员变量 含义说明 修改该成员的函数及场景
conv 会话编号(唯一标识一个 KCP 连接,类似 TCP 的会话标识) - ikcp_send:创建分片时赋值为kcp->conv; - ikcp_parse_data:解析接收的分片时从数据包中提取。
cmd 分片类型(IKCP_CMD_PUSH/ACK/WASK/WINS - ikcp_flush: 发送数据时设为IKCP_CMD_PUSH,发送 ACK 时设为IKCP_CMD_ACK,发送探测时设为WASK/WINS; - ikcp_parse_data:解析接收的分片时从数据包中提取。
frg 分片编号(消息分片时,从count-1递减到 0,0 表示最后一个分片) - ikcp_send:分片时根据总片数分配(消息模式下);流模式下固定为 0。
wnd 发送方当前的接收窗口空闲大小(告知对方自己的接收能力) - ikcp_flush:发送分片时设为ikcp_wnd_unused(kcp)的返回值(rcv_wnd - nrcv_que)。
ts 分片发送时的时间戳(用于对方计算 RTT) - ikcp_flush:首次发送或重传时,设为当前时间戳kcp->current
sn 分片序号(全局递增,唯一标识一个分片) - ikcp_flush:将分片从snd_queue移到snd_buf时,分配为kcp->snd_nxt并递增snd_nxt
una 发送方已确认的最大序号(告知对方 "我已收到una之前的所有分片") - ikcp_flush:发送分片时设为当前kcp->rcv_nxt(接收方下一个期望的序号)。
len 分片的数据部分长度 - ikcp_send:分片时根据 MSS 设置(不超过kcp->mss);- ikcp_parse_data:解析接收的分片时从数据包中提取。
resendts 下次重传的时间戳(超时重传的触发时间) - ikcp_flush:首次发送时设为current + rto + rtomin;重传时更新为current + rto
rto 该分片的重传超时时间(动态调整) - ikcp_flush:首次发送时设为kcp->rx_rto;重传时根据nodelay模式调整(正常模式累加 RTO,无延迟模式累加 RTO/2)。
fastack 被其他 ACK 跳过的次数(用于触发快速重传) - ikcp_parse_fastack:当收到序号更大的 ACK 时,对中间未确认的分片递增此值。
xmit 该分片的发送次数(包括首次和重传) - ikcp_flush:首次发送或重传时递增(每次发送 + 1)。

2.struct IKCPCB(KCP 控制块,管理会话状态)

关键参数(并非全部)

成员变量 含义说明 修改该成员的函数及场景
conv 会话编号(唯一标识当前 KCP 连接) - ikcp_create:初始化时赋值,后续不变。
mtu 最大传输单元(单个 UDP 包的最大大小,默认 1400 字节) - ikcp_create:初始化为IKCP_MTU_DEF; - ikcp_setmtu:用户调用时修改(需≥50 字节且≥头部开销)。
mss 最大分片大小(mtu - IKCP_OVERHEAD,默认 1376 字节) - ikcp_create:初始化为mtu - IKCP_OVERHEAD; - ikcp_setmtu:修改mtu后重新计算。
state 连接状态(0 = 正常,-1= 链路断开) - ikcp_flush:当分片重传次数xmit ≥ dead_link时,设为-1
snd_una 发送方第一个未确认的序号(发送窗口左边界) - ikcp_shrink_buf:发送缓存非空时设为队头分片的sn,否则设为snd_nxt;该函数在ikcp_parse_ack/ikcp_parse_una后调用。
snd_nxt 发送方下一个待分配的序号(发送窗口右边界) - ikcp_flush:将分片从snd_queue移到snd_buf时递增(分配sn)。
rcv_nxt 接收方下一个期望的序号(接收窗口左边界) - ikcp_parse_data:当接收缓存的分片序号等于rcv_nxt时递增; - ikcp_recv:转移有序分片到接收队列时递增。
rx_srtt 平滑 RTT(往返时间) - ikcp_update_ack:根据新测量的 RTT 更新(首次设为 RTT,后续加权平滑)。
rx_rttval RTT 偏差(反映网络抖动) - ikcp_update_ack:根据 RTT 与平滑 RTT 的差值更新(加权平滑)。
rx_rto 重传超时时间(基于rx_srttrx_rttval计算) - ikcp_update_ack:计算为rx_srtt + 4*rx_rttval,并限制在[rx_minrto, IKCP_RTO_MAX]
rx_minrto 最小 RTO(避免过小的 RTO 导致冗余重传) - ikcp_create:初始化为IKCP_RTO_MIN; - ikcp_nodelaynodelay=1时设为IKCP_RTO_NDL(30ms)。
snd_wnd 发送窗口大小(本地允许的未确认分片最大数量) - ikcp_create:初始化为IKCP_WND_SND; - ikcp_wndsize:用户调用时修改。
rcv_wnd 接收窗口大小(本地可缓存的乱序分片最大数量) - ikcp_create:初始化为IKCP_WND_RCV; - ikcp_wndsize:用户调用时修改(不小于默认值)。
rmt_wnd 远端告知的接收窗口大小(对方的接收能力) - ikcp_input:解析任何分片时,从wnd字段提取并更新。
cwnd 拥塞窗口大小(动态调整,限制实际发送速率) - ikcp_input:收到新确认时,按慢启动 / 拥塞避免策略增长; - ikcp_flush:发生快速重传或超时重传时收缩。
probe 窗口探测标记(IKCP_ASK_SEND/IKCP_ASK_TELL - ikcp_recv:接收窗口有空闲时设IKCP_ASK_TELL; - ikcp_input:收到WASK时设IKCP_ASK_TELL; - ikcp_flush:处理后清零。
interval 状态刷新间隔(ikcp_update的调用周期,默认 100ms) - ikcp_create:初始化为IKCP_INTERVAL; - ikcp_interval/ikcp_nodelay:用户调用时修改(10~5000ms)。
ts_flush 下一次调用ikcp_flush的时间戳 - ikcp_update:首次调用时初始化,之后按interval递增(超时则校正)。
nrcv_buf 接收缓存(rcv_buf)中的分片数量 - ikcp_parse_data:添加新分片时递增,移除有序分片时递减。
nsnd_buf 发送缓存(snd_buf)中的分片数量 - ikcp_flush:从snd_queue移到snd_buf时递增; - ikcp_parse_ack/ikcp_parse_una:删除分片时递减。
nrcv_que 接收队列(rcv_queue)中的分片数量(可交付给用户) - ikcp_parse_data:转移有序分片时递增; - ikcp_recv:读取数据时递减。
nsnd_que 待发送队列(snd_queue)中的分片数量(未进入发送窗口) - ikcp_send:添加分片时递增; - ikcp_flush:移到snd_buf时递减。
nodelay 无延迟模式开关(0 = 关闭,1 = 开启) - ikcp_create:初始化为 0; - ikcp_nodelay:用户调用时修改。
ackcount ACK 列表中待发送的 ACK 数量 - ikcp_ack_push:添加 ACK 时递增; - ikcp_flush:发送后清零。
fastresend 快速重传触发阈值(被跳过的 ACK 数量) - ikcp_create:初始化为 0; - ikcp_nodelay:用户调用时修改(如设为 2 则被跳过 2 次时重传)。
nocwnd 拥塞控制开关(0 = 启用,1 = 禁用) - ikcp_create:初始化为 0; - ikcp_nodelay:用户调用时修改。

3.struct IQUEUEHEAD

成员变量 含义说明 修改该成员的函数及场景
next 指向链表中的下一个节点(双向链表的后向指针) - iqueue_init:初始化队列时,next指向自身(空队列); - iqueue_add/iqueue_add_tail:插入节点时,调整当前节点与前后节点的next指针(如将新节点的next指向原后继节点,原前驱节点的next指向新节点); - iqueue_del:删除节点时,将前驱节点的next指向后继节点,断开当前节点的后向连接。
prev 指向链表中的上一个节点(双向链表的前向指针) - iqueue_init:初始化队列时,prev指向自身(空队列); - iqueue_add/iqueue_add_tail:插入节点时,调整当前节点与前后节点的prev指针(如将新节点的prev指向原前驱节点,原后继节点的prev指向新节点); - iqueue_del:删除节点时,将后继节点的prev指向前驱节点,断开当前节点的前向连接。

二、函数

1. 基础工具函数(跨平台与数值运算)

函数名 功能描述 操作细节 核心作用
ikcp_encode_seg 编码 KCP 分段为字节流(用于组装发送数据包) 按顺序调用 ikcp_encode32u(编码conv)、ikcp_encode8u(编码cmd/frg)、ikcp_encode16u(编码wnd)、ikcp_encode32u(编码ts/sn/una/len)等函数,将分段的各字段编码为连续字节流,填充到输出缓冲区中 把 KCP 分段(IKCPSEG)的控制信息与数据转化为符合协议格式的字节流,为底层传输(如 UDP 发送)提供可直接发送的数据,是 "KCP 逻辑→网络数据包" 的编码核心步骤
ikcp_encode8u 编码 8 位无符号整数 直接将 8 位数据写入指针,指针后移 1 字节 确保 8 位数据跨平台传输一致性,用于协议头基础字段(如cmd/frg)读写
ikcp_decode8u 解码 8 位无符号整数 直接从指针读取 8 位数据,指针后移 1 字节 同上,解析 8 位协议头字段(如cmd/frg
ikcp_encode16u 编码 16 位无符号整数(小端序) 大端 / 需对齐系统手动拆分低 8 位→高 8 位;小端系统直接内存拷贝,指针后移 2 字节 解决 16 位数据(如wnd)大小端差异,保证协议头(如接收窗口大小)解析一致
ikcp_decode16u 解码 16 位无符号整数(小端序) 大端 / 需对齐系统手动拼接低 8 位→高 8 位;小端系统直接内存拷贝,指针后移 2 字节 同上,解析 16 位协议头字段(如接收窗口大小)
ikcp_encode32u 编码 32 位无符号整数(小端序) 大端 / 需对齐系统手动拆分 4 字节(低→高);小端系统直接内存拷贝,指针后移 4 字节 解决 32 位核心数据(如conv/sn/ts)大小端差异,为协议关键字段(会话 ID、序号、时间戳)读写提供基础
ikcp_decode32u 解码 32 位无符号整数(小端序) 大端 / 需对齐系统手动拼接 4 字节(低→高);小端系统直接内存拷贝,指针后移 4 字节 同上,解析 32 位核心协议字段(如会话 ID、序号、时间戳)
_imin_ 取两个无符号整数的最小值 直接比较返回较小值 窗口控制、拥塞控制中取边界值(如发送窗口与远端窗口的最小值)
_imax_ 取两个无符号整数的最大值 直接比较返回较大值 边界判断(如 RTT 偏差与刷新间隔的最大值)
_ibound_ 将数值限制在 [lower, upper] 范围内 先取与lower的最大值,再取与upper的最小值 确保参数合法性(如 RTO 限制在rx_minrtoIKCP_RTO_MAX之间)
_itimediff 计算两个时间戳的差值(支持 32 位无符号回绕) 强制转换为有符号整数计算later - earlier 超时判断、RTT 计算(如判断重传时间是否到达、计算往返时间)

2. 内存与分段管理函数(资源分配释放)

函数名 功能描述 操作细节 核心作用
ikcp_malloc 内部内存分配函数 优先调用用户自定义分配器(ikcp_malloc_hook),无则用系统malloc 为 KCP 所有资源(分段、缓冲区、ACK 列表)分配内存,支持自定义内存策略
ikcp_free 内部内存释放函数 优先调用用户自定义释放器(ikcp_free_hook),无则用系统free 释放ikcp_malloc分配的内存,避免内存泄漏
ikcp_allocator 设置自定义内存分配器,替换默认malloc/free 将用户提供的分配 / 释放函数指针赋值给ikcp_malloc_hook/ikcp_free_hook 支持用户接入内存池,优化高频内存操作性能(如游戏、高并发场景)
ikcp_segment_new 创建 KCP 数据分段(IKCPSEG,含协议头 + 数据缓冲区) 分配sizeof(IKCPSEG) + size内存(size为数据长度),返回分段指针 生成数据传输的基础单元,承载协议头与用户数据
ikcp_segment_delete 释放 KCP 数据分段 调用ikcp_free回收分段内存 销毁无用分段(如已确认的发送分段、重复的接收分段),释放内存资源

3. KCP 控制块管理函数(核心对象生命周期)

ikcp_create、ikcp_release、ikcp_setoutput

函数名 功能描述 操作细节 核心作用
ikcp_create 创建 KCP 控制块(ikcpcb,管理单个连接的所有状态) 1. 核心参数初始化 :设置会话 ID(conv)、用户自定义数据(user),初始化发送未确认序号(snd_una)、下一个发送序号(snd_nxt)、期望接收序号(rcv_nxt),以及窗口大小(snd_wnd/rcv_wnd)、RTT 相关参数(rx_srtt/rx_rttval/rx_rto)等; 2. 队列与缓存初始化 :创建 4 个核心双向队列(snd_queue待发送队列、rcv_queue接收队列、snd_buf发送缓存、rcv_buf接收缓存),并将队列计数(nrcv_buf/nsnd_buf/nrcv_que/nsnd_que)初始化为 0;3. 内存分配 :分配发送缓冲区(大小为 3 倍MTU + IKCP_OVERHEAD,用于临时组装多个分片为一个数据包); 4. 返回结果 :成功时返回ikcpcb指针,若内存分配失败则返回NULL 初始化 KCP 连接的核心对象,为协议运行提供基础数据结构(队列、缓冲区)与参数(窗口、RTT 等),是 KCP 会话的 "启动入口"。
ikcp_release 释放 KCP 控制块及所有关联资源 1. 队列资源清理 :遍历并清空 4 个队列(snd_queue/rcv_queue/snd_buf/rcv_buf)中的所有IKCPSEG分段,调用ikcp_segment_delete释放分段内存,并更新队列计数; 2. 缓冲区与 ACK 列表释放 :释放发送缓冲区(buffer)和 ACK 列表(acklist); 3. 控制块释放 :调用ikcp_free释放ikcpcb本身的内存。 销毁 KCP 连接,回收所有动态分配的内存资源(分段、缓冲区、控制块等),避免内存泄漏,是 KCP 会话的 "终止出口"。
ikcp_setoutput 设置数据输出回调(KCP 发送数据时调用,通常绑定 UDP 发送) 将用户提供的输出函数指针(原型为int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user))赋值给ikcpcboutput成员。 打通 KCP 与底层传输层(如 UDP)的接口,让 KCP 协议栈能通过该回调函数将封装好的数据包发送到网络,是 "协议逻辑→网络传输" 的关键桥梁。

4. 数据发送与接收函数(用户层接口,与队列的交互)

ikcp_send、ikcp_recv、ikcp_peeksize

| 函数名 | 功能描述 | 操作细节 | 核心作用 |
| ikcp_send | 上层发送接口:将用户数据分片并加入待发送队列 | 1. 流模式优化 :若启用流模式(stream=1),优先尝试填充发送队列中最后一个未填满的分片(利用剩余空间减少小分片数量,提升传输效率);2. 分片计算与拆分 :按 MSS(最大分片大小,由mtu - IKCP_OVERHEAD计算)拆分用户数据,若数据长度超过 MSS 则向上取整计算分片数; 3. 分片有效性校验 :分片数量不得超过接收窗口大小(IKCP_WND_RCV),避免接收方因缓存不足无法处理; 4. 分片创建与入队 :为每个分片分配内存,设置frg(消息模式下从count-1递减到 0,标识分片在消息中的顺序;流模式下固定为 0),将分片加入待发送队列(snd_queue),并更新队列计数(nsnd_que)。 | 处理用户发送的数据,将其拆分为符合 KCP 协议的分片并缓存,是用户数据进入 KCP 协议栈的入口,为后续ikcp_flush的发送流程做准备。 |
| ikcp_recv | 上层接收接口:从接收队列读取已重组的完整用户数据 | 1. 接收队列空检查 :检查接收队列(rcv_queue)是否有数据,无数据则返回 - 1; 2. 数据长度预览与校验 :通过ikcp_peeksize获取下一条完整消息的长度,若用户缓冲区长度不足(或 "窥视模式" 下长度不匹配)则返回 - 3; 3. 数据重组与拷贝 :按frg编号重组分片数据(拼接同一消息的所有分片),复制到用户缓冲区;非 "窥视模式" 时,删除已读取的分片并更新接收队列计数(nrcv_que); 4. 有序分片转移 :将接收缓存(rcv_buf)中序号连续(等于rcv_nxt)且接收队列未满的分片,移至接收队列(rcv_queue),并更新rcv_nxt和队列计数; 5. 窗口通知标记:若接收窗口从 "满状态" 变为 "有空闲",标记需要告知对方(`probe= IKCP_ASK_TELL`),让远端继续发送数据。 | 向用户提供完整的接收数据,管理接收缓存与队列的状态流转,是用户层获取 KCP 可靠传输后数据的出口。 |

ikcp_peeksize 预览接收队列中下一条完整消息的总长度 遍历接收队列(rcv_queue),累加同一消息的所有分片长度: - 若为单分片消息(frg=0),直接返回该分片长度; - 若为多分片消息,校验队列中分片数是否≥frg+1(确保分片完整),再累加所有属于该消息的分片长度(直到遇到frg=0的分片为止)。若分片不完整,返回 - 1。 辅助ikcp_recv判断用户提供的缓冲区是否足够容纳下一条完整消息,避免数据截断,是接收流程的 "预检查" 环节。

5. 输入数据处理函数(UDP 数据包解析,用户层接口)

ikcp_input

| 函数名 | 功能描述 | 操作细节 | 核心作用 |

ikcp_input 解析接收到的 UDP 数据包,处理 KCP 协议逻辑(ACK 确认、数据分片、窗口探测 / 通知等),维护协议状态与拥塞控制 1. 数据校验 :检查数据长度(至少包含 24 字节 KCP 协议头)和会话编号(conv),不匹配则返回错误; 2. 循环解析分片:逐一分片解析,直到剩余数据不足协议头长度: - ACK 分片(IKCP_CMD_ACK :若时间戳有效(当前时间 ≥ 分片发送时间),调用ikcp_update_ack更新 RTT/RTO;调用ikcp_parse_ack删除发送缓存中已确认的分片;记录最大 ACK 序号,触发ikcp_parse_fastack统计快速重传; - 数据分片(IKCP_CMD_PUSH :检查分片序号是否在接收窗口范围([rcv_nxt, rcv_nxt + rcv_wnd));去重后调用ikcp_parse_data将分片加入接收缓存(rcv_buf);生成 ACK 记录存入acklist; - 窗口探测分片(IKCP_CMD_WASK :标记需要回复本地窗口大小(probe |= IKCP_ASK_TELL); -窗口通知分片(IKCP_CMD_WINS :更新远端接收窗口大小(rmt_wnd); 3.拥塞控制:若收到新的确认(snd_una增大,即数据被远端确认),根据慢启动/拥塞避免策略动态调整拥塞窗口(cwnd`)。 是 KCP 接收逻辑的核心入口,处理底层 UDP 数据,解析各类 KCP 分片,维护发送 / 接收状态(窗口、RTT、ACK 记录等),并驱动拥塞控制,保障数据可靠传输与流量适配。

ikcp_parse_data

函数名 功能描述 操作细节 核心作用
ikcp_parse_data 处理接收的新分片:去重、排序,将有序分片转移至接收队列,为上层读取完整数据做准备 1. 窗口范围校验 :检查分片序号(sn)是否在接收窗口范围内([rcv_nxt, rcv_nxt + rcv_wnd)),超出范围则直接丢弃分片; 2. 去重处理 :遍历接收缓存(rcv_buf),若缓存中已存在相同sn的分片,则丢弃新分片;否则按序号顺序插入rcv_buf(保持rcv_buf内分片序号递增); 3. 有序分片转移 :循环检查rcv_buf队头分片,若其sn等于rcv_nxt且接收队列(rcv_queue)未满(nrcv_que < rcv_wnd),则将该分片从rcv_buf移至rcv_queue,并递增rcv_nxt(更新下一个期望接收的序号),直到遇到乱序分片或队列已满。 维护接收数据的有序性与完整性,将乱序接收的分片暂存于rcv_buf并逐步排序,最终把连续有序的分片转移到rcv_queue,确保上层ikcp_recv能读取到完整、连续的用户数据。

6. 发送驱动与调度函数(数据发送核心)

函数名 功能描述 操作细节 核心作用
ikcp_flush 实际发送数据(ACK 包、探测包、数据分片),处理重传和拥塞控制 1. 发送 ACK 包 :将acklist中缓存的 ACK 记录编码后发送,清空 ACK 列表; 2. 窗口探测: 若远端窗口(rmt_wnd)为 0,发送窗口探测包(IKCP_CMD_WASK); 若需告知本地窗口状态,发送窗口通知包(IKCP_CMD_WINS); 3. 数据分片发送: - 将发送队列(snd_queue)中的分片移至发送缓存(snd_buf),分配序号(sn); - 处理重传:首次发送的分片直接发送;超时未确认的分片重传(更新 RTO);被跳过的 ACK 次数达阈值时触发快速重传; 4. 拥塞控制 :根据重传情况(快速重传 / 超时重传)调整拥塞窗口(cwnd)和慢启动阈值(ssthresh)。 驱动实际数据发送,处理重传逻辑(超时重传、快速重传),并通过拥塞控制适应网络状况,是 KCP 实现可靠传输的核心发送逻辑。
ikcp_update 定时驱动 KCP 状态更新(核心调度函数) 1. 记录当前时间戳(current),初始化首次调用标志(updated); 2. 若当前时间达到预设的刷新时间(ts_flush),调用ikcp_flush发送数据; 3. 调整下一次刷新时间(ts_flush):按配置的间隔(interval)累加,若刷新时间因处理延迟落后于当前时间,则校正为 "当前时间 + 间隔"。 按固定间隔触发 KCP 状态更新和数据发送,是驱动整个 KCP 协议逻辑运行的 "定时器",保障重传检测、窗口探测等逻辑的周期性执行。
ikcp_check 计算下一次需要调用ikcp_update的时间,优化调度频率 1. 综合下一次刷新时间(ts_flush)和发送缓存(snd_buf)中各分片的重传时间(resendts); 2. 取上述时间中的最小值 作为 "最早需要调度的时间差",且该时间差不超过配置的刷新间隔(interval); 3. 返回 "当前时间 + 最早调度时间差" 的时间戳。 减少不必要的ikcp_update调用,仅在真正需要处理协议逻辑(如到达刷新时间、分片需重传)时触发,优化高并发场景下的性能(避免 CPU 空转)。

7. ACK 与 RTT 相关函数(可靠性基础)

| 函数名 | 功能描述 | 操作细节 | 核心作用 |
| ikcp_update_ack | 根据 RTT 更新平滑 RTT、RTT 偏差和 RTO | 1. 首次测量:rx_srtt=RTTrx_rttval=RTT/2; 2. 后续: - rx_srtt=(7*rx_srtt+RTT)/8; - `rx_rttval=(3*rx_rttval+RTT-rx_srtt)/4`; 3.RTO=rx_srtt+4*rx_rttval,限制范围 | 动态调整 RTO,让重传既及时又避免冗余,是可靠性的核心参数计算 |
| ikcp_shrink_buf | 更新未确认序号(snd_una),指向发送缓存最小未确认序号 | 1. 发送缓存非空:取队头分片sn; 2. 缓存为空:设为snd_nxt | 标记发送窗口左边界,明确未确认数据范围 |
| ikcp_parse_ack | 处理单个 ACK:删除发送缓存中对应序号的分片 | 1. 检查sn是否在[snd_una, snd_nxt); 2. 遍历snd_buf找到并删除分片 | 释放已确认的发送资源,更新发送窗口 |
| ikcp_parse_una | 处理批量 ACK(una):删除发送缓存中所有序号小于una的分片 | 遍历snd_buf,删除sn < una的分片,直到sn ≥ una | 批量释放已确认资源,提升效率 |
| ikcp_parse_fastack | 统计被跳过的分片,用于触发快速重传 | 1. 检查sn是否在[snd_una, snd_nxt); 2. 遍历snd_buf,对不匹配sn的分片增加fastack计数 | 为快速重传提供判断依据,减少重传延迟 |
| ikcp_ack_push | 将需要确认的snts加入 ACK 列表,用于批量发送 | 1. 容量不足时按 2 的幂扩容; 2. 加入sntsacklist | 批量发送 ACK,减少小数据包数量,提升网络效率 |

ikcp_ack_get 从 ACK 列表获取指定位置的snts 通过索引p读取acklist[p*2](sn)和acklist[p*2+1](ts) 辅助ikcp_flush批量编码 ACK 分片

8. 配置与控制函数(协议参数调整)

函数名 功能描述 操作细节 核心作用
ikcp_setmtu 设置 MTU(最大传输单元),计算 MSS(MSS=MTU-IKCP_OVERHEAD 1. 校验 MTU≥50 且≥头部开销; 2. 分配新缓冲区; 3. 更新kcp->mtukcp->mss 适配不同网络的 MTU,避免 IP 分片,提升传输效率
ikcp_interval 设置 KCP 状态刷新间隔(interval 限制间隔在 10~5000 毫秒,更新kcp->interval 调整协议处理频率,平衡延迟与 CPU 占用
ikcp_nodelay 配置无延迟模式、刷新间隔、快速重传阈值、是否禁用拥塞控制 1. nodelay=1rx_minrto=30ms; 2. 调整intervalfastresendnocwnd 优化实时场景(如游戏)的延迟,按需关闭拥塞控制
ikcp_wndsize 设置发送窗口(snd_wnd)和接收窗口(rcv_wnd 1. 发送窗口直接更新; 2. 接收窗口不小于IKCP_WND_RCV 调整流量控制力度,平衡吞吐量与缓存占用
ikcp_waitsnd 获取等待发送的数据包总数(发送缓存 + 待发送队列) 返回kcp->nsnd_buf + kcp->nsnd_que 告知用户当前发送队列积压情况,辅助流量控制
ikcp_getconv 从 KCP 数据包中解析会话 ID(conv 解码数据包头部第一个 32 位整数(conv 用于多连接场景,区分不同 KCP 会话的数据包

9. 日志与输出相关函数(调试与监控)

函数名 功能描述 操作细节 核心作用
ikcp_log 通过用户回调输出日志,支持掩码过滤 1. 检查日志掩码匹配且回调存在; 2. 格式化日志后调用kcp->writelog 提供协议运行日志,辅助调试(如数据发送 / 接收、重传、RTT 变化)
ikcp_canlog 检查是否允许记录指定类型的日志 判断(mask & kcp->logmask) != 0kcp->writelog != NULL 避免无效日志格式化操作,提升性能
ikcp_output 调用用户输出回调发送数据(底层传输接口) 1. 检查回调存在; 2. 记录输出日志(若允许); 3. 调用kcp->output 封装底层传输调用,让 KCP 与具体传输层(如 UDP)解耦
ikcp_qprint 调试用:打印队列中分片的序号和时间戳(默认关闭) 遍历队列,格式化输出每个分片的snts%10000(仅#if 1时生效) 调试队列状态(如分片排序、积压情况),仅用于开发阶段

10. 数据解析与转移函数(分段处理)

函数名 功能描述 操作细节 核心作用
ikcp_encode_seg 将 KCP 分段编码为字节流(用于发送) 按顺序编码convcmdfrgwndtssnunalen字段 生成符合 KCP 协议格式的字节流,为底层发送做准备
ikcp_wnd_unused 计算接收窗口的空闲大小(可接收的新分片数量) 返回kcp->rcv_wnd - kcp->nrcv_que(窗口满则返回 0) 告知远端当前接收能力,用于流量控制(如 ACK 中携带窗口大小)

三、运作逻辑

1. ack是如何发送的

recvfrom (用户调用)

-> ikcp_input (用户调用)

-> cmd == IKCP_CMD_PUSH

-> if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0)

-> ikcp_ack_push(kcp, sn, ts); (推到acklists)

同时if (_itimediff(sn, kcp->rcv_nxt + kcp- >rcv_wnd) <0),ikcp_parse_data(kcp, seg);

-> ikcp_update(用户调用)

-> ikcp_flush

-> for (i = 0; i < kcp->ackcount; i++) ikcp_output(kcp, buffer, size);

2. 收到ack、una、是如何删除发送缓存区的

recvfrom (用户调用)

-> ikcp_input (用户调用)

-> 解析kcp头

-> 得到una

-> ikcp_parse_una、ikcp_shrink_buf

-> cmd == IKCP_CMD_ACK

-> ikcp_parse_ack、ikcp_shrink_buf

3. RTT、RTO是如何更新的

kcp系统级别(第一次重传时间):

recvfrom (用户调用)

-> ikcp_input (用户调用)

-> cmd == IKCP_CMD_ACK

-> if (_itimediff(kcp->current, ts) >= 0)

-> ikcp_update_ack (更新RTT、RTO)

单个包多次超时重传自己本身的RTO更新

-> ikcp_update(用户调用)

-> ikcp_flush

-> for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next)

-> else if (_itimediff(current, segment->resendts) >= 0)

-> segment->xmit++; 发送次数+1

-> // 正常模式:RTO += max(当前RTO, 最新RTO)(快速增长,避免频繁重传)

if (kcp->nodelay == 0) segment->rto += imax(segment->rto, (IUINT32)kcp->rx_rto);

// 无延迟模式:RTO += RTO/2( slower增长,更快重传)

else IINT32 step = (kcp->nodelay < 2) ? ((IINT32)(segment->rto)) : kcp->rx_rto;

segment->rto += step / 2;

-> segment->resendts = current + segment->rto; // 更新重传时间

4. 窗口大小的更新

接收窗口: kcp->rcv_wnd (为常量)

空闲窗口:wnd:kcp->rcv_wnd - kcp->nrcv_que (为变量)

接收窗口: kcp->snd_wnd (为常量)

拥塞窗口:kcp->cwnd (为变量)

远程窗口(对方):kcp->rmt_wnd(为变量)

实际发送窗口cwnd:取接收窗口、远程窗口、拥塞窗口kcp->rmt_wnd(若开启)三者的最小值

发送缓存最大为实际发送窗口的大小

判断序号能否插入send_buf:_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0

接收队列长度最大为接收窗口大小

判断序号能否插入recv_buf: _itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0

  1. ikcp_input 中的更新:

recvfrom (用户调用)

-> ikcp_input (用户调用)

-> 解析kcp头

-> 得到远端窗口大小 rmt_wnd

-> kcp->rmt_wnd = rmt_wnd; 更新对方远端窗口大小

-> 处理完该UDP包中所有的KCP包之后

-> if (_itimediff(kcp->snd_una, prev_una) > 0)

-> 更新kcp->cwnd 更新拥塞窗口

  1. ikcp_flush中的更新

-> ikcp_update(用户调用)

-> ikcp_flush

-> 初始化通用KCP头

-> // 更新传给对方的空闲窗口( kcp->rcv_wnd - kcp->nrcv_que)

seg.wnd = ikcp_wnd_unused(kcp);

-> 处理完ACK、ASKW的发送后

-> 更新实际发送窗口大小

cwnd = imin(kcp->snd_wnd, kcp->rmt_wnd);

// 如果启用拥塞控制,再受拥塞窗口限制

if (kcp->nocwnd == 0) cwnd = imin(kcp->cwnd, cwnd);

-> for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next)

-> 发送完send_buf的数据后

-> if (change) 如果有快传

-> 更新kcp->cwnd 更新拥塞窗口

-> if (lost) 如果有重传

-> 更新kcp->cwnd 更新拥塞窗口

5. 快速重传是如何实现的

recvfrom (用户调用)

-> ikcp_input (用户调用)

-> 解析kcp头

-> while (1) 遍历一个UDP数据(内含多个KCP包)

-> cmd == IKCP_CMD_ACK

-> 记录max_ack

-> 遍历结束

-> if (flag != 0) 如果收到ACK

-> ikcp_parse_fastack 处理快速重传统计(统计被跳过的分段,seg->fastack++)

-> ikcp_update(用户调用)

-> ikcp_flush

-> ...

-> for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next)

-> else if (segment->fastack >= resent)

-> ikcp_output

6. 怎么判断连接状态

当 segment->xmit > kcp->dead_link ,即该分片的重传次数(超时重传与快速重传)大于最大值(20次),则判定连接断开

ikcp_update(用户调用)

-> ikcp_flush

-> ...

-> 发送单个分片完毕后

-> if (segment->xmit >= kcp->dead_link)

-> kcp->state = (IUINT32)-1; // 标记链路断开 (0为正常连接)

注意需要用户自己手动获取state的值,然后选择是否关闭(dnag

7. 用户如何收发数据

kcp->snd_nxt: snd_que的第一个数据

将数据插入snd_que的条件:无

将snd_que的数据插入snd_buf的条件:_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0

kcp ->recv_nxt: recv_que期望收到的数据

将数据插入recv_buf的条件:_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0

将数据从recv_buf插入recv_que的条件:满足sn == kcp ->recv_nxt 且 当前接收队列小于接收窗口

发送数据

ikcp_send (用户调用) : 将数据存到snd_que

-> ikcp_update(用户调用)

-> ikcp_fllush : 将数据有选择的从snd_que存到snd_buf

-> output : 按需发送snd_buf中的数据

接收数据

ikcp_input(用户调用):接续KCP包

-> cmd == IKCP_CMD_PUSH

-> ikcp_parse_data : 按需插入recv_buf、然后按需将recv_buf的数据插入recv_que

-> ikcp_recv (用户调用): 按需取recv_que的数据、然后按需将ecv_buf的数据插入recv_que

  1. 字节流与报文流的差异
差异点 报文流模式(stream=0,默认) 字节流模式(stream=1
消息边界 保留消息边界:每个ikcp_send调用对应一个完整消息,接收方按消息粒度交付。 无消息边界:多次ikcp_send的数据会被合并为连续字节流,类似 TCP。
分片标识(frg frgn-1递减到0n为分片数),0表示消息最后一个分片,用于标识消息内部分片顺序。 frg固定为0,不区分分片顺序(仅用于填充数据,无消息内部分片逻辑)。
分片策略 严格按消息拆分:每个消息独立分片,分片仅属于当前消息,不与其他消息的分片合并。 尽量填充分片:发送时会优先填充上一个未填满的分片(利用剩余空间),减少小分片数量。
接收行为 ikcp_recv一次返回一个完整消息,必须等待该消息的所有分片到达(否则返回 - 2)。 ikcp_recv返回尽可能多的连续字节(无需等待完整消息),按用户缓冲区大小返回数据。
数据合并 不同消息的分片不会合并,接收方严格区分不同ikcp_send的数据。 多个ikcp_send的数据会被合并为字节流,接收方无法区分原始发送次数。
适用场景 适合小消息、需要明确消息边界的场景(如游戏指令、协议交互、短消息)。 适合大文件传输、字节流交互场景(如文件下载、长连接数据流式传输)。
实现细节 ikcp_send会为每个消息独立分配分片,frg按消息内部分片顺序编号。 ikcp_send优先复用未填满的分片(减少分片数量),frg固定为 0,不维护消息内部分片关系。

四、值得借鉴的设计结构

1. 柔性数组

cpp 复制代码
struct IKCPSEG
{
    struct IQUEUEHEAD node;  // 链表节点:用于将分片加入发送/接收队列
    ....
    char data[1];            // 数据缓冲区:柔性数组,存储用户数据(实际长度由len指定)
};
项目 具体说明
柔性数组定义 C99 标准引入的特性,指结构体最后一个成员为未指定大小的数组(语法可写为data[]data[1]data[1]为兼容早期编译器的写法)。
柔性数组核心特点 1. 不占用结构体固定内存:sizeof(IKCPSEG)计算时不包含data数组的大小; 2. 位置约束:必须作为结构体的最后一个成员; 3. 内存分配:需通过动态内存分配(如malloc)为其预留空间,空间大小由实际数据长度决定。
KCP 中数据存储需求 KCP 分片(IKCPSEG)需携带用户数据,且数据长度动态变化(最大不超过 MSS,即mtu - IKCP_OVERHEAD): - 短消息:1 个分片,数据长度可能仅几十字节; - 长消息:拆分为多个分片,每个分片数据长度接近 MSS(如 1376 字节)。
传统方案的缺陷 1. 固定大小数组(如char data[1400]):多数场景下数据长度远小于 MSS,会造成大量内存浪费; 2. 指针(如char* data):需额外分配数据内存并维护指针,增加内存碎片和访问开销(需二次指针跳转)。
柔性数组data[1]的优势 1. 动态适配长度:通过ikcp_segment_new分配内存时,总大小为sizeof(IKCPSEG) + sizesize为实际数据长度),data指向结构体末尾预留的size字节空间,直接存储用户数据; 2. 内存连续:结构体元数据(convsn等)与用户数据在内存中连续,访问无需二次跳转,效率更高; 3. 管理简单:一次malloc分配结构体 + 数据总内存,一次free即可释放,避免指针管理不当导致的内存泄漏。

2. 通过结构体成员指针反推结构体本身指针

cpp 复制代码
// 计算结构体成员的偏移量:用于从成员指针反向获取结构体指针
#define IOFFSETOF(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

// 从成员指针获取结构体指针(容器_of实现):KCP核心技巧
// 原理:通过成员偏移量计算结构体起始地址,适用于链表节点嵌入其他结构体的场景
#define ICONTAINEROF(ptr, type, member) ( \
		(type*)( ((char*)((type*)ptr)) - IOFFSETOF(type, member)) )

// 从链表节点指针获取包含该节点的结构体指针
#define IQUEUE_ENTRY(ptr, type, member) ICONTAINEROF(ptr, type, member)
项目 说明 示例 / 原理(结合 KCP 的struct IKCPSEG
核心原理 通过结构体成员的指针,结合该成员在结构体中的偏移量,反向计算出结构体本身的指针。 已知成员member的指针ptr,结构体起始地址 = ptr - 成员在结构体中的偏移量
关键宏 1:计算偏移量 IOFFSETOF(TYPE, MEMBER):计算成员MEMBER在结构体TYPE中的偏移量(字节数)。 原理:假设结构体首地址为 0,成员MEMBER的地址即为偏移量。示例:IOFFSETOF(struct IKCPSEG, node) 计算nodeIKCPSEG中的偏移量(因node是第一个成员,偏移量为 0)。
关键宏 2:反推结构体指针 ICONTAINEROF(ptr, type, member):由成员member的指针ptr,反推type类型结构体的指针。 公式:(type*)((char*)ptr - IOFFSETOF(type, member))。步骤: 1. 将成员指针ptr转为char*(按字节寻址); 2. 减去成员偏移量,得到结构体起始地址; 3. 转为type*类型。
KCP 中的典型应用 通过链表节点IQUEUEHEAD*反推所属的IKCPSEG*分片指针。 IKCPSEG中嵌入struct IQUEUEHEAD node作为链表节点,遍历链表时: 已知IQUEUEHEAD* node_ptr(指向某个分片的node),通过IQUEUE_ENTRY(node_ptr, struct IKCPSEG, node)(封装了ICONTAINEROF)得到对应的IKCPSEG*,从而操作分片的sndata等成员。
核心价值 1. 无需为成员额外存储 "所属结构体指针",节省内存; 2. 简化链表等数据结构的操作,通过嵌入的成员即可关联到完整结构体; 3. 广泛用于内存敏感场景(如协议栈、内核)。 KCP 通过该技巧,仅用node成员即可将IKCPSEG接入链表,遍历链表时能快速定位分片,避免冗余指针带来的内存开销和管理复杂度。

3. hook钩子

cpp 复制代码
// 内存分配/释放钩子(允许用户自定义内存管理,如使用内存池)
static void *(*ikcp_malloc_hook)(size_t) = NULL;
static void (*ikcp_free_hook)(void *) = NULL;

// 内部内存分配函数(优先使用用户自定义钩子,否则用系统malloc)
static void *ikcp_malloc(size_t size)
{
	if (ikcp_malloc_hook)
		return ikcp_malloc_hook(size);
	return malloc(size);
}

// 内部内存释放函数(优先使用用户自定义钩子,否则用系统free)
static void ikcp_free(void *ptr)
{
	if (ikcp_free_hook)
	{
		ikcp_free_hook(ptr);
	}
	else
	{
		free(ptr);
	}
}

// 重定义内存分配器(供用户设置自定义malloc/free)
void ikcp_allocator(void *(*new_malloc)(size_t), void (*new_free)(void *))
{
	ikcp_malloc_hook = new_malloc;
	ikcp_free_hook = new_free;
}

3.1 钩子定义

在 KCP 的内存管理代码中,hook(钩子)是一种允许用户介入或替换库默认逻辑的机制

3.2 为什么使用钩子?

KCP 作为通用协议库,默认使用系统malloc/free管理内存,但不同场景可能有特殊需求(如性能优化、调试跟踪等)。钩子机制允许用户用自定义逻辑替换默认内存操作,而无需修改 KCP 源码,举例:

  1. 内存池优化 :高频内存分配(如 KCP 分片的创建 / 销毁)会导致系统malloc/free性能损耗。用户可实现内存池(预先分配一批内存块,按需分配 / 回收),通过ikcp_allocator挂钩后,KCP 会使用内存池管理内存,提升性能。

  2. 内存调试与监控 :用户可在自定义malloc/free中添加日志(记录分配大小、地址、调用栈),或检测内存泄漏(如统计分配 / 释放次数是否匹配),帮助调试 KCP 的内存使用问题。

  3. 跨平台适配 :某些嵌入式系统或特殊环境可能不支持标准malloc/free,用户可通过钩子接入平台专用的内存管理接口,确保 KCP 正常运行

3.3 钩子机制的核心优势

优势 说明
无侵入性 无需修改 KCP 核心代码,通过外部接口(ikcp_allocator)即可替换逻辑,降低维护成本。
灵活性与可扩展性 不同用户 / 场景可根据需求定制(如内存池、调试、跨平台),库本身保持通用。
默认兼容性 未设置钩子时自动使用系统默认逻辑,保证基础功能可用,无需用户额外配置。

总结

KCP 中的ikcp_malloc_hookikcp_free_hook函数钩子的典型应用:通过函数指针预留 "扩展点",允许用户在不侵入库核心逻辑的前提下,替换内存管理等关键操作,从而适配不同场景的需求(性能、调试、跨平台等)。这种机制是库设计中 "开闭原则"(对扩展开放、对修改关闭)的经典实践。

4.snd_queue、rcv_queue、snd_buf、rcv_buf设计结构的优势

队列名称 所属侧 核心功能 数据流转(来源→去向) 设计优势具体体现
snd_queue 发送侧 暂存用户发送的分片,这些分片未进入发送窗口,等待窗口空闲后被处理 用户数据(ikcp_send分片)→ snd_queue → 符合窗口条件后移至snd_bufikcp_flush 1. 解耦用户发送与窗口控制:用户调用ikcp_send后直接返回,无需等待窗口,降低用户逻辑阻塞; 2. 隔离未发送数据:与已进入窗口的snd_buf分离,简化状态管理。
snd_buf 发送侧 存储已进入发送窗口的分片,负责重传(超时 / 快速重传)和等待 ACK 确认 snd_queuesnd_buf → 网络(UDP 发送);确认后从snd_buf删除(ikcp_parse_ack 1. 集中管理重传逻辑:仅处理已发送未确认的分片,便于统一检测超时和快速重传; 2. 精细流量控制:受snd_wnd(本地发送窗口)、cwnd(拥塞窗口)、rmt_wnd(远端窗口)限制,避免发送过载。
rcv_buf 接收侧 暂存从网络收到的分片(可能乱序),等待排序后转移至接收队列 网络数据(ikcp_input解析)→ rcv_buf → 序号连续的分片移至rcv_queue 1. 处理 UDP 乱序:缓存乱序到达的分片,通过序号排序确保数据连续性; 2. 隔离未就绪数据:与用户可读取的rcv_queue分离,避免用户直接处理乱序数据。
rcv_queue 接收侧 存储已排序且连续 的分片,供用户通过ikcp_recv读取 rcv_buf(有序分片)→ rcv_queue → 用户(ikcp_recv读取后删除) 1. 保障用户数据有序性:仅向用户提供连续完整的数据,屏蔽 UDP 乱序特性; 2. 接收端流量控制:大小受rcv_wnd限制,数据被读取后释放窗口,通过IKCP_CMD_WINS告知发送端继续发送。

对比传统单队列:设计核心优势总结

传统单队列问题 四队列设计的改进
用户发送需立即判断窗口,窗口满则阻塞 snd_queue 暂存数据,用户发送无需等待窗口,降低延迟
已发送 / 未发送、已接收 / 未接收分片混杂 四队列分层管理,状态隔离,逻辑清晰
无法精细控制发送速率,易引发拥塞或崩溃 snd_buf 和 rcv_buf 通过多层窗口限制,实现闭环流量控制
乱序与重传逻辑混杂,状态管理复杂 rcv_buf 处理乱序、snd_buf 处理重传,职责单一

总的来说:分层队列设计使逻辑清晰,便于修改单一阶段(如调整拥塞控制策略仅需修改snd_buf逻辑),且支持多线程优化(分队列加锁)。

五、用户能调用的API

接口函数 功能描述
ikcp_create(IUINT32 conv, void *user) 创建 KCP 控制块(会话),conv为会话编号(双方需一致),user为用户自定义数据指针;返回控制块指针,失败返回 NULL。
ikcp_release(ikcpcb *kcp) 释放 KCP 控制块及所有资源(队列、缓存、ACK 列表等),调用后控制块指针失效。
ikcp_setoutput(ikcpcb *kcp, int (*output)(...)) 设置发送回调函数,KCP 需发送数据时会调用该函数(由用户实现 UDP 底层发送逻辑)。
ikcp_recv(ikcpcb *kcp, char *buffer, int len) 从接收队列(rcv_queue)读取已排序的完整用户数据;buffer为接收缓冲区,len为缓冲区最大长度;返回读取的数据长度,失败返回负数(如无数据)。
ikcp_send(ikcpcb *kcp, const char *buffer, int len) 将用户数据加入发送队列(snd_queue)等待分片发送;buffer为待发送数据,len为数据长度;返回 0 表示成功,失败返回负数(如窗口满)。
ikcp_update(ikcpcb *kcp, IUINT32 current) 定期更新 KCP 状态(必须调用),处理超时重传、ACK 批量发送、窗口探查等逻辑;current为当前时间戳(毫秒级)。
ikcp_check(const ikcpcb *kcp, IUINT32 current) 获取下次调用ikcp_update的时间戳,用于优化调度(减少不必要的频繁调用);返回下次更新时间。
ikcp_input(ikcpcb *kcp, const char *data, long size) 处理底层接收的 UDP 数据,解析为 KCP 分片并处理(数据分片、ACK、窗口探查等);data为 UDP 数据包,size为包长度;返回 0 表示成功,失败返回负数。
ikcp_flush(ikcpcb *kcp) 刷新待发送数据:将send_queue中的消息分片,加入send_buf并调用发送回调发送;可主动调用或由ikcp_update自动触发。
ikcp_peeksize(const ikcpcb *kcp) 获取接收队列(rcv_queue)中下一条消息的长度,用于判断缓冲区是否足够;返回消息长度,无消息返回 0。
ikcp_setmtu(ikcpcb *kcp, int mtu) 设置最大传输单元(MTU),自动计算最大分片大小(MSS = MTU - 协议头长度);mtu最小为 50 字节;返回 0 表示成功。
ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd) 设置发送窗口和接收窗口大小:sndwnd为本地允许的未确认分片数,rcvwnd为本地可缓存的未处理分片数;返回 0 表示成功。
ikcp_waitsnd(const ikcpcb *kcp) 获取待发送(含已发送未确认)的分片总数,用于监控发送队列状态;返回分片数量。
ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc) 配置无延迟模式:nodelay为无延迟开关(1 启用),interval为刷新间隔(毫秒),resend为快速重传阈值,nc为拥塞控制开关(1 关闭);返回 0 表示成功。
ikcp_log(ikcpcb *kcp, int mask, const char *fmt, ...) 输出 KCP 日志,通过writelog回调函数实现;mask为日志类型掩码(如IKCP_LOG_SEND),fmt为格式化字符串。
ikcp_allocator(void* (*new_malloc)(size_t), void (*new_free)(void*)) 设置自定义内存分配器,替换默认的mallocfree(如使用内存池优化性能)。
ikcp_getconv(const void *ptr) 从 UDP 数据包中解析会话编号(conv),用于匹配对应的 KCP 会话;返回解析出的conv

六、基于kcp协议实现的简单实例

示例1:客户端不发送心跳,服务器监测到客户端在某个时间内没发信息就断开连接

1. 服务端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include "ikcp.h"

// 最大连接数
#define MAX_CONNECTIONS 1024
// 连接超时时间(30秒)
#define TIMEOUT_MS 30000
#define SHUTDOWN_MSG "KCP_SERVER_SHUTDOWN"


// UDP套接字
int udp_socket;

// KCP连接结构体(管理单个会话)
typedef struct {
    uint32_t conv;               // 会话标识
    ikcpcb *kcp;                 // KCP实例
    struct sockaddr_in client_addr; // 客户端地址
    socklen_t client_addr_len;   // 地址长度
    uint32_t last_active;        // 最后活动时间(毫秒)
} KcpConnection;

// 连接管理数组
KcpConnection connections[MAX_CONNECTIONS];
int conn_count = 0;

// 互斥锁(单线程可省略,多线程需添加)
// pthread_mutex_t conn_mutex = PTHREAD_MUTEX_INITIALIZER;

// KCP发送回调(使用当前连接的客户端地址)
int kcp_send_cb(const char *buf, int len, ikcpcb *kcp, void *user) {
    KcpConnection *conn = (KcpConnection *)user;
    if (sendto(udp_socket, buf, len, 0, 
              (struct sockaddr*)&conn->client_addr, conn->client_addr_len) < 0) {
        perror("sendto failed");
        return -1;
    }
    return len;
}

// 获取当前毫秒时间
uint32_t current_ms() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// 查找或创建KCP连接(根据conv)
KcpConnection* find_or_create_connection(uint32_t conv, 
                                         struct sockaddr_in *client_addr, 
                                         socklen_t addr_len) {
    // 查找现有连接
    for (int i = 0; i < conn_count; i++) {
        if (connections[i].conv == conv) {
            // 更新最后活动时间
            connections[i].last_active = current_ms();
            return &connections[i];
        }
    }

    // 超过最大连接数,返回NULL
    if (conn_count >= MAX_CONNECTIONS) {
        fprintf(stderr, "Too many connections (max: %d)\n", MAX_CONNECTIONS);
        return NULL;
    }

    // 创建新连接
    KcpConnection *conn = &connections[conn_count++];
    conn->conv = conv;
    conn->kcp = ikcp_create(conv, conn); // user参数绑定当前连接
    if (!conn->kcp) {
        fprintf(stderr, "Failed to create KCP instance for conv: %u\n", conv);
        conn_count--;
        return NULL;
    }

    // 初始化KCP参数
    conn->kcp->output = kcp_send_cb;
    ikcp_nodelay(conn->kcp, 1, 10, 2, 1); // 快速模式
    ikcp_wndsize(conn->kcp, 128, 128);   // 窗口大小

    // 保存客户端地址
    memcpy(&conn->client_addr, client_addr, addr_len);
    conn->client_addr_len = addr_len;
    conn->last_active = current_ms();

    printf("New connection created, conv: %u\n", conv);
    return conn;
}

// 清理超时连接
void cleanup_timeout_connections() {
    uint32_t now = current_ms();
    for (int i = 0; i < conn_count; ) {
        if (now - connections[i].last_active > TIMEOUT_MS) {
            printf("Connection conv: %u timeout, closed\n", connections[i].conv);
            ikcp_send(connections[i].kcp, SHUTDOWN_MSG, strlen(SHUTDOWN_MSG)); // 可选:通知客户端
            ikcp_flush(connections[i].kcp); // 立即发送数据
            ikcp_release(connections[i].kcp); // 释放KCP实例
            // 用最后一个连接覆盖当前位置,减少数组移动
            if (i < conn_count - 1) {
                connections[i] = connections[conn_count - 1];
            }
            conn_count--;
        } else {
            i++;
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        return 1;
    }

    int port = atoi(argv[1]);

    // 创建UDP套接字
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        return 1;
    }

    // 绑定地址和端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);

    if (bind(udp_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(udp_socket);
        return 1;
    }

    printf("KCP server started on port %d (dynamic conv)\n", port);

    char udp_buf[4096];
    uint32_t current_time, last_update = current_ms();

    while (1) {
        current_time = current_ms();

        // 每10ms更新所有KCP实例状态
        if (current_time - last_update >= 10) {
            for (int i = 0; i < conn_count; i++) {
                ikcp_update(connections[i].kcp, current_time);
            }
            last_update = current_time;
            // 定期清理超时连接
            cleanup_timeout_connections();
        }

        // 非阻塞接收UDP数据
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        ssize_t n = recvfrom(udp_socket, udp_buf, sizeof(udp_buf), 
                            MSG_DONTWAIT, (struct sockaddr*)&client_addr, &client_addr_len);

        if (n > 0) {
            // KCP数据包至少包含4字节conv,否则忽略
            if (n < 4) {
                fprintf(stderr, "Invalid KCP packet (too short)\n");
                continue;
            }

            // 从KCP头部解析conv(前4字节,小端序)
            uint32_t conv = *(uint32_t*)udp_buf;

            // 查找或创建连接
            KcpConnection *conn = find_or_create_connection(conv, &client_addr, client_addr_len);
            if (!conn) continue;

            // 将UDP数据输入到KCP
            if (ikcp_input(conn->kcp, udp_buf, n) != 0) {
                fprintf(stderr, "ikcp_input failed for conv: %u\n", conv);
                continue;
            }

            // 从KCP中提取有效数据
            char kcp_recv_buf[4096];
            int recv_len;
            while ((recv_len = ikcp_recv(conn->kcp, kcp_recv_buf, sizeof(kcp_recv_buf))) > 0) {
                kcp_recv_buf[recv_len] = '\0';
                printf("Received from conv %u: %s\n", conv, kcp_recv_buf);
                // 回显数据给客户端
                ikcp_send(conn->kcp, kcp_recv_buf, recv_len);
            }
        }

        usleep(1000); // 降低CPU占用
    }

    // 清理资源(实际运行中不会执行到这里)
    for (int i = 0; i < conn_count; i++) {
        ikcp_release(connections[i].kcp);
    }
    close(udp_socket);
    return 0;
}

2. 客户端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include "ikcp.h"

#define SHUTDOWN_MSG "KCP_SERVER_SHUTDOWN"

// UDP套接字和服务端地址
int udp_socket;
struct sockaddr_in server_addr;
socklen_t server_addr_len = sizeof(server_addr);

// KCP发送回调
int kcp_send_cb(const char *buf, int len, ikcpcb *kcp, void *user)
{
    if (sendto(udp_socket, buf, len, 0,
               (struct sockaddr *)&server_addr, server_addr_len) < 0)
    {
        perror("sendto failed");
        return -1;
    }
    return len;
}

// 获取当前毫秒时间
uint32_t current_ms()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <server_ip> <port>\n", argv[0]);
        return 1;
    }

    // 随机生成conv(实际应用中可使用更复杂的生成逻辑)
    srand(time(NULL));
    uint32_t conv = rand();
    printf("Client conv: %u\n", conv);

    // 创建UDP套接字
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        perror("socket creation failed");
        return 1;
    }

    // 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0)
    {
        perror("invalid address");
        close(udp_socket);
        return 1;
    }

    // 初始化KCP(使用随机生成的conv)
    ikcpcb *kcp = ikcp_create(conv, NULL);
    if (!kcp)
    {
        fprintf(stderr, "Failed to create KCP instance\n");
        close(udp_socket);
        return 1;
    }

    // 配置KCP参数
    kcp->output = kcp_send_cb;
    ikcp_nodelay(kcp, 1, 10, 2, 1); // 快速模式
    ikcp_wndsize(kcp, 128, 128);    // 窗口大小

    printf("Connected to %s:%s, enter messages to send (Ctrl+C to exit)\n", argv[1], argv[2]);

    char input_buf[1024];
    char udp_buf[4096];
    char kcp_recv_buf[4096];
    uint32_t last_update = current_ms();

    // 用select处理输入和接收
    fd_set read_fds;
    struct timeval tv;
    int max_fd = udp_socket + 1;

    while (1)
    {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(udp_socket, &read_fds);

        // 超时10ms,用于KCP定期更新
        tv.tv_sec = 0;
        tv.tv_usec = 10000;

        int activity = select(max_fd, &read_fds, NULL, NULL, &tv);
        if (activity < 0 && errno != EINTR)
        {
            perror("select error");
            break;
        }

        // 更新KCP状态
        ikcp_update(kcp, current_ms());

        // 处理用户输入
        if (FD_ISSET(STDIN_FILENO, &read_fds))
        {
            if (fgets(input_buf, sizeof(input_buf), stdin) != NULL)
            {
                size_t len = strlen(input_buf);
                if (len > 0 && input_buf[len - 1] == '\n')
                {
                    input_buf[len - 1] = '\0'; // 移除换行符
                    len--;
                }
                if (len > 0)
                {
                    ikcp_send(kcp, input_buf, len);
                    printf("Sent: %s\n", input_buf);
                }
            }
        }

        // 处理服务端回复
        if (FD_ISSET(udp_socket, &read_fds))
        {
            ssize_t n = recvfrom(udp_socket, udp_buf, sizeof(udp_buf), 0, NULL, NULL);
            if (n > 0)
            {
                ikcp_input(kcp, udp_buf, n); // 输入到KCP
                // 提取有效数据
                int recv_len;
                while ((recv_len = ikcp_recv(kcp, kcp_recv_buf, sizeof(kcp_recv_buf))) > 0)
                {
                    kcp_recv_buf[recv_len] = '\0';
     
                    if (strcmp(kcp_recv_buf, SHUTDOWN_MSG) == 0)
                    {
                        printf("Connection timed out by server.\n");
                        ikcp_release(kcp);
                        close(udp_socket);

                        return 0;
                    }
                    
                    printf("Received from server: %s\n", kcp_recv_buf);
                }
            }
        }
    }

    // 清理资源
    ikcp_release(kcp);
    close(udp_socket);
    return 0;
}

示例2:客户端定期发送心跳PING,服务端回应PONG,在一定时间内客户端或服务端没收到对方的心跳或连接,就断开连接。

1. 服务端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <errno.h>
#include <signal.h>
#include "ikcp.h"

#define MAX_CONNECTIONS 1024
#define TIMEOUT_MS 30000
#define SHUTDOWN_MSG "KCP_SERVER_SHUTDOWN" // 断开通知标识

int udp_socket;
int server_running = 1; // 服务端运行状态

typedef struct {
    uint32_t conv;
    ikcpcb *kcp;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len;
    uint32_t last_active;
} KcpConnection;

KcpConnection connections[MAX_CONNECTIONS];
int conn_count = 0;

// 发送回调(同前)
int kcp_send_cb(const char *buf, int len, ikcpcb *kcp, void *user) {
    KcpConnection *conn = (KcpConnection *)user;
    if (sendto(udp_socket, buf, len, 0, 
              (struct sockaddr*)&conn->client_addr, conn->client_addr_len) < 0) {
        return -1;
    }
    return len;
}

// 获取当前毫秒时间(同前)
uint32_t current_ms() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// 查找连接(同前)
KcpConnection* find_connection(uint32_t conv) {
    for (int i = 0; i < conn_count; i++) {
        if (connections[i].conv == conv) {
            return &connections[i];
        }
    }
    return NULL;
}

// 创建连接(同前)
KcpConnection* create_connection(uint32_t conv, struct sockaddr_in *client_addr, socklen_t addr_len) {
    if (conn_count >= MAX_CONNECTIONS) return NULL;
    KcpConnection *conn = &connections[conn_count++];
    conn->conv = conv;
    conn->kcp = ikcp_create(conv, conn);
    if (!conn->kcp) { conn_count--; return NULL; }
    conn->kcp->output = kcp_send_cb;
    ikcp_nodelay(conn->kcp, 1, 10, 2, 1);
    ikcp_wndsize(conn->kcp, 128, 128);
    memcpy(&conn->client_addr, client_addr, addr_len);
    conn->client_addr_len = addr_len;
    conn->last_active = current_ms();
    printf("New connection, conv: %u\n", conv);
    return conn;
}

// 清理超时连接(同前)
void cleanup_timeout_connections() {
    uint32_t now = current_ms();
    for (int i = 0; i < conn_count; ) {
        if (now - connections[i].last_active > TIMEOUT_MS) {
            printf("Connection conv: %u timeout\n", connections[i].conv);
            ikcp_release(connections[i].kcp);
            if (i < conn_count - 1) connections[i] = connections[conn_count - 1];
            conn_count--;
        } else {
            i++;
        }
    }
}

// 服务器关闭处理函数:发送断开通知
void server_shutdown() {
    server_running = 0;
    printf("\nServer shutting down, notifying clients...\n");
    
    // 向所有连接发送断开通知
    for (int i = 0; i < conn_count; i++) {
        KcpConnection *conn = &connections[i];
        // 发送断开标识
        ikcp_send(conn->kcp, SHUTDOWN_MSG, strlen(SHUTDOWN_MSG));
        // 强制刷新KCP缓冲区,确保消息发送
        ikcp_flush(conn->kcp);
        printf("Notified client conv: %u\n", conn->conv);
    }
    
    // 短暂等待,确保消息发送完成(UDP可能丢包,多次尝试)
    usleep(100000); // 100ms
    for (int i = 0; i < conn_count; i++) {
        KcpConnection *conn = &connections[i];
        ikcp_send(conn->kcp, SHUTDOWN_MSG, strlen(SHUTDOWN_MSG));
        ikcp_flush(conn->kcp);
    }
    
    // 释放资源
    for (int i = 0; i < conn_count; i++) {
        ikcp_release(connections[i].kcp);
    }
    close(udp_socket);
    printf("Server closed\n");
    exit(0);
}

// 捕获Ctrl+C信号,触发优雅关闭
void handle_signal(int sig) {
    if (sig == SIGINT) {
        server_shutdown();
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        return 1;
    }

    signal(SIGINT, handle_signal); // 注册信号处理

    int port = atoi(argv[1]);
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket failed");
        return 1;
    }

    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);
    if (bind(udp_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(udp_socket);
        return 1;
    }

    printf("KCP server (port %d) running, press Ctrl+C to exit\n", port);

    char udp_buf[4096];
    uint32_t last_update = current_ms();

    while (server_running) {
        uint32_t now = current_ms();

        // 每10ms更新KCP状态
        if (now - last_update >= 10) {
            for (int i = 0; i < conn_count; i++) {
                ikcp_update(connections[i].kcp, now);
            }
            last_update = now;
            cleanup_timeout_connections();
        }

        // 接收UDP数据
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);
        ssize_t n = recvfrom(udp_socket, udp_buf, sizeof(udp_buf), MSG_DONTWAIT, 
                            (struct sockaddr*)&client_addr, &addr_len);

        if (n > 0) {
            if (n < 4) continue; // 无效KCP包
            uint32_t conv = *(uint32_t*)udp_buf;

            KcpConnection *conn = find_connection(conv);
            if (!conn) {
                conn = create_connection(conv, &client_addr, addr_len);
                if (!conn) continue;
            }
            conn->last_active = now;

            // 处理KCP输入
            if (ikcp_input(conn->kcp, udp_buf, n) != 0) continue;

            // 接收客户端数据(包含心跳)
            char kcp_recv_buf[4096];
            int recv_len;
            while ((recv_len = ikcp_recv(conn->kcp, kcp_recv_buf, sizeof(kcp_recv_buf))) > 0) {
                kcp_recv_buf[recv_len] = '\0';
                printf("conv %u: %s\n", conv, kcp_recv_buf);

                // 若客户端发送心跳,回应心跳
                if (strcmp(kcp_recv_buf, "PING") == 0) {
                    ikcp_send(conn->kcp, "PONG", 4);
                } else {
                    // 普通消息回显
                    ikcp_send(conn->kcp, kcp_recv_buf, recv_len);
                }
            }
        }

        usleep(1000);
    }

    server_shutdown();
    return 0;
}

2. 客户端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include "ikcp.h"

#define SHUTDOWN_MSG "KCP_SERVER_SHUTDOWN" // 服务器断开标识
#define HEARTBEAT_INTERVAL 5000 // 心跳间隔(5秒)
#define TIMEOUT_MS 10000 // 超时时间(10秒,超过则判定断开)

int udp_socket;
struct sockaddr_in server_addr;
socklen_t server_addr_len = sizeof(server_addr);
uint32_t last_recv_time; // 最后一次收到服务器数据的时间

// 发送回调(同前)
int kcp_send_cb(const char *buf, int len, ikcpcb *kcp, void *user) {
    if (sendto(udp_socket, buf, len, 0, (struct sockaddr*)&server_addr, server_addr_len) < 0) {
        perror("sendto failed");
        return -1;
    }
    return len;
}

// 获取当前毫秒时间(同前)
uint32_t current_ms() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <server_ip> <port>\n", argv[0]);
        return 1;
    }

    // 随机生成conv
    srand(time(NULL));
    uint32_t conv = rand();
    printf("Client conv: %u\n", conv);

    // 创建UDP套接字
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket failed");
        return 1;
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
        perror("invalid address");
        close(udp_socket);
        return 1;
    }

    // 初始化KCP
    ikcpcb *kcp = ikcp_create(conv, NULL);
    if (!kcp) {
        fprintf(stderr, "KCP create failed\n");
        close(udp_socket);
        return 1;
    }
    kcp->output = kcp_send_cb;
    ikcp_nodelay(kcp, 1, 10, 2, 1);
    ikcp_wndsize(kcp, 128, 128);

    printf("Connected to %s:%s, enter messages (Ctrl+C to exit)\n", argv[1], argv[2]);

    char input_buf[1024];
    char udp_buf[4096];
    char kcp_recv_buf[4096];
    uint32_t last_heartbeat = current_ms(); // 最后一次发送心跳的时间
    last_recv_time = current_ms(); // 初始化最后接收时间

    fd_set read_fds;
    struct timeval tv;
    int max_fd = udp_socket + 1;
    int running = 1;

    while (running) {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(udp_socket, &read_fds);

        // 超时10ms,用于定期任务
        tv.tv_sec = 0;
        tv.tv_usec = 10000;

        int activity = select(max_fd, &read_fds, NULL, NULL, &tv);
        if (activity < 0 && errno != EINTR) {
            perror("select error");
            break;
        }

        uint32_t now = current_ms();
        ikcp_update(kcp, now);

        // 定期发送心跳
        if (now - last_heartbeat >= HEARTBEAT_INTERVAL) {
            ikcp_send(kcp, "PING", 4);
            last_heartbeat = now;
            // printf("Sent heartbeat\n");
        }

        // 检测服务器超时(长时间未收到数据)
        if (now - last_recv_time > TIMEOUT_MS) {
            printf("\nServer disconnected (timeout)\n");
            running = 0;
            break;
        }

        // 处理用户输入
        if (FD_ISSET(STDIN_FILENO, &read_fds)) {
            if (fgets(input_buf, sizeof(input_buf), stdin) != NULL) {
                size_t len = strlen(input_buf);
                if (len > 0 && input_buf[len-1] == '\n') {
                    input_buf[len-1] = '\0';
                    len--;
                }
                if (len > 0) {
                    ikcp_send(kcp, input_buf, len);
                    printf("Sent: %s\n", input_buf);
                }
            }
        }

        // 处理服务器数据
        if (FD_ISSET(udp_socket, &read_fds)) {
            ssize_t n = recvfrom(udp_socket, udp_buf, sizeof(udp_buf), 0, NULL, NULL);
            if (n > 0) {
                last_recv_time = now; // 更新最后接收时间
                ikcp_input(kcp, udp_buf, n);

                int recv_len;
                while ((recv_len = ikcp_recv(kcp, kcp_recv_buf, sizeof(kcp_recv_buf))) > 0) {
                    kcp_recv_buf[recv_len] = '\0';
                    
                    // 检测服务器主动断开通知
                    if (strcmp(kcp_recv_buf, SHUTDOWN_MSG) == 0) {
                        printf("\nServer notified shutdown\n");
                        running = 0;
                        break;
                    }
                    
                    // 处理心跳回应
                    if (strcmp(kcp_recv_buf, "PONG") == 0) {
                        // printf("Received heartbeat ack\n");
                    } else {
                        printf("Received: %s\n", kcp_recv_buf);
                    }
                }
                if (!running) break;
            }
        }
    }

    // 清理资源
    ikcp_release(kcp);
    close(udp_socket);
    printf("Client exited\n");
    return 0;
}
相关推荐
SKYDROID云卓小助手1 天前
无人设备遥控器之数字图传技术
运维·服务器·单片机·嵌入式硬件·fpga开发
Larry_Yanan1 天前
QML学习笔记(五十三)QML与C++交互:数据转换——序列类型与 JavaScript 数组的转换
c++·笔记·学习
特立独行的猫a1 天前
HarmonyOS黑马云音乐项目增加网络听歌功能(一、轮播图的实现)
网络·华为·harmonyos·开源项目·黑马云音乐
jenchoi4131 天前
【2025-11-03】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
网络·安全·web安全·网络安全
努力努力再努力wz1 天前
【Linux进阶系列】:线程(上)
java·linux·运维·服务器·数据结构·c++·redis
仟千意1 天前
C++:类和对象---初级篇
c++
java 乐山1 天前
蓝牙网关(备份)
linux·网络·算法
2301_803554521 天前
面试后查缺补漏--cmake,makefiles,g++,gcc(自写精华版)
linux·运维·服务器
Brianna Home1 天前
现代C++:从性能泥潭到AI基石
开发语言·c++·算法
煤球王子1 天前
浅学任务调度
linux