C# 语言进阶(十二)TCP粘包问题、网络通信架构
本篇核心知识点TCP 粘包产生原因、消息 Packet 包头包体设计、异步 BeginReceive 拆包逻辑、NetworkStream 流封装、Session 会话完善、连接断开回调重写、Unity 线程安全消息队列、网络管理器单例、消息多态序列化 / 反序列化、客户端 LoginSession 子类、消息指令约定、主线程消息处理、项目分层规范、多线程 UI 限制一、TCP 粘包问题核心原理1. 粘包产生原因TCP 是面向字节流协议无天然消息边界操作系统内核会合并多个小数据包一起发送提升 IO 效率多条消息字节拼接在同一缓冲区接收端无法区分一条消息起止出现粘包。类比外卖骑手顺路多单合并派送客户端一次性收到多条消息混杂字节流。2. 行业标准解决方案包头 包体结构每条完整消息封装为Packet数据包包头 Head固定 4 字节 int存储后续包体字节长度包体 BodyGameMessage 序列化后的二进制字节数组发送逻辑先写入 4 字节长度再写入消息数据接收逻辑先读取 4 字节包头获取包体长度再读取对应长度字节作为完整消息。3. Packet 数据包完整封装核心成员byte [] headBuf固定 4 字节存储消息长度byte [] bodyBuf存储序列化消息体构造重载带 GameMessage 参数用于发送自动序列化、填充长度无参空构造用于接收缓存读取的字节数据。using System; using System.Collections.Generic; namespace MyNet.Core{ public class MessagePacket{ // 包头固定4字节 public byte[] HeadBuf new byte[4]; // 消息包体 public byte[] BodyBuf; // 发送用构造传入游戏消息自动打包 public MessagePacket(GameMessage msg){ byte[] data msg.ToBytes(); BodyBuf data; // 将长度写入4字节包头 BitConverter.GetBytes(data.Length).CopyTo(HeadBuf, 0); } // 接收空构造 public MessagePacket() { } // 获取完整待发送总字节包头包体 public byte[] GetFullBuffer(){ Listbyte full new Listbyte(HeadBuf); full.AddRange(BodyBuf); return full.ToArray(); } } }4. Session 发送方法改造原先直接发送 Message现在必须封装为 Packet 再发送public void Send(Game msg){ MessagePacket packet new MessagePacket(msg); byte[] sendBuf packet.GetFullBuffer(); _socket.Send(sendBuf); }二、异步 BeginReceive 拆包完整逻辑1. 同步 Receive 缺陷同步阻塞Receive会卡子线程无法同时处理收发采用异步BeginReceive非阻塞接收IO 完成自动回调。2. 拆包完整流程先异步读取固定 4 字节包头判断实际读取字节数不足 4 字节则等待下一轮数据补齐4 字节完整后通过BitConverter解析得到包体长度 len再异步读取 len 长度包体字节包体读取完成调用上层消息回调推入消息队列清空缓存循环再次接收下一条包头。3. 缓冲区规范默认缓冲区推荐 4096/8192 字节兼顾小型游戏消息性能不分配超大数组避免内存浪费。4. NetworkStream 流封装Socket 原生字节操作繁琐使用NetworkStream封装读写流统一读写接口NetworkStream _stream new NetworkStream(_socket);5. 异常与断开处理网络 IO 随时抛出异常掉线、断网所有 Receive 逻辑包裹 try-catch捕获异常调用Close()关闭会话。Session Close 关闭流程触发断开回调OnDisconnect()虚方法客户端子类重写先关闭 NetworkStream 流再关闭底层 Socket 套接字释放端口资源public void Close(){ OnDisconnect(); // 断开事件回调 _stream?.Close(); _socket?.Close(); } // 虚断开回调供客户端子类重写 protected virtual void OnDisconnect(){ Console.WriteLine(客户端连接断开); }三、GameMessage 序列化 / 反序列化完善多态虚方法1. 底层字节读写工具统一封装读写 int、float、string 工具方法操作 Listbyte缓冲WriteInt写入 4 字节 intWriteFloat写入 4 字节浮点数WriteString先写入字符串长度再写入字符字节解决不定长字符串边界问题ReadInt/ReadFloat/ReadString按顺序读取字节内部维护光标偏移量。2. 基类虚序列化 ToBytes父类定义 virtual 序列化方法子类 override 时先调用 base.ToBytes ()序列化父类通用字段Uid、主命令 Cmd、子命令 SubCmd、结果 Result再追加子类业务字段。public virtual byte[] ToBytes(){ Listbyte buf new Listbyte(); // 序列化父类通用字段 WriteInt(buf, Uid); WriteInt(buf, Cmd); WriteInt(buf, SubCmd); WriteInt(buf, Result); return buf.ToArray(); }3. 登录消息子类示例C2SLogin客户端→登录服消息额外携带账号、密码字符串public class C2SLogin : GameMessage{ public string Account; public string Password; public override byte[] ToBytes(){ Listbyte buf new Listbyte(base.ToBytes()); WriteString(buf, Account); WriteString(buf, Password); return buf.ToArray(); } // 反序列化 public override void FromBuffer(byte[] data){ base.FromBuffer(data); int offset 16; // 父类4个int占16字节 Account ReadString(data, ref offset); Password ReadString(data, ref offset); } }4. 消息指令统一约定规范客户端发服务端C2S子命令为奇数1001 登录、1003 注册服务端回客户端S2C子命令为偶数1002 登录结果、1004 注册结果主 Cmd 区分业务模块1000 登录模块、2000 战斗模块、3000 聊天模块5. 泛型反序列化工具静态泛型 FromBytesT约束 T 必须继承 GameMessage 且有无参构造自动实例化对应消息对象并反填充字段public static T FromBytesT(byte[] buffer) where T : GameMessage, new(){ T msg new T(); msg.FromBuffer(buffer); return msg; }四、服务端消息队列多线程锁1. 设计目的网络异步接收子线程、主线业务循环多线程并发读写队列多客户端同时发消息并发入队会造成数据错乱必须互斥锁。核心结构private QueueGameMessage _msgQueue; private readonly object _queueLock new object; // 子线程收到消息加锁入队 public void EnqueueMsg(byte[] body, Session session){ lock(_queueLock){ GameMessage msg GameMessage.FromBytesGameMessage(body); _msgQueue.Enqueue(msg); } } // 主线业务循环加锁批量取出处理 public void ProcessAllMsg(){ lock(_queueLock){ while(_msg.Count 0){ GameMessage msg _msgQueue.Dequeue(); DispatchMsg(msg); // 按Cmd分发业务逻辑 } } } // 根据命令分发处理 private void DispatchMsg(GameMessage msg){ switch(msg.SubCmd){ case 1001: // 登录业务逻辑 break; case 1003: // 注册逻辑 break; } }五、Unity 客户端分层架构重点课堂内容1 分层结构底层网络 DLL 库独立项目Session、Packet、GameMessage 通用服务端 / Unity 共用LoginSession 会话子类继承基础 Session重写OnDisconnect断开回调NetManager 网络单例管理器Unity 全局唯一挂载空 GameObjectUpdate 主线处理消息UI 登录面板按钮触发发送登录消息。2 LoginSession 子类重写断开回调利用多态覆盖虚方法断网时弹出 UI 提示框使用委托事件观察者模式通知界面public class LoginSession : Session{ // 断网事件委托UI面板订阅 public event Action OnClientDisconnect; protected override void OnDisconnect(){ base.OnDisconnect(); OnClientDisconnect?.Invoke(); } }3 NetManager 全局单例Unity 主线程专用核心作用持有 LoginSession 连接对象内部带锁消息队列接收子线程消息缓存Update 生命周期主线取出消息处理规避 Unity 多线程操作 UI 报错关键知识点Unity 线程限制Unity 所有 GameObject、UI、Transform、组件只能在主线程访问异步网络子线程无法操作界面必须通过队列中转到 Update 主线处理。public class NetManager : MonoBehaviour{ // 单例 public static NetManager Instance { get; private set; } private LoginSession _session; private QueueGameMessage _uiMsgQueue new QueueGameMessage(); private readonly object _msgLock new object(); private void Awake(){ Instance this; DontDestroyOnLoad(gameObject); } // 连接服务器 public void ConnectServer(string ip, int port){ // 实例化子类会话 _session new LoginSession(); bool res _session.Connect(ip, port); // 订阅断开事件 _session.OnClientDisconnect OnNetLost; } // 子线程消息入队 public void AddMsgToQueue(GameMessage msg){ lock(_msgLock) _uiMsgQueue.Enqueue(msg); } // Update主线处理消息 private void Update(){ lock(_msgLock){ while(_uiMsgQueue.Count 0){ var msg _uiMsgQueue.Dequeue(); HandleClientMsg(msg); } } } // 处理服务端返回消息 void HandleClientMsg(GameMessage msg){ switch(msg.SubCmd){ case 1002: Console.WriteLine(登录成功更新UI玩家信息); break; } } // 断网回调 void OnNetLost(){ Debug.LogError(与服务器断开连接弹出提示弹窗); } // 对外发送消息接口 public void SendMsg(GameMessage msg){ if(_session ! null) _session.Send(msg); } }4 UI 登录面板调用public class LoginUI : MonoBehaviour{ public Button loginBtn; private void Start(){ loginBtn.onClick.AddListener(OnLoginClick); } void OnLoginClick(){ // 构造登录消息 C2SLogin loginMsg new C2SLogin(); loginMsg.Account Player001; loginMsg.Password 123456; // 调用网络管理器发送 NetManager.Instance.SendMsg(loginMsg); } }六、完整双向通信运行流程启动控制台 Tcp 服务端开启监听Unity 运行NetManager Awake 初始化点击登录按钮构造 C2SLogin 消息调用 SendSession 内部封装 Packet 包头包体异步发送 TCP 字节流服务端 Session 异步 BeginReceive 先读取 4 字节包头解析长度读取对应包体字节反序列化为 GameMessage加锁推入服务端消息队列主线循环分发登录业务服务端处理完成生成 S2CLoginResp 返回包客户端异步线程收到字节解析消息入 UI 安全队列Unity Update 主线取出消息刷新玩家 UI 数据客户端 / 服务端任意一方关闭连接触发 OnDisconnect 回调。七、核心工程拓展TCP 粘包根本成因包头 4 字节长度解决方案原理Packet 包头 包体分层设计作用字节数组拼接逻辑同步 Receive 阻塞缺陷异步 BeginReceive 优势Session 虚断开回调 委托事件观察者模式解耦 UI 与底层网络Unity 多线程限制子线程禁止操作游戏对象消息队列主线中转方案GameMessage 多态序列化子类必须 base 父类序列化消息指令奇偶约定规范大型游戏模块分层设计多线程 Queue 读写必须 lock 互斥锁防止并发错乱DLL 复用优势服务端、Unity 客户端共用同一套网络底层无重复开发泛型反序列化约束where T : GameMessage, new()作用。