Unity 3D 笔记(十五)Unity/C# Socket 网络笔记4

本篇核心知识点:服务端通用代码客户端移植、Unity 多线程报错处理、Protobuf 环境配置、客户端 NetManager 单例、异步 BeginConnect/BeginReceive 回调、缓冲区复用、多线程消息分离、业务 / 工具消息分类、委托协议回调、Update 主线程处理 UI、心跳断线重连

一、服务端通用代码客户端移植与环境配置

1 概念

将服务端成熟工具类(Buffer 缓冲区、AES 加解密、MessageBase 消息基类)移植到 Unity 客户端,统一通信底层逻辑;修复.Net 版本、第三方库、多线程命名空间报错,搭建客户端网络基础环境。

2 特性 / 规则

  1. 可复用移植模块:Buffer 字节缓冲区、AES 加密工具、MessageBase 协议父类;

  2. 需删除服务端独有 Client 字典、多路复用 Select 等无关代码;

  3. Protobuf 库放入 Plugins 文件夹,该文件夹为 Unity 专用第三方库目录;

  4. 项目 API 兼容设置为.NET 4.7.2,匹配服务端工程框架版本;

  5. 高版本 Unity 无 Task 多线程报错,2017 旧版本需移除 Task 命名空间避免编译失败。

3 语法 & 代码片段

复制代码
// 客户端复用缓冲区结构(和服务端完全一致)
public class Buffer{
    private byte[] buffer;
    private int readIndex;
    private int writeIndex;
    private const int DefaultCap = 1024;
    public int DataLen => writeIndex - readIndex;
    public int Remain => buffer.Length - writeIndex;
}
// Protobuf实体基类统一结构
public abstract class MessageBase{
    public abstract string ProtoName { get; }
}

4 相关知识点对比

从零重写客户端底层 VS 服务端代码移植复用

从零重写:大量重复逻辑,易出现两端逻辑不一致、分包粘包处理差异; 移植复用:收发、扩容、加解密逻辑两端统一,减少 Bug,开发效率翻倍。

5 模块拓展

移植完成后统一封装 NetManager 单例,所有联网调用仅通过单例入口,杜绝分散联网代码。

二、客户端 NetManager 单例与异步连接逻辑

1 概念

全局唯一网络管理单例,存储 Socket、公私钥、缓冲区;使用 TCP 异步 BeginConnect 建立连接,通过回调函数处理连接成功 / 失败,不阻塞 Unity 主线程。

2 特性 / 规则

  1. 公钥写死配置,对称密钥由服务首次下发,客户端初始密钥为空;

  2. 连接前置判断 Socket 状态,已连接直接打印日志不重复发起连接;

  3. Socket.NoDelay=true 关闭 Nagle 延迟,消息立即发送无缓冲等待;

  4. BeginConnect 为异步 API,连接结果通过注册回调函数自动触发,无需轮询;

  5. 回调函数运行在子线程,不能直接操作 Unity 游戏对象、UI 组件。

3 语法 & 代码片段

复制代码
// 网络单例基础结构
public class NetManager{
    public static NetManager Instance { get; private set; }
    private Socket clientSocket;
    private Buffer recvBuf;
    private string publicKey;
    private string secretKey; // 服务下发,初始空
    // 异步连接入口
    public void Connect(string ip, int port){
        if(clientSocket != null && clientSocket.Connected){
            Debug.LogError("已建立连接,无需重连");
            return;
        }
        clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        clientSocket.NoDelay = true;
        recvBuf = new Buffer();
        // 注册连接回调
        clientSocket.BeginConnect(IPAddress.Parse(ip), port ConnectCallback, clientSocket);
    }
    // 连接回调函数
    void ConnectCallback(IAsyncResult ar){
        Socket sock = (Socket)ar.AsyncState;
        sock.EndConnect(ar);
        Debug.Log("客户端连接成功");
        // 连接成功后开启异步接收
        StartRecv();
    }
}

4 相关知识点对比

同步 Connect VS 异步 BeginConnect

同步 Connect:阻塞主线程,Unity 游戏会卡顿卡死; 异步 BeginConnect:后台子线程执行连接,游戏画面、逻辑不受影响。

5 模块拓展

增加断开连接统一 Close 方法,清空密钥、关闭 Socket、置空网络资源。

三、异步接收数据回调与缓冲区分包粘包处理

1 概念

连接成功后持续调用 BeginReceive 异步接收字节流,数据写入客户端独立 Buffer 缓冲区;复用服务端完整分包、粘包解析逻辑,在子线程完成原始字节存储。

2 特性 / 规则

  1. BeginReceive 接收完成自动触发接收回调,递归调用实现持续监听数据流;

  2. 接收返回值 <=0 判定客户端断开,执行资源释放;

  3. 缓冲区剩余空间不足时执行二倍扩容,和服务扩容算法完全统一;

  4. 子线程仅做字节存储,禁止解析后直接操作 Unity 业务逻辑。

3 语法 & 代码片段

复制代码
// 开启循环接收
void StartRecv(){
    Socket sock = clientSocket;
    sock.BeginReceive(recvBuf.buffer, recvBuf.writeIndex, recvBuf.Remain,
        SocketFlags.None, RecvCallback, sock);
}
// 接收回调
void RecvCallback(IAsyncResult ar){
    Socket sock = (Socket)ar.AsyncState;
    int recvCount = sock.EndReceive(ar);
    if(recvCount <= 0){
        Close();
        return;
    }
    recvBuf.writeIndex += recvCount;
    // 解析缓冲区完整消息
    ParseBuffer();
    // 继续异步接收,实现循环
    StartRecv();
}

4 相关知识点对比

主线程 Update 轮询接收 VS 异步回调递归接收

Update 轮询:每一帧主动读取,网络延迟波动大,消耗主线程性能; 异步回调:有数据才触发读取,无数据休眠,性能极低。

5 模块拓展

ParseBuffer 复用服务端包头读取、协议名解析、Protobuf 反序列化全套逻辑。

四、多线程消息隔离:工具消息 & 业务消息分离

1 概念

子线程回调无法操作 Unity 主线程 API,因此将解析完成消息分为两类:工具消息(心跳、密钥,子线程直接处理)、业务消息(登录、背包,存入线程安全集合,主线程 Update 统一处理)。

2 特性 / 规则

  1. 心跳、密钥属于底层网络工具逻辑,无 UI 依赖,子线程可直接处理;

  2. 登录、商城、背包等业务涉及按钮、弹窗、数据渲染,必须主线程执行;

  3. 消息集合读写加线程锁,防止多线程同时修改导致集合崩溃;

  4. 自定义计数器记录消息总数,替代 List.Count 避免多线程计数失真。

3 语法 & 代码片段

复制代码
private List<MessageBase> allMsg = new List<MessageBase>();
private List<MessageBase> unityMsg = new List<MessageBase>();
private object msgLock = new object();
private int msgCount;
// 解析完成消息存入集合(加锁)
void SaveMsg(MessageBase msg){
    lock(msgLock){
        allMsg.Add(msg);
        msgCount++;
        // 判断消息类型,区分工具/业务
        if(msg is HeartMsg || msg is SecretMsg){
            // 工具消息子线程直接处理
            HandleToolMsg(msg);
        }
        else{
            unityMsg.Add(msg);
        }
    }
}

4 相关知识点对比

子线程直接处理所有消息 VS 线程隔离分流

直接处理:调用 Instantiate、Text.text 等会直接触发 Unity 多线程报错、程序闪退; 分流隔离:底层网络工具子线程处理,UI 业务主线程执行,完全规避引擎线程限制。

5 模块拓展

独立开启专用消息处理后台线程,死循环轮询消息集合,减轻接收回调线程压力。

五、委托字典协议回调分发机制

1 概念

客户端无法使用反射分发(子线程反射易异常、主线程反射开销高),采用委托 Dictionary 存储协议名 处理方法键值对,发送消息前注册回调,收到业务消息匹配委托执行逻辑。

2 特性 / 规则

  1. 自定义无返回带 MessageBase 委托类型;

  2. 字典 Key 为协议名,Value 为对应业务处理委托;

  3. 提供统一注册、执行方法,新增业务仅注册对应回调;

  4. 所有业务逻辑在 Unity Update 主线程安全执行。

3 语法 & 代码片段

复制代码
// 协议委托定义
public delegate void MsgDelegate(MessageBase msg);
// 存储所有协议回调
private Dictionary<string, MsgDelegate> protoDict = new Dictionary<string>();
// 注册协议监听
public void RegisterProto(string protoName, Msg del){
    if(!protoDict.ContainsKey(protoName))
        protoDict[protoName] = del;
}
// 执行对应协议回调
public void ExecuteMsg(string proto, MessageBase msg){
    if(protoDict.TryGetValue(proto, out var del))
        del.Invoke(msg);
}

4 相关知识点对比

反射分发 VS 委托字典分发

反射:运行动态查找方法,客户端多线程环境不稳定,存在性能消耗; 委托字典:提前绑定方法,匹配直接调用,无反射开销,线程安全可控。

5 模块拓展

游戏登录、注册、背包、商城界面启动时统一注册对应协议回调。

六、Unity 主线程 Update 统一执行业务消息

1 概念

在 MonoBehaviour 脚本 Update 每一帧读取线程安全的业务消息集合,取出消息匹配委托回调,所有 UI 创建、文本修改、界面开关操作全部在主线程执行。

2 特性 / 规则

  1. 每一帧加锁读取 unityMsg 列表,遍历完成清空;

  2. 逐条解析协议名,调用 ExecuteMsg 执行注册的业务委托;

  3. 帧内消息处理完成再执行下一轮网络接收,避免 UI 卡顿;

3 语法 & 代码片段

复制代码
void Update(){
    lock(netManager.msgLock){
        while(netManager.unityMsg.Count > 0){
            MessageBase msg = netManager.unityMsg[0];
            netManager.unityMsg.RemoveAt(0);
            // 根据协议名执行回调
            netManager.ExecuteMsg(msg.ProtoName, msg);
        }
    }
}

4 相关知识点对比

子线程直接操作 UI VS Update 主线程中转

子线程 UI 操作:Unity 引擎保护机制直接抛出多线程异常,游戏崩溃; Update 中转:完全遵循 Unity 单线程渲染规则,无报错稳定运行。

5 模块拓展

消息处理增加帧限数量,单帧最多处理 10 条消息,防止海量消息造成帧率暴跌。

七、客户端心跳机制与断线重连逻辑

1 概念

客户端定时向服务发送心跳包,同时记录服务端心跳接收时间;长时间未收到服务心跳判定断线,自动清空资源,提供重连入口。

2 特性 / 规则

  1. 客户端存储两个时间戳:发送心跳时间、接收服务心跳时间;

  2. 心跳属于工具消息,子线程直接更新接收时间戳;

  3. 主线程定时对比当前 UTC 时间与心跳时间,超时触发断开;

  4. 断开后清空密钥、消息集合、Socket 资源,可调用 Connect 重新建立连接。

3 语法 & 代码片段

复制代码
private long lastSendHeart;
private long lastRecvHeart;
// 定时发送心跳协程
IEnumerator SendHeartCor(){
    while(true){
        yield return new WaitForSeconds(3f);
        if(clientSocket != null && clientSocket.Connected){
            HeartMsg heart = new HeartMsg();
            SendMsg(heart);
            lastSendHeart = GetUtcTimeStamp();
        }
    }
}
// 心跳超时检测
void CheckHeartTimeout(){
    long now = GetUtcTimeStamp();
    if(now - lastRecvHeart > 120 * 1000){
        Close();
        Debug.Log("心跳超时,连接断开");
    }
}

4 相关知识点对比

仅服务端心跳检测 VS 客户端双向心跳校验

仅服务端检测:客户端切后台、断网时服务端延迟踢人; 双向校验:两端互相判定在线,断线立刻感知,快速触发重连。

5 模块拓展

断线后弹出弹窗,提供一键重连按钮,重连成功自动重新请求登录、背包数据。

综合拓展

1 客户端与服务端统一架构分层

底层层:Buffer 缓冲区、AES 加解密、Protobuf 序列化、TCP 异步 Socket;

网络层:NetManager 单例、异步收发、分包粘包统一解析;

消息层:工具 / 业务消息线程隔离、委托协议回调分发;

主线程层:Update 帧统一处理 UI 业务、心跳定时检测;

业务层:登录 / 背包 / 商城等界面注册协议回调。

2 高频 Bug 排查清单

客户端连接立即断开:公私钥不一致、Protobuf 协议名字拼写错误;

子线程操作 UI 报错:未分流存入 unityMsg,直接处理业务消息;

消息不执行回调:界面未调用 RegisterProto 注册对应协议;

缓冲区数据丢失:扩容逻辑读写索引处理错误;

心跳误踢:心跳容错时间设置过短,时间戳使用本地 DateTime 而非 UTC。

3 面试核心考点

客户端异步 Socket 原理、Unity 多线程限制、工具 / 业务消息分流目的、委托字典分发优势、两端心跳双向机制、服务端代码复用思路。

4 进阶拓展学习方向

断线自动重连完整逻辑、Protobuf 复杂嵌套协议、RSA 非对称加密握手、玩家本地缓存、异步资源加载配合联网数据。

相关推荐
JackieDYH1 小时前
WebSocket-不同平台封装-Hooks和使用案例
网络·websocket·网络协议
羑悻的小杀马特2 小时前
把随身WiFi改成网盘聚合器:中兴F50挂载本地存储+夸克网盘实战
运维·服务器·人工智能·网盘·openlist
EIP低代码平台10 小时前
EIP 低代码平台 - 角色维护
低代码·c#·权限·工作流·netcore
andxe12 小时前
安科士 AndXe 技术博客:400G QSFP112 SR4 光模块|AI 算力与超算短距互联最优方案
网络·人工智能·光模块·光通信
shiyi.十一13 小时前
第2章:应用层 — 知识要点与架构
网络·计算机网络·架构
吳所畏惧14 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
DFT计算杂谈14 小时前
无 Root 权限在 Tesla K80 零门槛部署 DeepSeek 大模型
linux·服务器·网络·数据库·机器学习
水境传感 李兆栋14 小时前
GNSS 位移监测站 :毫米级感知,筑牢安全监测防线
网络
心平气和量大福大14 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf