Unity中的NetworkManager基于protobuf, Socket-TCP

下面的NetworkManager基于与大端服务器通信,如果服务器不是大端,记得修改。如果没有引入protobuf,记得修改。如果没有引入Newtonsoft.Json,记得修改。

本文章只做备份用,如有不理解,多看注释与问ai

cs 复制代码
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using Google.Protobuf;
using Network.Protocol;
using Newtonsoft.Json;
using UnityEngine;

public class NetworkManager : MonoSingleton<NetworkManager>
{
    [Header("网络设置")] 
    public string serverIP = "127.0.0.1";
    public int serverPort = 8080;

    [Header("心跳设置")] 
    public float heartbeatInterval = 30f;                   // 心跳间隔30秒

    private Socket clientSocket;                            // 本机客户端Socket
    private int _expectedBodyLength = -1;                   // 期望的消息体长度
    private List<byte> _receiveBuffer = new();              // 累积接收缓冲区
    private bool isConnected;                               // 连接状态标志位
    private Queue<NetworkMessage> messageQueue = new();     // 处理消息队列
    private object queueLock = new();                       // 消息队列线程锁
    private Thread receiveThread;                           // 后台持续接收消息线程

    #region 生命周期

    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        Connect();
    }

    private void Update()
    {
        ProcessMessageQueue();
    }

    protected override void OnDestroy()
    {
        Disconnect();
    }

    protected override void OnApplicationQuit()
    {
        base.OnApplicationQuit();
        Disconnect();
    }

    #endregion
    
    // 初始化连接
    private void Connect()
    {
        try
        {
            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            clientSocket.Connect(serverIP, serverPort);
            isConnected = true;

            receiveThread = new Thread(ReceiveData);
            receiveThread.IsBackground = true;
            receiveThread.Start();

            Debug.Log("连接至服务器成功");

            // 持续发送心跳消息
            StartHeartbeat();
            
            // 发送加入消息
            //SendJoinMessage();
        }
        catch (Exception e)
        {
            Debug.LogError($"连接服务器失败: {e.Message}");
        }
    }

    // 断开连接
    private void Disconnect()
    {
        try
        {
            isConnected = false;

            if (clientSocket != null)
            {
                if (clientSocket.Connected) clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
                clientSocket.Dispose();
                clientSocket = null;
            }

            receiveThread?.Join(1000); // 等待接收线程结束

            Debug.Log("与服务器断开连接");
        }
        catch (Exception e)
        {
            Debug.LogError($"断开连接错误: {e.Message}");
        }
    }

    // 持续接收来自服务器的消息 后台线程
    private void ReceiveData()
    {
        // 创建接收缓冲区,每次最多接收1024字节
        var buffer = new byte[1024];

        while (isConnected && clientSocket != null && clientSocket.Connected)
        {
            try
            {
                // 从socket接收数据,该方法会阻塞直到有数据到达或连接关闭
                var bytesRead = clientSocket.Receive(buffer);
                Debug.Log($"收到{bytesRead}字节数的消息");
                
                // 检查接收到的字节数
                if (bytesRead > 0)
                {
                    // 创建正确大小的数组存放本次接收的数据
                    var newData = new byte[bytesRead];
                    Array.Copy(buffer, newData, bytesRead);
                    // 将新数据添加到累积缓冲区
                    _receiveBuffer.AddRange(newData);

                    // 处理累积缓冲区中的数据
                    ProcessReceivedData();
                }
                else
                {
                    // 连接已关闭
                    Debug.Log("服务器连接已关闭");
                    isConnected = false;
                    break;
                }
            }
            catch (SocketException se)
            {
                Debug.LogError($"网络接收错误: {se.Message}");
                isConnected = false;
                break;
            }
            catch (Exception e)
            {
                Debug.LogError($"接收数据异常: {e.Message}");
                isConnected = false;
                break;
            }
        }

        Debug.Log("接收线程结束");
        Disconnect();
    }

    // 处理接收到的数据,解析完整消息
    private void ProcessReceivedData()
    {
        try
        {
            // 循环处理所有完整消息
            while (_receiveBuffer.Count >= 4) // 至少要有消息头
            {
                // 如果还没有解析出消息体长度,先解析消息头
                if (_expectedBodyLength == -1)
                {
                    // 读取消息头(4字节)
                    var headBytes = _receiveBuffer.GetRange(0, 4).ToArray();
                    
                    // 从大端序语言接收转为小端序
                    // Array.Reverse(headBytes);
                    
                    // 默认小端序消息头
                    // _expectedBodyLength = BitConverter.ToInt32(headBytes, 0);
                    
                    // 手动读取并转换大端序消息头
                    _expectedBodyLength = (headBytes[0] << 24) | 
                                          (headBytes[1] << 16) | 
                                          (headBytes[2] << 8) | 
                                          headBytes[3];

                    // 添加长度验证
                    if (_expectedBodyLength < 0 || _expectedBodyLength > 10 * 1024 * 1024) // 限制10MB
                    {
                        Debug.LogError($"无效的消息长度: {_expectedBodyLength}");
                        Disconnect();
                        return;
                    }
                }

                // 检查是否收到了完整的消息体
                if (_receiveBuffer.Count >= 4 + _expectedBodyLength)
                {
                    // 提取消息体
                    var bodyBytes = _receiveBuffer.GetRange(4, _expectedBodyLength).ToArray();
                    // 序列化为文本 再使用Newtonsoft序列化为对应类
                    //string message = Encoding.UTF8.GetString(bodyBytes);
                    // 处理消息
                    //ProcessMessage(message);

                    // 使用protobuf直接序列化为类 之后直接加入消息队列
                    var message = NetworkMessage.Parser.ParseFrom(bodyBytes);
                    lock (queueLock)
                    {
                        messageQueue.Enqueue(message);
                    }

                    // 从缓冲区中移除已处理的数据
                    _receiveBuffer.RemoveRange(0, 4 + _expectedBodyLength);

                    // 重置期望长度,准备处理下一条消息
                    _expectedBodyLength = -1;
                }
                else
                {
                    // 数据不完整,等待下次接收
                    break;
                }
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"处理接收数据错误: {e.Message}");
            Disconnect();
        }
    }

    // 转换消息类型并保存到消息队列(不使用protobuf)
    private void ProcessMessage(string message)
    {
        try
        {
            // 从string 的Json消息反序列化为主消息类
            var msg = JsonConvert.DeserializeObject<NetworkMessage>(message);
            lock (queueLock)
            {
                // 加入消息队列
                messageQueue.Enqueue(msg);
            }
        }
        catch (JsonException je)
        {
            Debug.LogError($"JSON解析错误: {je.Message}, 原始消息: {message}");
        }
        catch (Exception e)
        {
            Debug.LogError($"处理消息错误: {e.Message}");
        }
    }

    // 处理消息队列
    private void ProcessMessageQueue()
    {
        lock (queueLock)
        {
            while (messageQueue.Count > 0)
            {
                var message = messageQueue.Dequeue();
                HandleMessage(message);
            }
        }
    }

    // 根据消息类型处理消息
    private void HandleMessage(NetworkMessage message)
    {
        try
        {
            switch (message.Type)
            {
                case MessageType.Unknown:
                    Debug.LogWarning($"为什么发这个消息给我");
                    break;
                
                case MessageType.Heartbeat:
                    Debug.Log("收到心跳消息");
                    Heartbeat heartbeatMessage = message.Heartbeat;
                    long timeTick = heartbeatMessage.ServerTimestamp;
                    Debug.Log($"收到时间戳:{timeTick}");
                    break;
                
                case MessageType.ChangeLevel:
                    
                    ChangeLevel changeLevelMessage = message.ChangeLevel;
                    TestNetwork.Instance.UpdateLog($"收到服务器权威消息: 关卡信息 - 当前选择关卡ID {changeLevelMessage.CurrentSelectLevelId}, 最大关卡ID {changeLevelMessage.MaxLevelId}, 是否可游玩 {changeLevelMessage.CanPlayThisLevel}");
                    
                    break;
                
                case MessageType.ChangeCharacter:
                    
                    ChangeCharacter changeCharacterMessage = message.ChangeCharacter;
                    TestNetwork.Instance.UpdateLog($"收到服务器权威消息: 角色信息 - 当前选择角色ID {changeCharacterMessage.CurrentSelectCharacterId}, 是否解锁 {changeCharacterMessage.IsUnlock}, 已解锁角色列表 {string.Join(", ", changeCharacterMessage.UnlockedCharacters)}");
                    
                    break;
                
                case MessageType.PassLevel:
                    
                    Debug.LogError($"不应该收到通关关卡消息");
                    
                    break;
                
                case MessageType.UnlockCharacter:
                    
                    UnlockCharacter unlockCharacterMessage = message.UnlockCharacter;
                    TestNetwork.Instance.UpdateLog($"收到服务器权威消息: 解锁角色 - 解锁角色ID {unlockCharacterMessage.CharacterId}, 解锁结果 {unlockCharacterMessage.CanUnlock}");
                    
                    break;
                
                default:
                    Debug.LogWarning($"不应该接收到该类型的消息: {message.Type}");
                    break;
            }
        }
        catch (Exception e)
        {
            throw new Exception("处理消息时出错: " + e.Message);
        }
    }

    /// <summary>
    /// 发送封装好的NetworkMessage消息到服务器
    /// </summary>
    /// <param name="message">封装好的消息</param>
    public void Send(NetworkMessage message)
    {
        try
        {
            if (!isConnected || clientSocket == null || !clientSocket.Connected)
            {
                Debug.LogWarning("连接已断开,无法发送消息");
                return;
            }

            // 使用 protobuf 序列化消息体
            // protobuf 序列化后无需转为大端序
            var bodyData = message.ToByteArray();
            var bodyLength = bodyData.Length;

            // // 使用小端序编码消息头(4字节长度)
            // var headData = BitConverter.GetBytes(bodyLength);
            // // 消息头转为大端序
            // Array.Reverse(headData);
            
            // 手动创建大端序的消息头
            // 手动计算的方法,这样不依赖于本机字节序,而且性能更好
            byte[] headData = new byte[4];
            headData[0] = (byte)(bodyLength >> 24);
            headData[1] = (byte)(bodyLength >> 16);
            headData[2] = (byte)(bodyLength >> 8);
            headData[3] = (byte)bodyLength;

            // 合并消息头和消息体
            var totalData = new byte[headData.Length + bodyData.Length];
            
            Buffer.BlockCopy(headData, 0, totalData, 0, headData.Length);
            Buffer.BlockCopy(bodyData, 0, totalData, headData.Length, bodyData.Length);
            
            clientSocket.Send(totalData);

            Debug.Log($"发送消息成功,类型: {message.Type}, 消息体长度: {bodyLength}");
        }
        catch (Exception exception)
        {
            Debug.LogError($"发送消息错误: {exception.Message}");
            Disconnect();
        }
    }

    // 启动心跳机制
    private void StartHeartbeat()
    {
        // 使用InvokeRepeating每30秒发送一次心跳
        InvokeRepeating(nameof(SendHeartbeat), heartbeatInterval, heartbeatInterval);
        Debug.Log($"心跳机制已启动,间隔: {heartbeatInterval}秒");
    }

    // 停止心跳机制
    private void StopHeartbeat()
    {
        CancelInvoke(nameof(SendHeartbeat));
        Debug.Log("心跳机制已停止");
    }

    // 发送心跳包
    private void SendHeartbeat()
    {
        if (!IsConnected())
        {
            Debug.LogWarning("连接已断开,跳过心跳发送");
            StopHeartbeat();
            return;
        }

        try
        {
            var message = new NetworkMessage
            {
                Type = MessageType.Heartbeat,
                Timestamp = DateTime.UtcNow.Ticks,
            };

            Send(message);
            // Debug.Log("心跳包已发送");
        }
        catch (Exception e)
        {
            Debug.LogError($"发送心跳包失败: {e.Message}");
            // 如果心跳发送失败,可能连接已断开
            Disconnect();
        }
    }
    
    /// <summary>
    /// 获取当前连接状态
    /// </summary>
    /// <returns></returns>
    public bool IsConnected()
    {
        return isConnected && clientSocket != null && clientSocket.Connected;
    }
}

对应的proto文件

cs 复制代码
syntax = "proto3";

package network.protocol;
option java_package = "network.protocol";  
option java_multiple_files = true;

option csharp_namespace = "Network.Protocol";


// 消息类型枚举
enum MessageType {
	UNKNOWN = 0;
	HEARTBEAT = 1;          	// 心跳包
	ERROR = 2;              	// 错误消息
	PLAYER_CONNECT = 3;     	// 玩家连接
	PLAYER_DISCONNECT = 4;  	// 玩家断开
	CHANGE_LEVEL = 5;			// 切换关卡 => 判断玩家能打哪些关
	CHANGE_CHARACTER = 6;		// 切换角色 => 判断玩家持有哪些角色
	PASS_LEVEL = 7;  			// 玩家通关 => 记录最高通关id,解锁新关,获取对应经验值
	PLAYER_LEVEL_UP = 8;		// 玩家升级 => 记录当前等级
	CHARACTER_STAR_UP = 9;		// 玩家的某个角色升星 => 消耗金币,经验值
	UNLOCK_CHARACTER = 10;		// 玩家解锁某个新角色 => 消耗金币
	USE_ITEM = 11;				// 玩家使用某个道具(双倍经验卡等)
}

// 玩家信息
message PlayerInfo {
	string player_id = 1;           // 玩家ID
	string player_name = 2;         // 玩家名称
	int32 level = 3;                // 玩家等级
	float health = 4;               // 玩家血量
}

// 心跳包消息
message Heartbeat {
	int64 client_timestamp = 1;     // 客户端时间戳
	int64 server_timestamp = 2;     // 服务器时间戳
	int32 ping = 3;                 // 网络延迟
}

// 玩家连接消息
message PlayerConnect {
	PlayerInfo player = 1;          // 玩家信息
	string session_id = 2;          // 会话ID
	string version = 3;             // 客户端版本
}

// 玩家断开消息
message PlayerDisconnect {
	string player_id = 1;           // 玩家ID
	string reason = 2;              // 断开原因
	int64 disconnect_time = 3;      // 断开时间
	DisconnectType type = 4;        // 断开类型
}

// 断开类型枚举
enum DisconnectType {
	NORMAL = 0;           // 正常断开
	TIMEOUT = 1;          // 超时断开
	KICKED = 2;           // 被踢出
	BANNED = 3;           // 被封禁
	ERROR_DISCONNECT = 4; // 错误断开
}

// 错误消息
message ErrorMessage {
	int32 error_code = 1;           // 错误代码
	string error_message = 2;       // 错误信息
	string details = 3;             // 详细错误信息
}

// 通用响应消息
message Response {
	bool success = 1;               // 是否成功
	string message = 2;             // 响应消息
	ErrorMessage error = 3;         // 错误信息(如果success为false)
}

// 切换关卡消息
message ChangeLevel {
	int32 max_level_id = 1;					// 玩家最高解锁的关卡
	int32 current_select_level_id = 2;		// 当前选择的关卡
	bool can_play_this_level = 3;			// 能够游玩本关
	repeated int32 passed_level_id = 4;		// 玩家已通过的关卡的链表
}

// 切换角色
message ChangeCharacter {
	int32 current_select_character_id = 1;		// 当前选择的角色id
	bool is_unlock = 2;							// 是否解锁/持有该角色
	repeated int32 unlocked_characters = 3;		// 已解锁/当前玩家持有的所有角色
}

// 通过关卡
message PassLevel{
	int32 pass_level_id = 1;					// 当前通过的关卡的id
	int32 geted_coint = 2;						// 通关获取的金币
	int32 geted_exp = 3;						// 通关获取的经验值
	repeated int32 passed_level_id = 4;			// 玩家已通过的关卡的链表
}

// 玩家等级升级
message PlayerLevelUp{
	int32 current_level = 1;	// 玩家当前等级
	int32 current_exp = 2;		// 玩家当前经验值
	int32 current_coin = 3;		// 玩家当前金币
}

// 角色星级
enum Star{
	NONE = 0;
	ONE_STAR = 1;
	TWO_STAR = 2;
	THREE_STAR = 3;
}

// 玩家角色升星
message CharacterStarUp{
	int32 character_id = 1;			// 升星的角色id
	Star star_level = 2;			// 当前星级
	bool can_star_up = 3;			// 能否升星
	int32 current_coin = 4;			// 新的金币数量
	int32 current_exp = 5;			// 新的经验值数量
}

// 解锁某个角色
message UnlockCharacter{
	int32 character_id = 1;			// 想解锁的角色id
	bool can_unlock = 2;			// 是否成功解锁
	int32 current_coin = 3;			// 新的金币数量
	int32 current_exp = 4;			// 新的经验值数量
}

// 使用某个道具
message UseItem{
	
}

// 主消息包装器 
message NetworkMessage {
	MessageType type = 1;                   // 消息类型
	string message_content = 2;             // 消息内容
	// 这里应该添加一个玩家的身份信息标识 不知道是咪咕快游的玩家信息还是咋样
	int64 timestamp = 3;                    // 消息时间戳
  
	// 根据type字段选择具体的消息内容
	oneof payload {
		Heartbeat heartbeat = 10;
		PlayerConnect player_connect = 11;
		PlayerDisconnect player_disconnect = 12;
		ErrorMessage error = 13;
		Response response = 14;
		ChangeLevel change_level = 15;
		ChangeCharacter change_character = 16;
		PassLevel pass_level = 17;
		PlayerLevelUp player_level_up = 18;
		CharacterStarUp character_star_up = 19;
		UnlockCharacter unlock_character = 20;
		UseItem use_item = 21;
  }
}
相关推荐
车载测试工程师3 小时前
CAPL学习-ETH功能函数-通用函数
网络·学习·tcp/ip·capl·canoe
老蒋新思维3 小时前
创客匠人洞察:AI 时代 IP 变现的认知重构,从流量焦虑到价值深耕的破局之道
网络·人工智能·tcp/ip·重构·知识付费·创始人ip·创客匠人
Wokoo77 小时前
HTTP不同版本核心对比
网络·网络协议·tcp/ip·http·udp·ssl
ZhengEnCi8 小时前
一次多线程同步问题的排查:从 thread_count 到 thread.join() 的踩坑之旅
python·网络协议·tcp/ip
AllBlue9 小时前
unity调用安卓方法
android·unity·游戏引擎
郝学胜-神的一滴9 小时前
Horse3D游戏引擎研发笔记(十):在QtOpenGL环境下,视图矩阵与投影矩阵(摄像机)带你正式进入三维世界
c++·3d·unity·游戏引擎·godot·图形渲染·unreal engine
jay9 小时前
ens2f0 IP 远程连线,balance-alb 模式配置双网卡(ens2f0 + ens6f0)Bond,避免断网
linux·运维·服务器·网络·tcp/ip
科技块儿9 小时前
简单易学的IP定位查找教程
网络·网络协议·tcp/ip
开心_开心急了11 小时前
TCP协议概要与Python示例
tcp/ip