本章导读

XRT 的随机能力分四层,每层有自己的位置:Secure 层走操作系统熵源(xrtSecureRandom,密钥与令牌专用);全局 Rand 层是线程本地的默认选择;Fast 层优先速度、非安全;RNG 实例 层显式播种、完全可复现(xrng)。本章把四层的选型讲清楚,重点钉死一条红线:PCG 与 Fast 系列不是密码学安全的——令牌、密钥、nonce 必须走 Secure 层。固定种子的可复现性是另一个主题:测试快照与回放系统都建立在"同种子必得同序列"之上。

11.1引入

两个看似矛盾的诉求同时存在。做游戏与仿真时,你要随机数可复现——同一次对局、同一个存档读入,随机事件序列必须一致,否则录像回放与联机同步全部失效;做安全时,你要随机数不可预测——会话令牌被猜中等于账号被劫持。这两件事在工程上必须由不同的机制承担:前者用显式播种的伪随机数生成器(种子相同则序列相同),后者用操作系统熵源(xrtSecureRandom)。

常见的错误是把两者混用:拿时间做种子喂给一个快速生成器就去生成令牌——时间可猜、生成器可预测,两层保护等于零。XRT 用明确的命名切断这类错误:Rand/FastRand/Rng 一看就是统计用途,SecureRandom/SecureText 一看就是安全用途,模块文档同时写明"安全边界必须使用 Secure"。命名即文档——代码评审时不需要追到实现层,函数名本身就完成了安全审计的一半。

11.2概念

四层体系与选型

1SecurexrtSecureRandom / SecureText,操作系统熵源——密钥、令牌、nonce 唯一合法来源
2全局 RandxrtRand64 等,线程本地默认——业务抽样、演示、负载模拟
3FastxrtFastRandSeed 等,速度优先非安全——热路径抖动注入、图形噪声
4RNG 实例xrng + 播种,显式状态可复现——测试快照、录像回放、并行独立流

选型只问两个问题:要不可预测吗? 要 → 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——固定种子、无全局状态、输出完全可复现:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/math/random/main.c -lws2_32 -liphlpapiexplicit: 4207372542dice : 6real : 0.219369470187bytes : 8641shuffle : 2 1 6

刚才发生了什么。 ① 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,对四层各走一遍并断言可复现性与有界性:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/math/random_tour/main.c -lws2_32 -liphlpapirandom: global rand64/below/range/bytes/shuffle okrandom: fast (seeded) 32/64/below/range/real/shuffle okrandom: rng instance ready=1 below32/64/range/text okrandom: secure text from alphabet len=8

刚才发生了什么。 ① 全局层五连:同线程内直接调用,无状态声明。② Fast 层先 xrtFastRandSeed 再取值——快但非安全,头注释钉死了边界。③ RNG 实例层验证 RngReady 与有界版本;④ Secure 层从自定义字母表生成 8 字符令牌——长度断言通过即完成(内容当然每次不同)。整个程序用"同种子两次生成结果一致"验证可复现性、用范围断言验证有界性——这也是你写自己的随机测试时该抄的两类断言。

11.4契约

  • 安全红线:密钥、令牌、密码、nonce、临时资源名等安全边界必须用 xrtSecureRandom / SecureText;Rng/Rand/FastRand 全部是统计用途,文档与命名双重声明。
  • 可复现契约:同种子(含流序号)同调用序列 → 同输出,跨平台跨版本稳定(xrtMath 的一致性在底层支撑)。
  • 独立流:多实例用不同流序号播种即互相独立;同种子同流 = 完全相关的复制——并行仿真前先检查播种行。
  • 线程:全局 Rand 线程本地,无锁并发;RNG 实例归属声明它的作用域,跨线程共享需要自己的同步。
  • 区间语义:RngRangeClosed 双闭(含两端);自写 x % n 有取模偏差,不要用。
  • 顺序即状态:复现一段序列时,生成调用的种类与次序必须与录制时完全一致。

常见错误

坑 1:用统计随机数生成安全令牌

症状:安全审计标记令牌可预测;理论上攻击者观测少量输出即可重建内部状态,后续令牌全部可算。

原因: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 章) */
坑 2:两个 RNG 播同一种子

症状:并行仿真里多个"独立"随机流高度相关——卡组顺序雷同、负载模式同步,统计结论失真。

原因:同种子同流序号产生完全相同的序列;"多实例"没有自带"多序列"。

错误写法
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 个条目)

点击查看完整签名、参数约束、返回值与错误说明