← ALL ARTICLES
游戏网络6 MIN READ

RPC

全库目录221
本文目录11

服务端执行了请求,响应却丢了,客户端无法由超时判断是否成功。RPC需要请求ID、权限和幂等边界,重试查询已有结果,避免一次加金币变成两次。

请求与执行语义

RPC 由 IDL/结构约束描述可调用消息,生成或手写 Stub 负责序列化、发送和分发。网络 RPC 不具有本地调用的可靠语义:它可能延迟、超时、重复执行,也可能在响应回来前连接断开。

每个请求应有请求ID、权限上下文和超时。自动重试只适合幂等操作,或服务端用唯一事务号去重。SetName(A) 可以设计成幂等,AddGold(100) 若重复执行就会出错。关键交易最好表达为“提交事务 X”,并保存处理结果。

客户端 RPC 只是请求,服务端必须重新验证位置、冷却、资源和身份。函数名叫 ServerDealDamage 并不会让传入伤害可信。

HTTP/HTTPS 适合登录、配置、商城和内容清单等控制面请求;高频状态同步通常使用长连接或数据报形成实时数据面。两类流量可以采用不同的超时、重试、扩缩容和观测策略。

TLS 在传输之上提供身份认证、机密性和完整性。客户端验证证书链与域名,握手协商密钥和算法,之后使用对称加密传输。TLS 不能替代应用权限检查,也不能防止已经取得合法会话的客户端发送作弊请求。

Mirror 等引擎网络库把 RPC、对象生成和 SyncVar 封装为组件接口。SyncVar 只负责同步声明的数据变化,不会自动解决所有权、预测、权限和带宽预算。使用框架时仍要查看实际发送频率、序列化字节和服务端验证路径。

消息分发与超时

一份请求封套保存协议版本、消息类型、请求ID、会话凭证、截止时间及有长度边界的载荷。接收端先验证封套和大小,再按版本选择结构解析器,解析后才分发到白名单处理函数;未知类型返回明确错误,不能把客户端字符串当作任意本地函数名。应答携带同一请求ID、结果或结构化错误。客户端待处理表以请求ID找到等待者,已结束请求的迟到应答直接丢弃;同一连接上的其他请求不受它影响。

截止时间先明确时钟域。客户端的绝对单调时刻不能直接交给服务器比较;本协议传递有上限的剩余处理预算,服务器接收时以自己的单调时钟建立截止点,依赖调用再扣除本地已用时间。客户端自己的等待截止独立生效,网络传输时间可能已经耗尽客户端预算,因此服务端未到截止并不保证客户端仍在等待。业务有效期由服务端订单或票据时间控制,与这份处理预算分别检查。

截止时间用于限制排队和依赖调用预算。处理器开始前发现已过期,可不执行并返回过期;执行中收到取消请求则在安全点检查。业务已经持久提交后,取消只能结束等待,不能撤销既有结果。客户端超时因此进入“结果未知”,用相同业务事务ID查询或重试,而不是换ID再提交。连接重建会换传输会话,已提交业务请求仍按账号与事务ID查询;旧连接包被拒绝,并不意味着已提交交易也被删除。

请求ID与业务事务ID可以相同,也可以分别记录,但映射必须稳定。传输重试沿用业务ID和载荷摘要,不能每次新建业务ID。去重窗口结束以后,服务端需要拒绝已过业务有效期的旧操作,而不是删除去重记录后重新接受:例如把交易有效期写入经服务端签发的交易票据,或只允许服务端已登记的未过期订单。这样有限去重缓存与合法操作寿命相互对应。

安全边界

握手阶段验证版本与身份,传输层按需要使用成熟的加密和认证协议。不要自创密码学。解析器限制包大小、频率和嵌套深度,网关按连接与账号做限流。错误日志避免回显令牌和敏感载荷。

UDP 需要额外防止地址伪造与放大攻击。建立会话前不要对小请求返回巨大数据,关键消息带会话验证信息。断线重连时旧连接和重放包也必须失效。

幂等请求

模型伪代码由服务端以账号与事务ID共同去重,结果记录与状态变化位于一致的事务边界。

text
validate session, schema and permission
begin serialized write transaction
if transaction already committed: validate payload; return stored result
apply validated state change
store result under transactionId
commit

并发重试的事务边界

在事务外查询“请求是否执行过”,两个并发重试可能同时查到空,然后各自增加一次余额。去重检查和状态写入必须由同一原子边界保护。单数据库实现可以用 (accountId,requestId) 唯一键,并在写事务内读取记录、修改状态和写入结果。

sql
BEGIN IMMEDIATE;
-- 在本写事务内查询requests;存在时核对载荷并返回保存结果。
-- 不存在时更新balance,并INSERT requests(account,id,payload,result)。
COMMIT;

这是 SQLite 事务机制片段。BEGIN IMMEDIATE 取得写事务资格,其他写者按超时策略等待或失败重试。完整实验的 credit 在同一事务内更新余额与结果;同ID不同金额拒绝,避免错误复用请求号。测试在更新后、记录前注入异常,重试仍只增加一次。

数据库提交与向外部服务发消息属于两个副作用。把待发送消息写进同一数据库事务的发件箱,再由发送器重试;接收方仍按消息ID幂等处理。事务不能自动把外部HTTP请求纳入原子提交。去重记录的保留时间也要覆盖允许重试的最长时间,过期会话的旧ID不能再次成为新操作。

发件箱的交付状态

一笔奖励同时需要更新余额并通知成就服务时,事务内写入余额、请求结果和outbox消息,三者一起提交。发送器只扫描已提交且未确认的消息。假设消息17已经被成就服务处理,但确认在网络中丢失,发送器仍会重发17;接收方以消息17为唯一键,把成就变化和接收记录放在同一事务中,第二次返回原结果。发送器收到确认后才标记完成。

发送前标记完成会在崩溃时丢通知;发送后标记完成则允许重复,因此需要接收方幂等。清理outbox之前,要确认完成状态耐久;接收方去重记录的寿命覆盖发送方允许的重试窗口。这里得到的是可重试交付与一次业务效果,传输本身仍可能发送多次。

去重与业务验证的顺序

重试查询与首次执行使用不同的状态判断。会话认证、账号归属和查询权限每次都验证;事务内先查稳定业务ID,存在时核对不可变载荷并返回保存结果,只有不存在时才检查余额、冷却和订单的当前有效期并执行。否则一次消费已扣完余额,响应丢失后重试会因“余额不足”失败,客户端便无法得到原来的成功结果。返回旧结果仍要求当前主体有读取该交易的权限,撤销会话不会变成越权查询入口。

有效期也分为执行与查询两种。过期订单禁止首次执行,已提交订单仍可按结果保留期查询。结果清理之后返回明确的“历史已归档/结果不可在线查询”,同时保留阻止再次执行的订单终态或关闭区间;不能把记录不存在解释成一个可重新执行的新请求。

发件箱同理:发送重试结束与接收去重退休需要约定消息寿命,先停止所有发送者的旧ID重试,再退休接收记录。

停止发送仍不能清除在途旧包,因此接收端还保存不可后退的关闭纪元或已关闭序号区间,并在查询去重表之前拒绝该范围的首次效果执行。另一种做法是在可信消息封套中给出执行到期时间,接收端按自己的可信时间拒绝过期首次执行;查历史结果仍沿独立查询入口。接收端只有确认旧ID即使迟到也不会重新执行,才删除逐条结果。

验证方法

在执行前后和响应前注入断线,重试应返回同一结果。测试权限、版本、非法长度及重复事务;TLS保护传输,不替代业务验证。

实现与证据

2026-10-22,以本机 Python 3.11 运行 python 知识库/examples/revision_16_21/test_reference.py,17项检查通过。参考实现与测试输入可复现本页注明的机制。 本页运行真实 SQLite 文件事务,覆盖重复ID、载荷冲突与提交前异常;没有进行多机消息投递或服务进程崩溃实验。

相关主题

参考资料