news 2026/10/21 5:54:38

Qt/C++ 手撸 RTMP 拉流:从 Chunk 解包到 AVCC 转 AnnexB 的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt/C++ 手撸 RTMP 拉流:从 Chunk 解包到 AVCC 转 AnnexB 的完整实现

之前写了两篇推流文章(RTSP 推流、RTMP 推流),这次补齐——RTMP 拉流。拉流和推流方向相反,但协议的坑一点不少。

本文按拉流的实际时序展开,每一步都给出真实的十六进制字节并逐字段中文注释。读完你能彻底搞懂:

  • RTMP 三次握手的字节交换过程

  • Chunk 分片如何重组为完整消息

  • AMF0 二进制编码的每条命令

  • AVCC → AnnexB 的转换(拉流方向)

  • AVCDecoderConfigurationRecord 的字段含义

全文覆盖四条主线:握手 → 信令 → Chunk 重组 → 载荷转换。


一、RTMP vs RTSP 对比

先建立全局认知:

维度RTSPRTMP
传输层TCP 信令 + UDP/TCP RTP纯 TCP
握手无(直接 OPTIONS)C0/C1/C2 + S0/S1/S2
信令编码文本(RTSP 头)AMF0 二进制
载荷格式RTP 分片Chunk 分片
H.264 格式AnnexBAVCC
config 传法SDPsprop-parameter-sets独立 Sequence Header 消息
端口5541935
拉流信令OPTIONS → DESCRIBE → SETUP → PLAYconnect → createStream → play

核心洞察:

RTSP 靠"文本协议 + RTP 分片",RTMP 靠"二进制协议 + Chunk 分片"。两者对 H.264 的表示方式完全不同,这是所有坑的根源。


二、RTMP 四层模型

┌──────────────────────────────────┐ │ Application Layer │ connect / createStream / play │ (AMF0 命令消息) │ ├──────────────────────────────────┤ │ Message Layer │ typeId + length + streamId + body ├──────────────────────────────────┤ │ Chunk Layer │ ★ 把 Message 切成固定大小 Chunk ├──────────────────────────────────┤ │ Transport Layer │ 纯 TCP └──────────────────────────────────┘

四个关键概念:

概念含义
Message一条完整逻辑消息(一个视频帧 / 一个命令)
ChunkMessage 的分片,每个最多chunkSize字节
Chunk Stream用CSID区分多条逻辑通道
Message Stream用MSID区分同一连接的多个流

为什么要分 Chunk?TCP 是字节流,一次写太大会阻塞低优先级消息。分片后可以交错发送不同 Chunk Stream 的数据。

默认 chunkSize = 128,握手后双方可用SetChunkSize消息修改。


三、★ Chunk 结构详解

每个 Chunk 三部分:

┌──────────────────┬──────────────────┬─────────────┐ │ Basic Header │ Message Header │ Chunk Data │ │ (1~3 字节) │ (0/3/7/11 字节) │ (≤ chunkSize)│ └──────────────────┴──────────────────┴─────────────┘

3.1 Basic Header(1~3 字节)

第 1 字节 bit 布局:

0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ | fmt | csid | +-+-+-+-+-+-+-+-+ 2bit 6bit
  • fmt(2 bit):Message Header 类型,0~3

  • csid(6 bit):Chunk Stream ID,1~63

csid 三种编码:

csid 值字节数编码
2~631直接写在低 6 bit
64~3192低 6 bit 填 0,第 2 字节 = csid-64
320~655993低 6 bit 填 1,第 2/3 字节 = (csid-64) 小端

中文示例:

csid = 5 → 1 字节: 0x05 csid = 63 → 1 字节: 0x3F csid = 64 → 2 字节: 0x00 0x00 csid = 100 → 2 字节: 0x00 0x24 (100 - 64 = 36 = 0x24) csid = 320 → 3 字节: 0x01 0x00 0x01

常用 CSID:

CSID用途
2协议控制(SetChunkSize/Ack/WinSize)
3命令(connect/createStream/play)
4音频数据
5视频数据

3.2 Message Header(0/3/7/11 字节)

fmt 决定大小:

fmt大小包含字段
011timestamp + length + typeId + streamId
17timestamp delta + length + typeId
23timestamp delta
30无(完全继承)

fmt=0(11 字节)逐字段:

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | timestamp (3B, 大端) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | message length (3B, 大端) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | message type id (1B) | | +-+-+-+-+-+-+-+-+-+-+-+-+ + | message stream id (4B, ★小端) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段大小字节序含义
timestamp3大端绝对时间戳(毫秒),0xFFFFFF 用扩展
message length3大端消息体字节数
message type id1-8=音频, 9=视频, 20=命令
message stream id4小端Message Stream ID(唯一小端字段)

Extended Timestamp(4 字节):当 timestamp ==0xFFFFFF时追加。

fmt=3 特殊规则:如果上一个 Chunk 用了扩展时间戳,本 Chunk 也必须带。

3.3 ★ 完整分片示例(重点)

假设:300 字节视频帧,chunkSize=128,csid=5,streamId=1,timestamp=1000。

分片计算:

  • 第 1 片:128 字节

  • 第 2 片:128 字节

  • 第 3 片:44 字节

第 1 个 Chunk(fmt=0):

偏移 字节值 含义 ───────────────────────────────────── [0] 0x05 Basic Header: fmt=0, csid=5 [1] 0x00 timestamp 高位 [2] 0x03 timestamp 中位 [3] 0xE8 timestamp 低位 (1000 = 0x0003E8) [4] 0x00 length 高位 [5] 0x01 length 中位 [6] 0x2C length 低位 (300 = 0x00012C) [7] 0x09 type = 9 (MSG_VIDEO) [8] 0x01 streamId 小端第 1 字节 (1 = 0x00000001) [9] 0x00 streamId 第 2 字节 [10] 0x00 streamId 第 3 字节 [11] 0x00 streamId 第 4 字节 [12..139] 视频数据前 128 字节

十六进制拼接:

05 00 03 E8 00 01 2C 09 01 00 00 00 <128B 数据>

第 2 个 Chunk(fmt=3):

[0] 0xC5 Basic Header: fmt=3, csid=5 (0b11<<6|5 = 0xC5) [1..128] 视频数据第 129~256 字节

第 3 个 Chunk(fmt=3):

[0] 0xC5 Basic Header [1..44] 视频数据剩余 44 字节

核心结论:第一片带完整 11 字节头,后续片只带 1 字节 Basic Header。


四、RTMP 三次握手

4.1 握手流程

C0 (1B: 0x03) + C1 (1536B) → S0 (1B) + S1 (1536B) + S2 (1536B) C2 (1536B) → 完成

4.2 C0+C1(客户端 → 服务器)

实际字节(前 32 字节示例):

┌──────────────────────────────────────────────────────────────────┐ │ 03 ← C0: 版本号 0x03 │ ├──────────────────────────────────────────────────────────────────┤ │ 00 00 00 00 ← C1 前 4 字节:客户端时间戳(毫秒) │ │ 00 00 00 00 ← C1 第 5~8 字节:保留,必须为 0 │ │ XX XX XX XX XX XX XX XX ... ← C1 第 9~1536 字节:随机字节 │ │ ... │ └──────────────────────────────────────────────────────────────────┘

中文说明:

字段大小含义
C0.version1B固定 0x03
C1.timestamp4B客户端启动时间戳
C1.zero4B固定 0x00000000
C1.random1528B随机字节

⚠️ 关键:ZLMediaKit 会嗅探 C1 判断是否支持复杂握手。建议 C1 用全 0 或固定字节,避免随机数碰巧匹配:

QByteArray c1(1536, '\0'); // 全填 0 uint32_t now = QDateTime::currentMSecsSinceEpoch(); ByteOrder::set_be32(c1.data(), now); // 前 4 字节时间戳

4.3 S0+S1+S2(服务器 → 客户端)

实际字节:

03 ← S0: 版本号 0x03 00 00 00 00 ← S1 前 4 字节:服务器时间戳 00 00 00 00 ← S1 第 5~8 字节:保留零 A1 C2 3F 88 90 11 5E 7B ... ← S1 第 9~1536 字节:随机 ... ← 直到 1536 字节 <C1 的完整 1536 字节数据> ← S2: 服务器对 C1 的回显

总长度:1 + 1536 + 1536 = 3073 字节

4.4 C2(客户端 → 服务器)

简单握手:C2 = S1

<S1 的完整 1536 字节数据>

握手成功日志:

RtmpClient: handshake finished

五、AMF0 编解码

RTMP 命令消息用AMF0(Action Message Format 0)编码。

5.1 类型标记

值名称说明
0x00Number8 字节 double 大端
0x01Boolean1 字节
0x02String2 字节长度 + UTF-8
0x03Object键值对,00 00 09结尾
0x05Null无附加
0x08ECMA Array4 字节 count + 键值对
0x0AStrict Array4 字节 count + 元素

5.2 connect 命令的中文示例

客户端发送内容:

enc.writeString("connect"); enc.writeNumber(1.0); AmfValue obj = AmfValue::makeObject(); obj.set("app", AmfValue::makeString("live")); obj.set("tcUrl", AmfValue::makeString("rtmp://127.0.0.1:1935/live")); obj.set("flashVer", AmfValue::makeString("FMLE/3.0")); obj.set("audioCodecs", AmfValue::makeNumber(0x0400)); obj.set("videoCodecs", AmfValue::makeNumber(0x0080)); enc.writeObject(obj);

生成的字节流(逐字节中文注释):

02 00 07 63 6F 6E 6E 65 63 74 ← String "connect"(长度 7) 00 3F F0 00 00 00 00 00 00 ← Number 1.0(8B double 大端) 03 ← Object 开始 00 03 61 70 70 ← key "app" 02 00 04 6C 69 76 65 ← String "live" 00 05 74 63 55 72 6C ← key "tcUrl" 02 00 1D 72 74 6D 70 3A 2F 2F ... ← String "rtmp://127.0.0.1:1935/live" 00 08 66 6C 61 73 68 56 65 72 ← key "flashVer" 02 00 0C 46 4D 4C 45 2F 33 2E 30 ← String "FMLE/3.0" 00 0B 61 75 64 69 6F 43 6F 64 65 63 73 ← key "audioCodecs" 00 40 B0 00 00 00 00 00 00 ← Number 1024 (0x400 = AAC) 00 0B 76 69 64 65 6F 43 6F 64 65 63 73 ← key "videoCodecs" 00 40 80 00 00 00 00 00 00 ← Number 128 (0x80 = H.264) 00 00 09 ← 空 key + ObjectEnd

关键:

  • 02 00 07:String 类型 + 长度 7(2 字节大端)

  • 00 3F F0 ...:Number 类型 + double 大端(1.0 = 0x3FF0000000000000)

  • 00 00 09:Object 结束标记


六、完整信令流程

6.1 时序图

客户端 服务器 │ │ │──── TCP 连接 ──────────────────────────►│ │ │ │──── C0+C1 (1+1536B) ──────────────────►│ 握手 │◄─── S0+S1+S2 (1+1536+1536B) ───────────│ │──── C2 (= S1, 1536B) ────────────────►│ │ │ │──── connect (AMF0) ──────────────────►│ 建连 │◄─── _result (Success) ─────────────────│ │◄─── Window Ack Size ──────────────────│ │◄─── Set Peer Bandwidth ───────────────│ │ │ │──── createStream (AMF0) ─────────────►│ 建流 │◄─── _result (streamId=1) ─────────────│ │ │ │──── play "test" -2000 (AMF0) ────────►│ 播放 │◄─── onStatus (Play.Reset) ────────────│ │◄─── onStatus (Play.Start) ────────────│ │◄─── onStatus (Data.Start) ────────────│ │◄─── onMetaData ───────────────────────│ │◄─── Video Sequence Header (avcC) ─────│ │◄─── Audio Sequence Header (ASC) ──────│ │ │ │◄─── Video Frame (AVCC) ───────────────│ 数据 │◄─── Audio Frame (raw AAC) ────────────│ │◄─── ... 持续 ... │

6.2 connect 命令

客户端发(Chunk 头 + AMF0 数据):

03 ← Basic Header: fmt=0, csid=3 00 00 00 ← timestamp = 0 00 00 C0 ← length = 192 (0xC0) 14 ← type = 20 (MSG_CMD) 00 00 00 00 ← streamId = 0 (小端) <上面 5.2 节的 AMF0 数据>

服务器回:

text

03 00 00 00 00 00 B8 14 00 00 00 00 02 00 07 5F 72 65 73 75 6C 74 ← "_result" 00 3F F0 00 00 00 00 00 00 ← txn = 1 05 ← Null 03 ... 00 00 09 ← 服务器信息 03 ... 00 00 09 ← 结果信息 level = "status" code = "NetConnection.Connect.Success" ← ★ 成功标志

6.3 createStream 命令

客户端发:

03 00 00 00 00 00 19 14 00 00 00 00 02 00 0C 63 72 65 61 74 65 53 74 72 65 61 6D ← "createStream" 00 40 00 00 00 00 00 00 00 ← txn = 2 05 ← Null

服务器回:

03 00 00 00 00 00 15 14 00 00 00 00 02 00 07 "_result" 00 40 00 00 00 00 00 00 00 ← txn = 2 05 ← Null 00 3F F0 00 00 00 00 00 00 ← ★ streamId = 1

6.4 play 命令

客户端发:

03 00 00 00 00 00 20 14 01 00 00 00 ← ★ streamId = 1 (小端) 02 00 04 70 6C 61 79 ← "play" 00 40 08 00 00 00 00 00 00 ← txn = 3 05 ← Null 02 00 04 74 65 73 74 ← String "test" 00 C0 9F 40 00 00 00 00 00 ← Number -2000.0(负数=live)

6.5 服务器响应:三个 onStatus

02 00 08 6F 6E 53 74 61 74 75 73 ← "onStatus" 00 00 00 00 00 00 00 00 00 ← txn = 0 05 ← Null 03 ... 00 00 09 ← Object level = "status" code = "NetStream.Play.Start" ← ★ 播放开始

七、媒体描述信息

7.1 onMetaData 示例

03 00 00 00 00 01 06 12 00 00 00 00 02 00 0D 40 73 65 74 44 61 74 61 46 72 61 6D 65 ← "@setDataFrame" 02 00 0A 6F 6E 4D 65 74 61 44 61 74 61 ← "onMetaData" 08 ← ECMA Array 00 00 00 07 ← count = 7 00 08 64 75 72 61 74 69 6F 6E ← key "duration" 00 00 00 00 00 00 00 00 ← Number 0(直播) 00 05 77 69 64 74 68 ← key "width" 00 40 88 00 00 00 00 00 00 ← Number 1920 00 06 68 65 69 67 68 74 ← key "height" 00 40 87 00 00 00 00 00 00 ← Number 1080 00 09 66 72 61 6D 65 72 61 74 65 ← key "framerate" 00 40 39 00 00 00 00 00 00 ← Number 25.0 00 0C 76 69 64 65 6F 63 6F 64 65 63 69 64 ← key "videocodecid" 00 40 1C 00 00 00 00 00 00 ← Number 7 (H.264) 00 0C 61 75 64 69 6F 63 6F 64 65 63 69 64 ← key "audiocodecid" 00 40 24 00 00 00 00 00 00 ← Number 10 (AAC) 00 00 09 ← ECMA Array 结束

7.2 ★ Video Sequence Header(最重要的 config 帧)

05 ← csid=5 (视频通道) 00 00 00 ← timestamp = 0 00 00 2A ← length = 42 09 ← type = 9 (MSG_VIDEO) 01 00 00 00 ← streamId = 1 ├─────────────────────────────────────────────────────┤ │ 17 ← ★ Video Tag Header │ │ │ bit7-4 = 1 (key) │ │ │ bit3-0 = 7 (AVC) │ │ 00 ← AVCPacketType = 0 │ │ │ (0 = Sequence Header)│ │ 00 00 00 ← CompositionTime = 0 │ │ 01 ← ★ configurationVersion=1 │ │ 42 C0 1E ← profile/compat/level │ │ FF ← lengthSizeMinusOne=3 │ │ E1 ← numOfSPS = 1 │ │ 00 19 ← SPS 长度 = 25 │ │ 67 42 C0 1E D9 00 50 05 BB 01 10 00 00 03 00 10 00 00 03 03 C8 F1 22 6A 2C │ │ ← SPS 数据(25 字节) │ │ 01 ← numOfPPS = 1 │ │ 00 05 ← PPS 长度 = 5 │ │ 68 CE 3C 80 ← PPS 数据(5 字节) │ └─────────────────────────────────────────────────────┘

关键:data[0]必须是0x01(configurationVersion)。服务器会检查,推流端发 AnnexB 会被拒绝。

7.3 Audio Sequence Header

04 ← csid=4 (音频通道) 00 00 00 ← timestamp = 0 00 00 04 ← length = 4 08 ← type = 8 (MSG_AUDIO) 01 00 00 00 ← streamId = 1 ├─────────────────────────────────────────────┤ │ AF ← Audio Tag Header │ │ │ 10=AAC │ │ │ 3=44kHz │ │ │ 1=16bit │ │ │ 1=stereo │ │ 00 ← AACPacketType = 0 │ │ 12 10 ← AudioSpecificConfig │ └─────────────────────────────────────────────┘

ASC 解析(12 10=0001 0010 0001 0000):

audioObjectType = 2 (AAC-LC) samplingFreqIdx = 4 (44100 Hz) channelConfig = 2 (stereo)

八、★ AVCC → AnnexB 转换

8.1 两种格式对比

AVCC(RTMP 里):

[00 00 00 19] | 67 42 C0 1E ... ← 4 字节长度 + SPS [00 00 00 05] | 68 CE 3C 80 ... ← 4 字节长度 + PPS [00 00 0A B4] | 65 88 84 ... ← 4 字节长度 + IDR

AnnexB(RTSP / 裸流用):

00 00 00 01 | 67 42 C0 1E ... ← 起始码 + SPS 00 00 00 01 | 68 CE 3C 80 ... ← 起始码 + PPS 00 00 00 01 | 65 88 84 ... ← 起始码 + IDR

核心区别:起始码 ↔ 4 字节长度前缀(大端)。

8.2 转换代码

static QByteArray avccToAnnexB(const QByteArray& avcc, int naluLenSize) { QByteArray out; if (naluLenSize != 1 && naluLenSize != 2 && naluLenSize != 4) naluLenSize = 4; const uint8_t* p = reinterpret_cast<const uint8_t*>(avcc.constData()); int size = avcc.size(); int off = 0; static const uint8_t kStartCode[4] = {0,0,0,1}; while (off + naluLenSize <= size) { // 读长度字段(大端) uint32_t len = 0; if (naluLenSize == 1) len = p[off]; else if (naluLenSize == 2) len = (p[off] << 8) | p[off+1]; else len = (p[off] << 24) | (p[off+1] << 16) | (p[off+2] << 8) | p[off+3]; off += naluLenSize; if (off + int(len) > size) break; out.append((const char*)kStartCode, 4); // 加起始码 out.append((const char*)(p + off), len); // 加 NALU off += len; } return out; }

九、★ AVCDecoderConfigurationRecord 解析

9.1 结构

[0] configurationVersion = 0x01 [1] AVCProfileIndication = SPS[1] [2] profile_compatibility = SPS[2] [3] AVCLevelIndication = SPS[3] [4] 0xFF (reserved(6) | lengthSizeMinusOne(2)) [5] 0xE1 (reserved(3) | numOfSPS(5)) [6..7] SPS length (大端) [8..] SPS 数据 [...] 0x01 (numOfPPS=1) [...] PPS length [...] PPS 数据

9.2 解析代码

static QByteArray parseAvcConfig(const QByteArray& cfg, int& naluLenSize) { if (cfg.size() < 7) return QByteArray(); const uint8_t* p = reinterpret_cast<const uint8_t*>(cfg.constData()); // lengthSizeMinusOne 在 [4] 的低 2 bit naluLenSize = (p[4] & 0x03) + 1; int off = 5; int numSps = p[off++] & 0x1F; QByteArray out; static const uint8_t kStartCode[4] = {0,0,0,1}; // 解析 SPS for (int i = 0; i < numSps && off + 2 <= cfg.size(); ++i) { int len = (p[off] << 8) | p[off+1]; off += 2; if (off + len > cfg.size()) return out; out.append((const char*)kStartCode, 4); out.append((const char*)(p + off), len); off += len; } // 解析 PPS if (off >= cfg.size()) return out; int numPps = p[off++]; for (int i = 0; i < numPps && off + 2 <= cfg.size(); ++i) { int len = (p[off] << 8) | p[off+1]; off += 2; if (off + len > cfg.size()) return out; out.append((const char*)kStartCode, 4); out.append((const char*)(p + off), len); off += len; } return out; }

9.3 实际收到字节(示例)

17 ← FrameType=1(key) | Codec=7(AVC) 00 ← AVCPacketType=0 (Sequence Header) 00 00 00 ← CompositionTime=0 01 ← ★ configurationVersion=1 42 C0 1E ← profile/compat/level FF ← lengthSizeMinusOne=3 E1 ← numOfSPS=1 00 19 ← SPS 长度=25 67 42 C0 1E D9 ... ← SPS 数据(25B) 01 ← numOfPPS=1 00 05 ← PPS 长度=5 68 CE 3C 80 ← PPS 数据(5B)

转换后的 AnnexB:

00 00 00 01 | 67 42 C0 1E D9 ... ← 起始码 + SPS 00 00 00 01 | 68 CE 3C 80 ← 起始码 + PPS
现象原因修复
握手后立即断开C1 随机字节触发复杂握手C1 用固定字节
_result收不到connect 参数不合法检查 app/tcUrl
play 后无数据app/stream 拼错检查 URL
黑屏Sequence Header 没收到检查 avcType==0 分支
花屏AVCC → AnnexB 转换错检查长度前缀处理
音频无声ASC 没解析对从 ASC 取采样率
时间戳跳变RTMP 用毫秒dts_90k / 90

RTMP 拉流信令的四个阶段:

阶段命令关键点
握手C0/C1/C2 ↔ S0/S1/S2C1 别用随机字节
建连connect → _resultAMF0 编码,检查 Success
建流createStream → _result拿 streamId
播放play → onStatus收 Sequence Header

三层要点:

  1. Chunk 层:fmt=0 首片 + fmt=3 续片,streamId 小端

  2. AMF 层:二进制编码,String 2 字节长度,Number 8 字节 double

  3. 载荷层:AVCC ↔ AnnexB,avcC 首字节0x01

与推流端的镜像关系:

推流拉流
buildAvcC()拼 avcCparseAvcConfig()拆 avcC
annexBToAvcc()转 AVCCavccToAnnexB()转 AnnexB
sendVideoConfig()发handleVideo(avcType==0)收
sendVideoFrame()发 AVCChandleVideo(avcType==1)转 AnnexB

核心一句话:RTMP 拉流 =握手 + AMF0 信令 + Chunk 重组 + AVCC 转 AnnexB。四条主线和 RTSP 一一对应,只是每层都换了一套编码方式。把这三层理清,RTMP 拉流就不再是黑盒。

遇到协议问题,先看日志的字节偏移和字段名,几乎都能定位到具体层。

rtmp拉流

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 5:54:35

加密流量分析实战指南:工具、特征工程与模型选型

1. 加密流量分析为什么成了必选题做了几年网络流量方向的工作&#xff0c;我最直观的感受是&#xff1a;互联网已经在事实上“全面加密化”了。从浏览器到App&#xff0c;从Web服务到数据库连接&#xff0c;默认就是加密传输。以前做协议分析&#xff0c;抓个包直接在载荷里搜关…

作者头像 李华
网站建设 2026/10/21 5:54:13

单机商城用户系统实战:SQLite存储与权限控制详解

最近翻到一个购物商城项目的源码&#xff0c;越看越觉得有意思。这个项目完全单机运行、不联网&#xff0c;所有数据存储都靠SQLite一个文件搞定&#xff0c;却能实现完整的用户注册登录、商品浏览、购物车、下单结算这些功能。很多人一听到“单机”“SQLite”&#xff0c;就觉…

作者头像 李华
网站建设 2026/10/21 5:53:49

大模型自适应路由:多模型协同的动态决策系统

1. 什么是大模型自适应路由&#xff1a;不是调度器&#xff0c;也不是负载均衡&#xff0c;而是一套动态决策系统“大模型自适应路由”这八个字最近在技术圈高频出现&#xff0c;但很多人一听到就下意识联想到API网关、Nginx转发或K8s Service的流量分发——这是典型的认知错位…

作者头像 李华
网站建设 2026/10/21 5:52:52

中考物理模拟卷:暴露概念迁移与实验逻辑断层的诊断工具

简介&#xff1a;本资源是一套紧扣初中物理中考考纲的模拟试题&#xff0c;专为初三学生冲刺复习与教师命题参考设计&#xff0c;覆盖力学、热学、电磁学等核心模块&#xff0c;直击惯性应用、大气压测量误差、电路故障诊断、凸透镜成像规律、滑轮组机械效率、浮力与压强综合计…

作者头像 李华
网站建设 2026/10/21 5:52:48

Maven 4升级指南:核心变化、迁移实践与性能提升

1. 项目概述&#xff1a;Maven 4到底是个什么版本先说结论&#xff1a;Maven 4是Apache Maven项目自2008年左右Maven 3发布以来&#xff0c;最重要的一次大版本升级。它不只是把版本号加一&#xff0c;而是把整个构建引擎、依赖解析机制、配置模型都重新梳理了一遍。Maven 3到M…

作者头像 李华
网站建设 2026/10/21 5:52:25

机器学习实战笔记:数据泄漏、分布漂移与模型上线的坑

机器学习笔记这东西&#xff0c;我断断续续记了快五年。最早几本是上学时候的课堂摘抄&#xff0c;满页都是公式和推导&#xff0c;后来工作以后&#xff0c;笔记的风格就完全变了——不再是抄知识点&#xff0c;而是专门记录那些“看起来没问题&#xff0c;跑起来就出问题”的…

作者头像 李华