WECHAT

扫码添加微信

YoungFan 微信二维码
← 返回 UE5 笔记
踩坑记录已完成

FGuid与RFC UUID的区别

UE FGuid生成的UUID无法通过Node.js Zod库的数据校验。

UE5Node.jsFGuidRFC UUID

问题

基于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 信息,用于安全审核和防止滥用。