C# 语言进阶(十三)网络 DLL 库分层设计

本篇核心知识点 :游戏网络通信完整分层、公共网络 DLL 库分层设计、Message 消息基类与多态子类、Packet 数据包粘包解决方案、Session 会话收发层、异步拆包完整逻辑、消息队列线程安全、服务端 / 客户端双端架构、Unity 对接网络库、消息命令约定、多态序列化

一、整体工程架构总览

1. 项目分层结构

  1. 公共底层 DLL 库(MyNet.dll)

    存放两端共用代码:Packet 数据包、GameMessage 消息基类、Session 会话层,控制台服务端、Unity 客户端同时引用,一套代码复用,消除重复开发。

  2. 控制台登录服务端(LoginServer)

    依赖 DLL,监听端口、接收客户端连接、消息分发、业务逻辑处理。

  3. 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 成员字段

  1. byte[] HeadBuf = new byte[4]:固定 4 字节包头,存放包体字节长度;

  2. byte[] BodyBuf:存放序列化后的 GameMessage 二进制数据;

  3. 两个构造函数:

    发送构造:传入 GameMessage,自动序列化填充 BodyBuf,计算长度写入 HeadBuf;

    空接收构造:用于异步接收时缓存分段字节;

  4. 辅助方法GetFullBuffer():拼接包头 + 包体,返回完整待发送字节数组;

  5. Reset()重置方法:清空缓冲区,复用 Packet 减少频繁 new 内存开销。

3. 关键拆包 Bug 修复知识点

问题场景

单次异步 Receive 仅读取部分包头(如只读到 2 字节,还差 2 字节),直接解析长度会数值错乱,程序崩溃。

修复逻辑
  1. 增加包头接收计数int headReadLen = 0,循环累加收到的字节;

  2. 只有headReadLen == 4时,才解析包头获取 Body 长度;

  3. 包体同样增加读取计数,未达到指定长度时持续异步接收补齐剩余字节;

  4. 完整包解析完成后调用 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. 基础通用字段(所有消息共用)

  1. int Uid:玩家唯一 ID;登录前为临时空值,登录成功服务下发玩家 Uid;

  2. int MainCmd:主命令,区分业务模块(登录 / 战斗 / 聊天);

  3. int SubCmd:子命令,区分具体操作;

    规范约定:客户端发服务端 = 奇数 (1001),服务返回客户端 = 偶数 (1002);

  4. int Result:操作结果码,0 成功、1 账号错、2 密码错等,替代 bool 支持多状态。

2. 序列化 / 反序列化核心机制

概念

序列化:将对象所有字段转为连续 byte 数组,网络传输只能发送字节;

反序列化:收到字节数组还原对象字段。

多态强制规则
  1. 父类virtual byte[] ToBytes(),序列化通用 Uid、Cmd 等字段;

  2. 子类(C2SLogin/S2LoginResp)必须override ToBytes()

  3. 子类序列化先调用 base.ToBytes () 写入父类通用字节,再追加自身业务字段;

  4. 反序列化同理,先解析父类字节,再读取子类专属数据。

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,账号密码等子类数据丢失。

解决方案
  1. 先通过包头解析的消息 SubCmd 判断消息真实子类;

  2. 泛型工具T GameMessage.FromBytes<T>(byte[] buf),约束 T 有无参构造;

  3. 根据命令 new 对应子类,调用子类 FromBuffer 完整解析全部字段。

6. 流量优化拓展

  1. int 数字直接 4 字节二进制远优于字符串传输(数字 123456 字符串占 6 字节,二进制仅 4 字节,大幅降低实时游戏流量消耗);

  2. 早期 2G/3G 时代极致优化:char 替代 byte、字段对齐压缩,现代项目仅需基础二进制序列化。

四、Session 会话收发层(底层 Socket 封装)

1. 核心职责

  1. 封装 Socket 与 NetworkStream,异步 BeginReceive 非阻塞收发;

  2. 完整包头、包体分段拆包逻辑,自动处理粘包;

  3. 消息委托回调:完整 Packet 解析完成后转发上层处理;

  4. 网络异常 / 主动断连触发虚函数OnDisconnect(),供子类重写弹窗提示;

  5. 对外SendMessage(GameMessage msg)接口,自动打包 Packet 发送。

2. 核心成员

  1. Socket _socket:底层套接字;

  2. NetworkStream _stream:网络读写流;

  3. Action<byte[], Session> OnMsgReceive:消息回调委托;

  4. protected virtual void OnDisconnect():断连虚回调;

  5. MessagePacket _recvPacket:复用接收包缓冲区。

3. 异步拆包完整流程

  1. 初始化空 Packet,调用_stream.BeginReceive()异步读取 4 字节包头;

  2. 异步回调判断本次读取字节数量,累加 HeadReadLen;

  3. 未凑齐 4 字节,再次发起异步读取补齐;

  4. 凑齐 4 字节后 BitConverter 解析包体长度,初始化 BodyBuf;

  5. 异步循环读取包体,累加 BodyReadLen 至目标长度;

  6. 包体完整触发委托,将包体字节 + 当前 Session 传给上层;

  7. 调用 Packet.Reset 重置,开启下一轮接收循环。

4. 断开连接执行顺序

  1. 触发 OnDisconnect 虚方法(子类重写弹窗、清理玩家数据);

  2. 关闭 NetworkStream 流;

  3. 关闭 Socket 释放端口资源;

分层解耦设计

Session 只负责底层字节收发、拆包,不处理任何业务逻辑,收到完整消息通过委托交给上层 LoginServer/NetManager,底层与业务完全隔离。

五、服务端 LoginServer 实现

1. 运行流程

  1. 初始化 Socket Bind 绑定端口、Listen 监听客户端连接;

  2. 循环Accept()等待客户端接入;

  3. 新连接创建 Session 对象,绑定消息处理委托、开启异步接收;

  4. 多线程消息队列,加锁保证并发安全:

    子线程收到消息加锁入队;

    主线循环遍历队列,根据 SubCmd 分发业务逻辑;

  5. 收到登录消息校验账号(课程写死模拟数据库),构造 S2CLoginResp 回复客户端;

  6. 维护 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. 工程配置规范

  1. DLL 输出路径指定 Unity Assets 文件夹,项目自动识别;

  2. 新建 Net 空 GameObject 挂载 NetManager 单例;

  3. UI 层独立 LoginUI 面板,按钮绑定登录发送逻辑。

2. NetManager 全局网络单例

核心限制:Unity 禁止子线程操作 UI、GameObject

解决方案:

  1. Session 异步子线程收到消息,加锁存入 UI 安全消息队列;

  2. 在 MonoBehaviour.Update 主线循环批量取出消息处理;

  3. 所有弹窗、界面跳转、物体操作仅在 Update 执行。

3. LoginSession 子类重写断连回调

继承底层 Session,重写OnDisconnect(),绑定事件委托通知 UI 弹出断网提示(观察者模式解耦)。

4. UI 层调用逻辑

LoginUI 按钮点击,直接调用NetManager.Instance.SendLogin()构造 C2SLogin 消息发送;Update 主线收到 1002 登录回执,根据 Result 值弹窗提示登录成功 / 密码错误。

七、消息命令统一设计规范

  1. 主 Cmd:区分大业务模块(1000 登录、2000 战斗、3000 聊天);

  2. 子 SubCmd 细分操作:

    C2S 客户端发:奇数 1001 登录请求、1003 注册请求;

    S2C 服务回:偶数 1002 登录结果、1004 注册结果;

  3. Result 结果码统一:0 成功、1 密码错误、2 账号不存在,拓展无需修改命令号。

八、项目调试与常见 Bug 排错

  1. 粘包解析错乱:未补齐包头 / 包体字节,修复累加读取计数;

  2. 消息子类字段丢失:仅反序列化 GameMessage 基类,需根据 SubCmd 实例对应子类;

  3. Unity 子线程报错:未中转主线消息队列,所有 UI 逻辑放 Update;

  4. 连接断开无提示:未重写 Session.OnDisconnect 虚回调;

  5. 服务端无法回复客户端:消息队列未绑定来源 Session;

  6. 频繁 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 四次挥手断连处理。

相关推荐
大师兄66681 小时前
HarmonyOS7 HTTP 网络请求封装:统一拦截器实战
网络·网络协议·http·arkui·harmonyos7·实战讲解
2601_949816351 小时前
C++内存对齐与结构体填充深度精讲:底层规则、内存裁剪、适配优化与工程避坑
开发语言·arm开发·c++
浩瀚地学1 小时前
【面试算法笔记】0105-数组-螺旋矩阵
java·开发语言·笔记·算法·面试
Geek-Chow2 小时前
Mobile App Certificate Pinning: Underlying Principle and a Swift Example
开发语言·ios·swift·安全架构
hbugs0012 小时前
BPF 表达式检查器:一款免费开源的 Wireshark 捕获过滤器语法检查与可视化构建工具
网络·测试工具·开源·eve-ng·bpf
韩曙亮2 小时前
【Flutter】iOS 网络权限设置 ② ( 配置 iOS 平台可使用 HTTP 访问 | ATS 网络安全强制规范 )
网络·flutter·http·ios·https·网络权限·ats
阿豪只会阿巴2 小时前
两小时快速入门 FastAPI--第一回
开发语言·python·fastapi
柠石榴2 小时前
远程连接校内服务器 ssh
运维·服务器·ssh
tiantianuser2 小时前
NVME-oF IP 设计1 :为什么要设计它?
网络·网络协议·rdma·roce v2·nvme of