Unity 游戏框架中的网络通信设计与实践
在很多 Unity 项目中,网络通信一开始看起来并不复杂。
客户端连接服务器,发送一条消息,服务器返回一条消息,然后根据返回结果刷新界面。
对于小 Demo 来说,这样就够了。
但是一旦项目变成长期维护的商业项目,尤其是 RPG、MMO、SLG、模拟经营 这类系统比较多的游戏,网络通信就会变成整个项目中非常核心的一层。
因为很多游戏逻辑最终都会落到网络通信上:
-
登录
-
角色数据
-
背包数据
-
战斗请求
-
技能释放
-
聊天
-
好友
-
邮件
-
商城
-
任务
-
活动
-
多人同步
-
状态更新
如果网络层设计得不好,项目后期会非常痛苦。
所以在 MyFramework 中,我没有把网络通信只当成一个简单的 Socket 封装,而是把它作为客户端和服务器协作工具链的一部分来设计。
项目地址:
https://github.com/ZHOURUIH/MyFramework
配套服务器框架:
https://github.com/ZHOURUIH/MyServerFramework
这篇文章主要聊一下:
-
为什么 Unity 项目需要自己的网络通信层
-
网络层真正难的地方是什么
-
协议设计为什么重要
-
为什么要做消息代码生成
-
客户端和服务器如何保持协议一致
-
网络通信如何和框架其他模块配合
-
一个长期项目中的网络层应该关注哪些问题
一、为什么要自己封装网络层
很多项目刚开始做网络通信时,可能只是简单封装一下 Socket。
比如:
-
连接服务器
-
发送字节流
-
接收字节流
-
解析消息
-
回调业务逻辑
从技术上看,这些事情并不复杂。
但真实项目中的网络层很少这么简单。
一个长期维护的游戏项目,网络层至少要考虑这些问题:
-
消息协议如何定义
-
消息 ID 如何管理
-
客户端和服务器字段如何保持一致
-
消息如何序列化和反序列化
-
收到消息后如何派发到对应模块
-
网络断开后如何重连
-
消息异常时如何定位
-
协议修改后如何避免遗漏
-
是否支持不同通信方式
-
是否方便调试和扩展
如果这些问题都散落在业务代码里,后期一定会很难维护。
所以我认为网络层不应该只是“能发消息”,还应该是一个稳定的工程基础设施。
MyFramework 中的网络模块主要目标不是封装一个 Socket,而是让客户端和服务器通信变得更可控、更统一、更容易维护。
二、网络层最重要的是协议一致性
客户端和服务器通信时,最怕的不是连接失败,而是双方对同一条消息的理解不一致。
比如:
客户端认为某条消息里有这些字段:
-
playerID
-
itemID
-
count
但服务器认为这条消息里是:
-
playerID
-
count
-
itemID
字段顺序不一致,就可能导致反序列化错误。
再比如客户端新增了一个字段,但服务器没有同步修改。
或者服务器修改了字段类型,客户端仍然使用旧类型。
这些问题有时候不会立刻崩溃,而是产生非常奇怪的逻辑错误。
所以网络层首先要解决的问题是:
客户端和服务器必须基于同一份协议定义。
这也是我做协议代码生成的重要原因。
如果客户端和服务器各自手写消息类,长期维护时很容易不一致。
而如果协议结构由工具统一生成,就可以大幅减少这类错误。
三、为什么要做消息代码生成
在游戏项目中,网络消息数量会越来越多。
刚开始可能只有几条:
-
登录请求
-
登录返回
-
心跳
-
拉取角色数据
后面会越来越多:
-
背包消息
-
技能消息
-
战斗消息
-
任务消息
-
商城消息
-
邮件消息
-
好友消息
-
聊天消息
-
活动消息
-
多人同步消息
如果每条消息都手写,问题会越来越明显。
1. 重复代码太多
每条消息通常都需要:
-
定义字段
-
写序列化
-
写反序列化
-
写消息 ID
-
写注册逻辑
-
写客户端类
-
写服务器类
这些代码大部分都是结构性代码。
它们本身没有太多业务含义,但非常容易写错。
所以这类代码非常适合生成。
2. 修改协议容易漏改
如果协议字段变化,需要同时修改客户端和服务器。
如果是手写代码,就很容易出现只改了一端的情况。
例如:
-
客户端字段改了,服务器没改
-
服务器字段顺序改了,客户端没改
-
客户端类型改了,服务器仍然是旧类型
-
注册表里忘记添加新消息
通过协议生成工具,可以让这些结构性代码由工具统一生成,减少人工同步错误。
3. 消息注册可以自动化
网络消息通常需要根据消息 ID 创建对应消息对象。
如果注册逻辑全部手写,很容易漏注册。
所以消息注册代码也适合生成。
例如协议工具可以根据消息定义自动生成:
-
消息 ID 常量
-
消息类
-
消息注册函数
-
客户端消息表
-
服务器消息表
这样新增消息后,只需要重新生成代码即可。
4. 手写逻辑应该保留
代码生成并不是要覆盖所有逻辑。
网络消息中仍然会有一些手写逻辑,例如:
-
收到消息后的业务处理
-
消息发送前的数据组织
-
特殊日志
-
调试辅助
-
某些模块自己的封装接口
所以工具生成的应该是结构性代码,而不是业务逻辑。
MyFramework 的整体思路一直是:
工具生成重复结构,程序编写业务逻辑。
这点在 UI 代码生成、配置表代码生成、协议代码生成中都是一致的。
四、消息收发流程应该清晰
网络通信的流程必须清晰。
否则一旦出现问题,很难定位消息到底卡在哪一步。
一个比较清晰的流程应该是:
-
业务模块发起请求
-
创建对应消息对象
-
写入字段
-
序列化成字节数据
-
通过网络连接发送
-
服务器接收并解析
-
服务器处理逻辑
-
服务器返回消息
-
客户端接收字节数据
-
反序列化成消息对象
-
根据消息 ID 派发给对应模块
-
业务模块刷新状态或界面
这里面每一步都可能出问题。
例如:
-
消息没有发送出去
-
连接已经断开
-
消息 ID 不存在
-
字段解析失败
-
收到消息但没有注册处理函数
-
业务处理顺序不对
-
UI 已经关闭但消息才回来
所以网络层需要提供清晰的收发流程,而不是让业务代码到处直接操作 Socket。
在 MyFramework 中,网络层会尽量把底层通信、消息解析、消息注册、消息派发这些流程统一管理。
业务层只关心:
我要发送什么消息。
我收到什么消息后该做什么。
而不是关心底层字节流如何处理。
五、网络通信不能只考虑正常情况
很多网络层 Demo 只演示正常情况。
比如连接成功、发送成功、收到返回。
但真实项目中,异常情况非常多。
例如:
-
网络断开
-
服务器主动断开
-
连接超时
-
消息超时
-
服务器返回错误码
-
客户端切后台
-
弱网环境
-
重连后状态不一致
-
重复收到消息
-
消息顺序异常
-
玩家在切场景时收到旧消息
这些情况如果不提前设计,后期会很难处理。
所以网络层必须考虑容错。
1. 断线和重连
移动端项目中,断线是很常见的。
玩家切后台、网络波动、Wi-Fi 和移动网络切换,都可能导致连接断开。
网络层需要明确:
-
什么情况下认为连接断开
-
是否自动重连
-
重连期间是否阻塞操作
-
重连成功后是否需要重新拉取数据
-
失败后如何提示玩家
-
是否需要回到登录界面
这些逻辑不应该散落在每个业务系统里,而应该由统一的网络状态管理来处理。
2. 消息超时
有些请求发出去以后,如果长时间没有收到响应,就应该认为超时。
比如登录、支付、关键操作等。
消息超时需要处理得很谨慎。
不能所有消息都简单等待,也不能所有消息都无限等待。
不同类型的消息可以有不同策略。
3. 错误码处理
服务器返回错误码时,客户端不能到处写重复判断。
比较好的方式是统一处理通用错误,再把特殊错误交给业务模块。
例如:
-
登录失效
-
资源不足
-
道具不存在
-
操作太频繁
-
服务器维护
-
版本不一致
这些错误需要有统一入口,否则项目越大,错误处理代码越混乱。
六、为什么网络层要和框架其他模块配合
网络层不是孤立存在的。
它会和很多系统发生关系。
例如:
1. 和事件系统配合
收到服务器消息后,可能需要通知多个模块。
比如角色数据更新后:
-
UI 要刷新
-
红点要刷新
-
任务系统要刷新
-
新手引导要检查
-
本地缓存要更新
如果网络层直接调用所有模块,会导致耦合严重。
更好的方式是通过事件系统或命令系统进行分发。
这样网络层只负责接收消息,具体逻辑由对应系统处理。
2. 和 UI 系统配合
很多网络消息最终会影响 UI。
例如登录结果、背包数据、商城数据、任务状态等。
但网络层不应该直接操作 UI。
否则网络模块会和界面强耦合。
更合理的方式是:
-
网络层接收消息
-
数据系统更新数据
-
事件系统通知数据变化
-
UI 根据当前状态刷新
这样 UI 和网络之间不会直接绑定。
3. 和资源系统配合
有些服务器消息中会带资源 ID 或资源路径。
例如角色头像、道具图标、活动图片等。
客户端收到消息后,可能需要加载对应资源。
这时网络层不应该直接加载资源,而应该把数据交给业务模块,由业务模块通过资源系统处理。
这样职责更清晰。
4. 和配置表系统配合
服务器消息中经常会包含配置 ID。
例如 itemID、skillID、taskID。
客户端收到 ID 后,需要查配置表得到名称、图标、描述、数值等。
这就需要网络层和配置表系统配合。
但配合方式不是网络层直接依赖所有配置表,而是业务模块根据消息内容去查询对应配置。
七、为什么需要多种通信方式
不同项目、不同平台、不同业务场景,对网络通信方式的需求不一样。
MyFramework 中网络模块会考虑多种通信方式,例如:
-
TCP
-
UDP
-
WebSocket
-
HTTP
它们适合的场景不同。
1. TCP
TCP 适合大多数长连接游戏逻辑。
比如登录后保持连接,持续收发游戏消息。
它的优势是可靠、有序,适合大部分业务消息。
2. UDP
UDP 适合对实时性要求更高、允许一定丢包的场景。
例如某些实时同步、位置同步、状态广播等。
当然 UDP 的可靠性需要业务层或网络层额外设计。
3. WebSocket
WebSocket 对 WebGL 或某些跨平台场景比较重要。
如果游戏需要运行在 Web 平台,WebSocket 通常会成为重要选择。
4. HTTP
HTTP 更适合一些短请求。
例如公告、版本检查、资源地址获取、某些平台接口等。
所以网络框架不应该只绑定一种通信方式,而应该根据项目需求支持扩展。
八、网络层的可调试性非常重要
网络问题最怕不好查。
很多 Bug 表面看是业务逻辑问题,实际可能是网络消息导致的。
比如:
-
某个 UI 没刷新
-
某个道具数量不对
-
某个任务状态异常
-
某个角色位置不正确
-
某个操作没有响应
这些问题都可能和网络消息有关。
所以网络层需要尽量方便调试。
我认为至少要能做到:
-
查看发出了哪些消息
-
查看收到了哪些消息
-
查看消息 ID
-
查看消息字段
-
查看消息耗时
-
查看连接状态
-
查看错误码
-
查看断线和重连日志
如果网络层没有足够日志,很多问题会非常难排查。
但日志也不能无脑打印。
因为网络消息可能非常频繁。
所以需要根据开发环境、调试开关、消息类型来控制日志输出。
九、网络层和服务器框架的关系
MyFramework 有配套服务器框架 MyServerFramework。
这使得网络通信设计不只是客户端单方面的问题。
客户端和服务器应该尽量在工具链上保持一致。
例如:
-
使用同一份协议定义
-
生成客户端消息代码
-
生成服务器消息代码
-
两端使用相同消息 ID
-
两端使用一致的字段顺序
-
两端都能根据协议变化重新生成代码
这样可以减少大量人为同步错误。
对于只有客户端框架的项目来说,服务器协议可能是外部系统。
但对于自己同时维护客户端和服务器的项目来说,协议工具链非常重要。
我更希望客户端和服务器之间不是靠口头约定,也不是靠复制粘贴代码,而是通过工具生成来保持一致。
这也是 MyFramework 和 MyServerFramework 配套设计的一个重点。
十、网络层不是越复杂越好
虽然网络层很重要,但它也不是越复杂越好。
一个好的网络层应该做到:
-
底层通信清晰
-
消息协议统一
-
代码生成稳定
-
收发流程明确
-
错误处理集中
-
日志方便调试
-
业务层使用简单
-
扩展时不破坏旧结构
很多时候,网络层最怕的是职责不清。
如果网络层既负责连接,又负责业务逻辑,又负责 UI,又负责资源加载,最后一定会变得很难维护。
所以网络层应该有明确边界。
它应该负责:
-
连接
-
发送
-
接收
-
序列化
-
反序列化
-
消息注册
-
消息派发
-
网络状态
而具体业务逻辑应该交给业务系统处理。
十一、MyFramework 中网络模块的目标
在 MyFramework 中,我对网络模块的目标可以总结为几个点。
1. 协议统一
客户端和服务器基于同一份协议定义生成代码,减少两端不一致的问题。
2. 收发清晰
消息从创建、发送、接收、解析到派发,流程尽量清晰可追踪。
3. 容错可控
断线、重连、错误码、超时等问题尽量有统一处理方式。
4. 便于调试
网络日志、消息 ID、消息字段、连接状态等信息应该方便查看。
5. 适合长期维护
网络层不只是能跑,而是要能随着项目长期演进。
新增协议、修改字段、增加模块时,不能让维护成本持续上升。
结语
网络通信是游戏项目中非常核心的一层。
它看起来只是客户端和服务器之间的数据传输,但实际上会影响整个项目的稳定性、可维护性和扩展能力。
我在 MyFramework 中设计网络模块时,关注的不是简单封装 Socket,而是希望构建一套更完整的通信流程:
协议统一。
代码生成。
消息收发清晰。
客户端和服务器配套。
异常情况可控。
长期维护成本可接受。
对于长期 Unity 商业项目来说,网络层不是可有可无的封装,而是整个工程体系中的基础设施。
如果你正在做 RPG、MMO、SLG、模拟经营,或者任何需要长期维护客户端和服务器通信的项目,那么网络通信层都值得认真设计。
项目地址:
https://github.com/ZHOURUIH/MyFramework
配套服务器框架:
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)