问题
基于Node.js编写了一个简易信令服务器,需要UE客户端上报本地创建的玩家存档信息。UE客户端使用FGuid为存档生成唯一标识符,但发送给服务端后无法通过Zod库的校验,认为UUID格式不合法。
const helloMessageSchema = z.object({
type: z.literal("hello"),
protocolVersion: z.literal(1),
profile: z.object({
profileId: z.uuid(), //解析profileId时报错
displayName: z.string().min(1).max(20),
}),
});
Zod是什么
Zod 是 TypeScript/JavaScript 的运行时数据校验库。
TypeScript 类型在编译后会被删除,因此网络收到的 JSON 仍然需要在运行时验证。
Zod基于
RFC UUID规则校验UUID格式是否合法。
调用类似:
const result = helloMessageSchema.safeParse(message);
- 校验成功:
result.success === true - 校验失败:返回具体的字段路径和原因
RFC UUID 与 UE FGuid
| 对比项 | RFC UUID | UE FGuid |
|---|---|---|
| 大小 | 128 位 | 128 位 |
| 常见文本外形 | 8-4-4-4-12 |
也支持 8-4-4-4-12 |
| 版本位 | 有明确要求 | FGuid自身不校验 |
| Variant 位 | 有明确要求 | FGuid自身不校验 |
| 主要用途 | 跨系统标准标识 | UE 内部对象、资源和实例标识 |
一个RFC UUID v4通常是:
xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
其中:
- 第三段首次字符必须是
4,表示UUID v4。 - 第四段首次字符必须是
8、9、a或b,表示RFC Variant。 例如:
085e4c96-49af-4b4e-97a9-f0bfbc8e1fc7
↑ ↑
版本4 Variant
UE可能生成的UUID是:
085e4c96-49af-9b4e-97a9-f0bfbc8e1fc7
↑
不是合法版本位
为什么UE会这样
Windows下FGuid::NewGuid()底层调用CoCreateGuid(),但FGuid内部按照四个uint32存储:
uint32 A;
uint32 B;
uint32 C;
uint32 D;
在以字符串形式发送UUID时,FGuid::ToString()按照UE自己的字段组合方式输出,这个排列和RFC UUID的规则不完全一致,导致版本位在UE字符串中可能不处于RFC要求的位置。
此外,FGuid::Parse()通常只检查:
- 长度是否正确。
- 连字符位置是否正确。
- 是否全部为十六进制字符。
不会要求第三段必须包含合法的UUID版本号。
因此:
UE
FGuid字符串通常具有UUID的外形,但不保证通过严格的RFC UUID校验。
解决方法
服务端业务只需要上报的profileId具有UUID外形、唯一且稳定即可,不是非要严格符合RFC UUID。在解析时只需校验“GUID文本格式”,不使用语义更严格的z.uuid()。
// Unreal Engine 的 FGuid::ToString(DigitsWithHyphensLower) 保证 GUID 的
// 8-4-4-4-12 十六进制外形,但不保证 RFC UUID 的版本位和 Variant 位。
// profileId 在本项目中是匿名玩家的稳定标识,因此校验外形即可。
const profileIdSchema = z
.string()
.trim()
.regex(
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/u,
"profileId 必须是 GUID 格式",
);
const helloMessageSchema = z.object({
type: z.literal("hello"),
protocolVersion: z.literal(1),
profile: z.object({
profileId: profileIdSchema, //确保具有UUID外形即可
displayName: z.string().min(1).max(20),
}),
});
Discussion
留言讨论
发表留言时,评论服务会记录必要的 IP 与 User-Agent 信息,用于安全审核和防止滥用。