本篇核心知识点 :游戏网络通信完整分层、公共网络 DLL 库分层设计、Message 消息基类与多态子类、Packet 数据包粘包解决方案、Session 会话收发层、异步拆包完整逻辑、消息队列线程安全、服务端 / 客户端双端架构、Unity 对接网络库、消息命令约定、多态序列化
一、整体工程架构总览
1. 项目分层结构
-
公共底层 DLL 库(MyNet.dll)
存放两端共用代码:Packet 数据包、GameMessage 消息基类、Session 会话层,控制台服务端、Unity 客户端同时引用,一套代码复用,消除重复开发。
-
控制台登录服务端(LoginServer)
依赖 DLL,监听端口、接收客户端连接、消息分发、业务逻辑处理。
-
Unity 客户端工程
DLL 放置 Assets 目录,NetManager 单例网络管理器、UI 登录面板、主线程消息队列(规避 Unity 多线程报错)。
2. 分层职责划分
底层 Packet:解决 TCP 粘包,封装 4 字节包头 + 消息包体;
GameMessage:消息父类,多态序列化 / 反序列化,统一通信数据载体;
Session 会话:底层 Socket 异步收发、完整拆包、断连回调、消息委托转发;
上层管理器:服务端 LoginServer / Unity NetManager,处理业务、存储消息队列、分发逻辑。
通俗类比
Packet = 快递包装盒(带长度标签区分包裹),GameMessage = 包裹内货物,Session = 快递员负责收发,服务端 / 客户端 = 收发货双方。
二、Packet 数据包(粘包核心解决方案)
1. 核心概念
TCP 是无边界字节流,多条消息会粘连在一起,无法区分一条消息起止;Packet 固定4 字节 int 包头存储包体长度,接收端先读包头再读取对应长度包体,彻底解决粘包 / 拆包。
2. Packet 成员字段
-
byte[] HeadBuf = new byte[4]:固定 4 字节包头,存放包体字节长度; -
byte[] BodyBuf:存放序列化后的 GameMessage 二进制数据; -
两个构造函数:
发送构造:传入 GameMessage,自动序列化填充 BodyBuf,计算长度写入 HeadBuf;
空接收构造:用于异步接收时缓存分段字节;
-
辅助方法
GetFullBuffer():拼接包头 + 包体,返回完整待发送字节数组; -
Reset()重置方法:清空缓冲区,复用 Packet 减少频繁 new 内存开销。
3. 关键拆包 Bug 修复知识点
问题场景
单次异步 Receive 仅读取部分包头(如只读到 2 字节,还差 2 字节),直接解析长度会数值错乱,程序崩溃。
修复逻辑
-
增加包头接收计数
int headReadLen = 0,循环累加收到的字节; -
只有
headReadLen == 4时,才解析包头获取 Body 长度; -
包体同样增加读取计数,未达到指定长度时持续异步接收补齐剩余字节;
-
完整包解析完成后调用 Reset 重置缓冲区,准备接收下一条消息。
代码示例(Packet 核心结构)
public class MessagePacket{
// 固定4字节包头,存储包体长度
public byte[] HeadBuf = new byte[4];
public byte[] BodyBuf;
// 接收累计读取长度
public int HeadReadLen = 0;
public int BodyReadLen = 0;
// 发送用构造
public MessagePacket(GameMessage msg){
BodyBuf = msg.ToBytes();
// 将包体长度写入4字节包头
BitConverter.GetBytes(BodyBuf.Length).CopyTo(HeadBuf, 0);
}
// 空构造,接收数据使用
public MessagePacket() { }
// 拼接包头包体,得到完整发送字节
public byte[] GetFullBuffer(){
List<byte> full = new List<byte>(HeadBuf);
full.AddRange(BodyBuf);
return full.ToArray();
}
// 重置缓冲区,复用对象,减少GC
public void Reset(){
HeadReadLen = 0;
BodyReadLen = 0;
BodyBuf = null;
}
}
三、GameMessage 消息基类(多态序列化)
1. 基础通用字段(所有消息共用)
-
int Uid:玩家唯一 ID;登录前为临时空值,登录成功服务下发玩家 Uid; -
int MainCmd:主命令,区分业务模块(登录 / 战斗 / 聊天); -
int SubCmd:子命令,区分具体操作;规范约定:客户端发服务端 = 奇数 (1001),服务返回客户端 = 偶数 (1002);
-
int Result:操作结果码,0 成功、1 账号错、2 密码错等,替代 bool 支持多状态。
2. 序列化 / 反序列化核心机制
概念
序列化:将对象所有字段转为连续 byte 数组,网络传输只能发送字节;
反序列化:收到字节数组还原对象字段。
多态强制规则
-
父类
virtual byte[] ToBytes(),序列化通用 Uid、Cmd 等字段; -
子类(C2SLogin/S2LoginResp)必须
override ToBytes(); -
子类序列化先调用 base.ToBytes () 写入父类通用字节,再追加自身业务字段;
-
反序列化同理,先解析父类字节,再读取子类专属数据。
3. 底层字节工具函数
父类内置静态读写工具,统一处理 int/string/float:
WriteInt(List<byte> buf, int val):写入 4 字节整数;
WriteString(List<byte> buf, string str):先写入字符串长度,再写入字符字节(解决不定长字符串边界);
ReadInt(byte[] data, ref int offset):从指定偏移读取 int,偏移自动后移 4;
ReadString(byte[] data, ref int offset):先读长度再读取字符。
4. 典型子类:登录请求消息 C2SLogin
// 客户端→服务端 登录消息 SubCmd=1001
public class C2SLogin : GameMessage{
public string Account;
public string Password;
public override byte[] ToBytes(){
// 先序列化父类通用字段
List<byte> buffer = new List<byte>(base.ToBytes());
// 追加子类账号、密码
WriteString(buffer, Account);
return buffer.ToArray();
}
// 反序列化,从字节还原账号密码
public override void FromBuffer(byte[] data){
int offset = 0;
base.FromBuffer(data, ref offset);
Account = ReadString(data, ref offset);
Password = ReadString(data, ref offset);
}
}
5. 高频坑点:仅反序列化父类丢失子类字段
问题
收到字节直接 new GameMessage 反序列化,只会解析 Uid/Cmd,账号密码等子类数据丢失。
解决方案
-
先通过包头解析的消息 SubCmd 判断消息真实子类;
-
泛型工具
T GameMessage.FromBytes<T>(byte[] buf),约束 T 有无参构造; -
根据命令 new 对应子类,调用子类 FromBuffer 完整解析全部字段。
6. 流量优化拓展
-
int 数字直接 4 字节二进制远优于字符串传输(数字 123456 字符串占 6 字节,二进制仅 4 字节,大幅降低实时游戏流量消耗);
-
早期 2G/3G 时代极致优化:char 替代 byte、字段对齐压缩,现代项目仅需基础二进制序列化。
四、Session 会话收发层(底层 Socket 封装)
1. 核心职责
-
封装 Socket 与 NetworkStream,异步 BeginReceive 非阻塞收发;
-
完整包头、包体分段拆包逻辑,自动处理粘包;
-
消息委托回调:完整 Packet 解析完成后转发上层处理;
-
网络异常 / 主动断连触发虚函数
OnDisconnect(),供子类重写弹窗提示; -
对外
SendMessage(GameMessage msg)接口,自动打包 Packet 发送。
2. 核心成员
-
Socket _socket:底层套接字; -
NetworkStream _stream:网络读写流; -
Action<byte[], Session> OnMsgReceive:消息回调委托; -
protected virtual void OnDisconnect():断连虚回调; -
MessagePacket _recvPacket:复用接收包缓冲区。
3. 异步拆包完整流程
-
初始化空 Packet,调用
_stream.BeginReceive()异步读取 4 字节包头; -
异步回调判断本次读取字节数量,累加 HeadReadLen;
-
未凑齐 4 字节,再次发起异步读取补齐;
-
凑齐 4 字节后 BitConverter 解析包体长度,初始化 BodyBuf;
-
异步循环读取包体,累加 BodyReadLen 至目标长度;
-
包体完整触发委托,将包体字节 + 当前 Session 传给上层;
-
调用 Packet.Reset 重置,开启下一轮接收循环。
4. 断开连接执行顺序
-
触发 OnDisconnect 虚方法(子类重写弹窗、清理玩家数据);
-
关闭 NetworkStream 流;
-
关闭 Socket 释放端口资源;
分层解耦设计
Session 只负责底层字节收发、拆包,不处理任何业务逻辑,收到完整消息通过委托交给上层 LoginServer/NetManager,底层与业务完全隔离。
五、服务端 LoginServer 实现
1. 运行流程
-
初始化 Socket Bind 绑定端口、Listen 监听客户端连接;
-
循环
Accept()等待客户端接入; -
新连接创建 Session 对象,绑定消息处理委托、开启异步接收;
-
多线程消息队列,加锁保证并发安全:
子线程收到消息加锁入队;
主线循环遍历队列,根据 SubCmd 分发业务逻辑;
-
收到登录消息校验账号(课程写死模拟数据库),构造 S2CLoginResp 回复客户端;
-
维护 Session 字典,存储所有在线客户端会话,支持点对点消息回复。
2. 多线程安全队列规范
private Queue<MsgSessionPair> _msgQueue = new Queue<MsgSessionPair>();
private readonly object _queueLock = new object;
// 消息载体:同时存消息+来源Session(服务端多客户端必备)
public struct MsgSessionPair{
public Game Msg;
public Session Session;
}
// 入队(子线程)
public void Enqueue(byte[] body, Session session){
lock(_queueLock){
// 根据SubCmd实例对应子类,反序列化
GameMessage msg = Game.FromBytes<C2SLogin>(body);
_msgQueue.Enqueue(new MsgSessionPair(){Msg = msg, Session = session});
}
}
// 主线循环处理
public void ProcessMsg(){
lock(_queueLock){
while(_msg.Count > 0){
var pair = _msg.Dequeue();
DispatchMsg(pair.Msg, pair.Session);
}
}
}
// 命令分发
void DispatchMsg(GameMessage msg, Session session){
switch(msg.SubCmd){
case 1001:
C2SLogin loginMsg = msg as C2SLogin;
// 模拟数据库校验账号
S2LoginResp resp = new S2LoginResp();
resp.SubCmd = 1002;
if(loginMsg.Account == "123456" && loginMsg.Password == "123456")
resp.Result = 0; // 成功
else
resp.Result = 1; // 密码错误
session.SendMessage(resp);
break;
}
}
3. 服务端特有设计
消息队列同时保存消息 + 来源 Session,回复消息时精准发给对应客户端;
多客户端长连接并发,每个客户端独立 Session 互不干扰。
六、Unity 客户端分层实现
1. 工程配置规范
-
DLL 输出路径指定 Unity Assets 文件夹,项目自动识别;
-
新建 Net 空 GameObject 挂载 NetManager 单例;
-
UI 层独立 LoginUI 面板,按钮绑定登录发送逻辑。
2. NetManager 全局网络单例
核心限制:Unity 禁止子线程操作 UI、GameObject
解决方案:
-
Session 异步子线程收到消息,加锁存入 UI 安全消息队列;
-
在 MonoBehaviour.Update 主线循环批量取出消息处理;
-
所有弹窗、界面跳转、物体操作仅在 Update 执行。
3. LoginSession 子类重写断连回调
继承底层 Session,重写OnDisconnect(),绑定事件委托通知 UI 弹出断网提示(观察者模式解耦)。
4. UI 层调用逻辑
LoginUI 按钮点击,直接调用NetManager.Instance.SendLogin()构造 C2SLogin 消息发送;Update 主线收到 1002 登录回执,根据 Result 值弹窗提示登录成功 / 密码错误。
七、消息命令统一设计规范
-
主 Cmd:区分大业务模块(1000 登录、2000 战斗、3000 聊天);
-
子 SubCmd 细分操作:
C2S 客户端发:奇数 1001 登录请求、1003 注册请求;
S2C 服务回:偶数 1002 登录结果、1004 注册结果;
-
Result 结果码统一:0 成功、1 密码错误、2 账号不存在,拓展无需修改命令号。
八、项目调试与常见 Bug 排错
-
粘包解析错乱:未补齐包头 / 包体字节,修复累加读取计数;
-
消息子类字段丢失:仅反序列化 GameMessage 基类,需根据 SubCmd 实例对应子类;
-
Unity 子线程报错:未中转主线消息队列,所有 UI 逻辑放 Update;
-
连接断开无提示:未重写 Session.OnDisconnect 虚回调;
-
服务端无法回复客户端:消息队列未绑定来源 Session;
-
频繁 GC 卡顿:Packet 未复用,循环大量 new,增加 Reset 复用机制。
九、拓展
1 TCP 粘包成因与包头 4 字节 Packet 行业标准解决方案;
2 GameMessage 多态序列化原理,子类必须 base 父类的核心原因;
3 Session 分层设计优势:底层收发与业务解耦,两端共用 DLL;
4 Unity 线程限制、主线消息队列解决方案;
5 服务端多客户端如何精准回复消息(消息绑定 Session);
6 异步 BeginReceive 相比同步 Receive 性能优势;
7 二进制序列化对比 JSON 流量优势;
8 长连接工作流程、TCP 四次挥手断连处理。