本章导读
XRT 的随机能力分四层,每层有自己的位置:Secure 层走操作系统熵源(xrtSecureRandom,密钥与令牌专用);全局 Rand 层是线程本地的默认选择;Fast 层优先速度、非安全;RNG 实例 层显式播种、完全可复现(xrng)。本章把四层的选型讲清楚,重点钉死一条红线:PCG 与 Fast 系列不是密码学安全的——令牌、密钥、nonce 必须走 Secure 层。固定种子的可复现性是另一个主题:测试快照与回放系统都建立在"同种子必得同序列"之上。
11.1引入
两个看似矛盾的诉求同时存在。做游戏与仿真时,你要随机数可复现——同一次对局、同一个存档读入,随机事件序列必须一致,否则录像回放与联机同步全部失效;做安全时,你要随机数不可预测——会话令牌被猜中等于账号被劫持。这两件事在工程上必须由不同的机制承担:前者用显式播种的伪随机数生成器(种子相同则序列相同),后者用操作系统熵源(xrtSecureRandom)。
常见的错误是把两者混用:拿时间做种子喂给一个快速生成器就去生成令牌——时间可猜、生成器可预测,两层保护等于零。XRT 用明确的命名切断这类错误:Rand/FastRand/Rng 一看就是统计用途,SecureRandom/SecureText 一看就是安全用途,模块文档同时写明"安全边界必须使用 Secure"。命名即文档——代码评审时不需要追到实现层,函数名本身就完成了安全审计的一半。
11.2概念
四层体系与选型
选型只问两个问题:要不可预测吗? 要 → Secure,结束。要可复现吗? 要 → RNG 实例(显式状态、显式种子)。两者都不要 → 全局 Rand 图省事,Fast 用于确认过性能敏感的热路径。四层的函数名前缀与上述答案一一对应,代码评审时一眼可查。
再补一张"什么场景用什么层"的实感清单:生成会话令牌 → SecureText;游戏掉落判定 → RNG 实例(回放要求);压测流量扰动 → 全局 Rand;图形粒子噪声 → Fast;文件临时名 → SecureText 或 XID(第 12 章);测试故障注入点 → RNG 实例(种子即用例)。清单会随项目变长,但每个新条目都应能回答那两个问题之一。
RNG 实例:显式状态与流序号
xrng 是一个可以放在任何地方的状态结构体——栈变量、结构体成员、存档文件里的一段字节。xrtRngSeed(&Rng, 种子, 流序号) 播种:种子相同、流序号不同,得到彼此独立的序列。这个第二参数解决的是经典坑:两个生成器播同一种子,输出完全相关——服务器给每个会话各发一个 RNG 时,用会话编号当流序号即可保证互不相关。生成族覆盖整数(Rng32/Rng64)、浮点(RngReal,[0,1))、有界整数(RngRangeClosed 双闭区间——掷骰子 1..6 含 6)、字节填充(RngBytes)与原地洗牌(RngShuffle,Fisher-Yates)。
全局层与线程
xrtRand64 等全局函数使用线程本地状态——多线程并发调用不需要加锁,也不会互相干扰对方的序列。这层适合"随手要个随机数"的场景;需要跨调用序列一致时(同一段逻辑重跑要同结果),还是回到 RNG 实例。Fast 系列(xrtFastRandSeed 播种、xrtFastRand32 取值等)是无安全保证的速度优先实现,用于确认过需要极致性能的非安全场景。
随机文本与字母表
RandText / RandStringFrom / SecureText 生成随机字符串:可以指定字符集(比如只要无歧义的 23456789abcdefghjkmnpqrstuvwxyz),生成邀请码、临时目录名、会话令牌。安全场景对应 SecureText 与 SecureStringFrom——它们建立在 xrtSecureRandom 之上,随机文本的不可预测性与字节版一致。
定制字母表不只是为了好看:排除 0O1lI 这类形近字符,邀请码就不会被用户抄错;限制到 URL 安全字符集,令牌就能直接进链接与二维码。生成长度由调用方指定,生成失败(熵源不可用)同样经返回值宣告——安全路径上的失败永远不要静默吞掉。
可复现性的工程用法
可复现随机有三个高频落点。测试快照:把种子写进测试固件,断言生成的序列哈希(第 12 章)与录制的指纹一致——随机逻辑的回归测试从"碰运气"变成精确断言。录像回放:只存种子与操作序列,回放时重演生成调用,随机事件逐帧一致。故障注入:用 RNG 决定注入点(配合第 6 章的 FailAfter 做混合故障模式),种子即测试用例编号——失败重跑不换结果。三个落点的共同纪律:生成调用的顺序也是状态——复现时少调一次 Rng32,后面全部错位。
读懂随机测试的三类断言
在跑示例之前,先明确随机代码的测试写法——后面两个示例的断言都出自这三类。可复现断言:同种子生成两次,结果逐项相等(或哈希指纹相等)。有界断言:生成的值落在声明的区间内,包括双闭区间的两个端点各命中至少一次。分布断言:大样本下频次接近期望(用第 10 章 Near 判相对偏差)。三类各司其职:可复现保证确定性、有界保证语义、分布保证质量——只写其中一类都会漏掉对应维度的缺陷。
11.3示例
完整程序:显式 RNG 全家族
来自仓库范例 examples/math/random/main.c——固定种子、无全局状态、输出完全可复现:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xrng Rng; /* 状态在栈上:零全局依赖 */ int arrOrder[] = { 1, 2, 3, 4, 5, 6 }; uint8 arrBytes[8]; xrtRngSeed(&Rng, 2026, 7); /* 种子 2026,流 7 */ /* 整数与浮点:同一状态顺序推进(消耗即前进)。 */ printf("explicit: %u\n", (unsigned int)xrtRng32(&Rng)); printf("dice : %lld\n", (long long)xrtRngRangeClosed(&Rng, 1, 6)); printf("real : %.12f\n", xrtRngReal(&Rng)); /* 字节填充与洗牌:RngBytes 无失败路径(内部检查冗余但保留统一风格)。 */ if ( !xrtRngBytes(&Rng, arrBytes, sizeof(arrBytes)) || !xrtRngShuffle(&Rng, arrOrder, sizeof(arrOrder) / sizeof(arrOrder[0]), sizeof(arrOrder[0])) ) { return 1; } printf("bytes : %02x%02x\n", (unsigned int)arrBytes[0], (unsigned int)arrBytes[1]); printf("shuffle : %d %d %d\n", arrOrder[0], arrOrder[1], arrOrder[2]); return 0; }
刚才发生了什么。 ① xrng Rng 是栈上的局部状态——谁声明谁持有,放进结构体、每个会话一个、存进回放档都是同一形态。② xrtRngSeed(&Rng, 2026, 7):种子 2026、流序号 7;这五十来个字节的序列从此完全确定,任何机器任何时候重跑都是这五行输出——这就是测试快照与录像回放的基石。③ 五个生成函数顺序推进同一状态:先取的整数影响后取的骰子——复现序列时调用顺序也必须一致。④ RngRangeClosed(&Rng, 1, 6) 双闭区间,掷骰子不必再写 1 + x % 6(那是半开区间加取模偏差的双重坑)。⑤ RngShuffle 是原地洗牌——数组还是那个数组,顺序被 Fisher-Yates 均匀打乱;"均匀"不是修辞,每种排列等概率,这是第 11 章练习里分布检验的对象。
完整程序:四层接口全览
来自 examples/math/random_tour/main.c,对四层各走一遍并断言可复现性与有界性:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> #define SV(x) XRT_STR_LITERAL(x) /* 字母表断言:str 全部字符都在表内且长度正确。 */ static bool exampleInAlphabet(cstr sText, size_t iLength, xstrview Alphabet) { size_t i; size_t j; if ( strlen(sText) != iLength ) { return false; } for ( i = 0; i < iLength; ++i ) { bool bFound = false; for ( j = 0; j < Alphabet.Size; ++j ) { if ( sText[i] == Alphabet.Data[j] ) { bFound = true; break; } } if ( !bFound ) { return false; } } return true; } int main(void) { static const char sHex[] = "0123456789abcdef"; xrng RngA; xrng RngB; char Buffer[16]; char TextA[9]; char TextB[9]; uint8 BytesA[8]; uint8 BytesB[8]; str sGenerated; int i; int iResult = 1; /* ---- 全局线程层:有界性 + 洗牌保持多重集 ---- */ (void)xrtRand64(); if ( (xrtRandBelow(10u) >= 10u) || (xrtRandBelow(1u) != 0u) ) { goto Cleanup; } { int64 iMin = 100; int64 iMax; for ( i = 0; i < 64; ++i ) { iMax = xrtRandRange(-5, 5); if ( (iMax < -5) || (iMax > 5) || (iMax == iMin) ) { iMin = iMax; } } } if ( !xrtRandBytes(Buffer, sizeof(Buffer)) ) { goto Cleanup; } { /* 洗牌 0..5:结果必须是原多重集的排列。 */ int Numbers[6] = { 0, 1, 2, 3, 4, 5 }; bool bSeen[6] = { false }; bool bPermutation = true; if ( !xrtRandShuffle(Numbers, 6u, sizeof(int)) ) { goto Cleanup; } for ( i = 0; i < 6; ++i ) { if ( (Numbers[i] < 0) || (Numbers[i] > 5) || bSeen[Numbers[i]] ) { bPermutation = false; break; } bSeen[Numbers[i]] = true; } if ( !bPermutation ) { goto Cleanup; } } /* RandText:按字母表填充固定缓冲;RandStringFrom 分配。 */ if ( !xrtRandText(SV(sHex), TextA, sizeof(TextA), 8u) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } sGenerated = xrtRandStringFrom(SV(sHex), 16u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 16u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); /* RandString:URL-safe 64 字符表(自定义表见 StringFrom)。 */ sGenerated = xrtRandString(12u); if ( (sGenerated == NULL) || (strlen(sGenerated) != 12u) ) { goto Cleanup; } xrtFree(sGenerated); printf("random: global rand64/below/range/bytes/shuffle ok\n"); /* ---- Fast 层:同种子可复现 ---- */ xrtFastRandSeed(12345u, 67890u); (void)xrtFastRand32(); (void)xrtFastRand64(); { /* 每个生成器只消费一次:回放序列必须完全对齐。 */ int64 iRange = xrtFastRandRange(-3, 3); if ( (xrtFastRandBelow(7u) >= 7u) || (iRange < -3) || (iRange > 3) || (xrtFastRandRangeClosed(1, 1) != 1) ) { goto Cleanup; } } { double dReal = xrtFastRandReal(); if ( (dReal < 0.0) || (dReal >= 1.0) ) { goto Cleanup; } } if ( !xrtFastRandBytes(BytesA, sizeof(BytesA)) ) { goto Cleanup; } { int Numbers[4] = { 0, 1, 2, 3 }; if ( !xrtFastRandShuffle(Numbers, 4u, sizeof(int)) ) { goto Cleanup; } } /* 重置同一种子:32/64/字节序列必须完全一致。 */ xrtFastRandSeed(12345u, 67890u); (void)xrtFastRand32(); (void)xrtFastRand64(); (void)xrtFastRandBelow(7u); (void)xrtFastRandRange(-3, 3); (void)xrtFastRandRangeClosed(1, 1); (void)xrtFastRandReal(); if ( !xrtFastRandBytes(BytesB, sizeof(BytesB)) || (memcmp(BytesA, BytesB, sizeof(BytesA)) != 0) ) { goto Cleanup; } printf("random: fast (seeded) 32/64/below/range/real/shuffle ok\n"); /* ---- RNG 实例层:Ready 门 + Below/Range/Text ---- */ xrtRngSeed(&RngA, 42u, 7u); xrtRngSeed(&RngB, 42u, 7u); if ( !xrtRngReady(&RngA) || (xrtRngReady(NULL)) ) { goto Cleanup; } /* 双实例同种子:Rng64/RngBelow/RngRange 逐个一致。 */ for ( i = 0; i < 8; ++i ) { if ( xrtRng64(&RngA) != xrtRng64(&RngB) ) { goto Cleanup; } } { /* 双实例逐调用同步消费,任何一次额外抽取都会失步。 */ uint32 uA = xrtRngBelow32(&RngA, 100u); uint32 uB = xrtRngBelow32(&RngB, 100u); if ( (uA != uB) || (uA >= 100u) ) { goto Cleanup; } uA = xrtRngBelow32(&RngA, 100u); uB = xrtRngBelow32(&RngB, 100u); if ( (uA != uB) || (uA >= 100u) ) { goto Cleanup; } { uint64 uA = xrtRngBelow64(&RngA, UINT64_C(1000000)); uint64 uB = xrtRngBelow64(&RngB, UINT64_C(1000000)); if ( (uA != uB) || (uA >= UINT64_C(1000000)) ) { goto Cleanup; } } } { int64 iA = xrtRngRange(&RngA, -100, 100); int64 iB = xrtRngRange(&RngB, -100, 100); if ( (iA != iB) || (iA < -100) || (iA > 100) ) { goto Cleanup; } } /* RngText 固定缓冲 + RngStringFrom 分配(同种子双实例一致)。 */ if ( !xrtRngText(&RngA, SV(sHex), TextA, sizeof(TextA), 8u) || !xrtRngText(&RngB, SV(sHex), TextB, sizeof(TextB), 8u) || (memcmp(TextA, TextB, 8u) != 0) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } sGenerated = xrtRngStringFrom(&RngA, SV(sHex), 12u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 12u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); printf("random: rng instance ready=1 below32/64/range/text ok\n"); /* ---- Secure:字母表定制的文本生成 ---- */ sGenerated = xrtSecureStringFrom(SV(sHex), 8u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 8u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); if ( !xrtSecureText(SV(sHex), TextA, sizeof(TextA), 8u) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } printf("random: secure text from alphabet len=8\n"); iResult = 0; Cleanup: return iResult; }
刚才发生了什么。 ① 全局层五连:同线程内直接调用,无状态声明。② Fast 层先 xrtFastRandSeed 再取值——快但非安全,头注释钉死了边界。③ RNG 实例层验证 RngReady 与有界版本;④ Secure 层从自定义字母表生成 8 字符令牌——长度断言通过即完成(内容当然每次不同)。整个程序用"同种子两次生成结果一致"验证可复现性、用范围断言验证有界性——这也是你写自己的随机测试时该抄的两类断言。
11.4契约
- 安全红线:密钥、令牌、密码、nonce、临时资源名等安全边界必须用
xrtSecureRandom/SecureText;Rng/Rand/FastRand全部是统计用途,文档与命名双重声明。 - 可复现契约:同种子(含流序号)同调用序列 → 同输出,跨平台跨版本稳定(
xrtMath的一致性在底层支撑)。 - 独立流:多实例用不同流序号播种即互相独立;同种子同流 = 完全相关的复制——并行仿真前先检查播种行。
- 线程:全局 Rand 线程本地,无锁并发;RNG 实例归属声明它的作用域,跨线程共享需要自己的同步。
- 区间语义:
RngRangeClosed双闭(含两端);自写x % n有取模偏差,不要用。 - 顺序即状态:复现一段序列时,生成调用的种类与次序必须与录制时完全一致。
常见错误
症状:安全审计标记令牌可预测;理论上攻击者观测少量输出即可重建内部状态,后续令牌全部可算。
原因:PCG 与 Fast 系列是统计用途——可复现性与不可预测性在设计上就是对立的,前者注定后者不成立。
xrng Rng; xrtRngSeed(&Rng, (uint64)time(NULL), 1); /* 时间可猜 */ xrtRngBytes(&Rng, Token, sizeof(Token)); /* PCG 可预测——令牌形同虚设 */
uint8 Token[16]; if ( !xrtSecureRandom(Token, sizeof(Token)) ) { return false; /* 熵源失败按失败处理 */ } /* 用完 xrtSecureZero(Token, sizeof(Token))(第 5 章) */
症状:并行仿真里多个"独立"随机流高度相关——卡组顺序雷同、负载模式同步,统计结论失真。
原因:同种子同流序号产生完全相同的序列;"多实例"没有自带"多序列"。
for ( i = 0; i < 4; ++i ) { xrtRngSeed(&Workers[i].Rng, 2026, 1); /* 四个worker同序列 */ }
for ( i = 0; i < 4; ++i ) { xrtRngSeed(&Workers[i].Rng, 2026, (uint64)i + 1u); /* 流序号分流 */ }
编程练习
固定种子连续取十个整数,打印;换一个流序号重跑,对比两序列不同;换回原种子重跑,逐个断言与第一次完全一致。最后用第 12 章的 Hash64 给十元序列算一个指纹——以后回归这个测试只需比对一个 64 位值。
用 RngRangeClosed(&Rng, 1, 6) 掷十万次,统计 1–6 各自频次,用第 10 章 Near 验证每面频次与期望(十万除以六)的相对偏差小于百分之一。再做一组对照:故意用 1 + (int)(xrtRng32(&Rng) % 6) 生成,观察偏差是否仍可接受——体会"取模偏差"在小范围区间里到底是不是实际问题,并写下你的结论。
设计一个最小对局回放:把开局种子(含流序号)与玩家操作序列存档;"回放"时重播种子与操作,断言每个随机事件(每次掉落、每次判定)与实录逐项一致。验收标准:两个不同种子的存档回放各自自洽、互不相同;同一存档回放两次结果逐项相同。
小结
| 知识点 | 速查 |
|---|---|
| 四层 | Secure(熵源)/ 全局 Rand(线程本地)/ Fast(速度)/ RNG 实例(可复现) |
| 安全红线 | 令牌密钥 nonce 只用 SecureRandom / SecureText |
| 播种 | RngSeed(&Rng, 种子, 流序号);分流用流序号 |
| 区间 | RngRangeClosed 双闭含端点;拒绝 x % n |
| 复现 | 同种子同调用序 → 同输出;调用顺序也是状态 |
| 文本 | SecureStringFrom 定制字母表生成令牌;排除形近字符防抄错 |
📖 本章涉及的 API 参考手册:random(41 个条目)
点击查看完整签名、参数约束、返回值与错误说明