下面的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;
}
}