MeteorCat / QFramework集成网络序打包

Created Sun, 17 Nov 2024 22:42:28 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
2868 Words

QFramework集成网络序打包

Unity 网络编程和其他网络服务端编程交互其实还有很大区别, 包含以下问题:

  • 网络传输默认采用 字节序(小端序) 传输, 并不是服务端常用的 网络序(大端序)
  • 默认字符串采用 Unicode 而非常规用的 UTF-8 字符串

对于网络序转化需要 bytes 反转下处理:

// 依靠 Array.Reverse 讲字节序转网络序
Int32 value = 120;
var buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer); // 翻转序列

对于字符串处理就需要编码处理下:

// Unicode 转 UTF8 字节流
var value = "hello.world";
var buffer = Encoding.UTF8.GetBytes(value);
var lengthBuffer = BitConverter.GetBytes(buffer.Length);
Array.Reverse(lengthBuffer);

// UTF8字节流 转 Unicode
var buffer = new byte[length]; // 假设客户端返回二进制流
Encoding.UTF8.GetString(buffer, 0, length); // 转化 Unicode

这里主要用到 EncodingBitConverter 方法来处理转化, 没什么需要说的.

这里定义结构接口处理网络:

/// <summary>
/// 网络序列化接口
/// </summary>
public interface IMessage
{
    /// <summary>
    /// 消息协议ID
    /// </summary>
    /// <returns>int</returns>
    int GetProtocolId();


    /// <summary>
    /// 设置协议Id
    /// </summary>
    /// <param name="protocolId">协议Id</param>
    void SetProtocolId(int protocolId);

    /// <summary>
    /// 序列化字节流 
    /// </summary>
    void Serialization(Stream stream);

    /// <summary>
    /// 反序列化
    /// </summary>
    void Deserialization(Stream stream);
}

这里接口比较简单, 只要求实现提供 ProtocolId(协议Id) | Serialization(序列化处理) | Deserialization(反序列化处理).

后续提供抽象类处理:

/// <summary>
/// 序列化消息抽象实现
/// </summary>
public abstract class AbstractMessage : IMessage
{
    /// <summary>
    /// 消息序列包ID, 默认为 0 
    /// </summary>
    /// <returns></returns>
    public virtual int GetProtocolId() => 0;

    /// <summary>
    /// 设置协议Id
    /// </summary>
    /// <param name="protocolId"></param>
    public virtual void SetProtocolId(int protocolId)
    {
    }


    /// <summary>
    /// 序列化
    /// </summary>
    /// <param name="stream"></param>
    public abstract void Serialization(Stream stream);

    /// <summary>
    /// 反序列化
    /// </summary>
    /// <param name="stream"></param>
    public abstract void Deserialization(Stream stream);
}

所有的消息结构类必须集成上面的抽象处理, 后续这里提供读写工具:

/// <summary>
/// 序列化处理工具集成
/// 注意: Unity采用字节序而非网络序列, 并且不是默认 UTF8 字符串
/// </summary>
public static class SerializeUtility
{
    /// <summary>
    /// Int16偏移值
    /// </summary>
    /// ReSharper disable once MemberCanBePrivate.Global
    public const int INT16Offset = sizeof(short);

    /// <summary>
    /// Int32 偏移值
    /// </summary>
    /// ReSharper disable once MemberCanBePrivate.Global
    public const int INT32Offset = sizeof(int);

    /// <summary>
    /// Int64 偏移值
    /// </summary>
    /// ReSharper disable once MemberCanBePrivate.Global
    public const int INT64Offset = sizeof(long);


    #region 编码消息

    /// <summary>
    /// 序列化 byte 到消息内
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static void WriteByte(Stream stream, byte value)
    {
        stream.WriteByte(value);
    }

    /// <summary>
    /// 序列化 byte 到消息内, 强制转化 int32
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    public static void WriteByte(Stream stream, Int32 value)
    {
        stream.WriteByte((byte)value);
    }


    /// <summary>
    /// 序列化 bytes 字节流到消息内
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    public static void WriteBytes(Stream stream, byte[] value)
    {
        WriteInt32(stream, value.Length);
        stream.Write(value, 0, value.Length);
    }


    /// <summary>
    /// 序列化 int16 到消息内, 约等于 short
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static void WriteInt16(Stream stream, Int16 value)
    {
        var buffer = BitConverter.GetBytes(value);
        Array.Reverse(buffer);
        stream.Write(buffer, 0, buffer.Length);
    }


    /// <summary>
    /// 序列化 int32 到消息内
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static void WriteInt32(Stream stream, Int32 value)
    {
        var buffer = BitConverter.GetBytes(value);
        Array.Reverse(buffer);
        stream.Write(buffer, 0, buffer.Length);
    }

    /// <summary>
    /// 序列化 int64 到消息内, 约等于 long 类型
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    public static void WriteInt64(Stream stream, Int64 value)
    {
        var buffer = BitConverter.GetBytes(value);
        Array.Reverse(buffer);
        stream.Write(buffer, 0, buffer.Length);
    }


    /// <summary>
    /// 序列化 string 到消息内, 只允许 utf8 传输
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static void WriteString(Stream stream, string value)
    {
        if (value == null)
        {
            WriteInt32(stream, 0);
            return;
        }

        var buffer = Encoding.UTF8.GetBytes(value);
        var lengthBuffer = BitConverter.GetBytes(buffer.Length);
        Array.Reverse(lengthBuffer);
        stream.Write(lengthBuffer, 0, lengthBuffer.Length);
        stream.Write(buffer, 0, buffer.Length);
    }


    /// <summary>
    /// 写入消息结构体
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="message"></param>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static void WriteStruct(Stream stream, IMessage message)
    {
        message.Serialization(stream);
    }

    #endregion


    #region 解码消息

    /// <summary>
    /// 解码 byte 数据
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static byte ReadByte(Stream stream)
    {
        return (byte)stream.ReadByte();
    }


    /// <summary>
    /// 解码 bytes 字节流数据
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public static byte[] ReadBytes(Stream stream)
    {
        var lengthBuffer = new byte[INT32Offset];
        _ = stream.Read(lengthBuffer, 0, INT32Offset);
        Array.Reverse(lengthBuffer);
        var length = BitConverter.ToInt32(lengthBuffer, INT32Offset);
        var buffer = new byte[length];
        _ = stream.Read(buffer, 0, length);
        return buffer;
    }


    /// <summary>
    /// 解码 int16 数据, 也就是 short
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static Int16 ReadInt16(Stream stream)
    {
        var buffer = new byte[INT16Offset];
        _ = stream.Read(buffer, 0, INT16Offset);
        Array.Reverse(buffer);
        return BitConverter.ToInt16(buffer, 0);
    }


    /// <summary>
    /// 解码 int32 数据
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    /// ReSharper disable once MemberCanBePrivate.Global
    public static Int32 ReadInt32(Stream stream)
    {
        var buffer = new byte[INT32Offset];
        _ = stream.Read(buffer, 0, INT32Offset);
        Array.Reverse(buffer);
        return BitConverter.ToInt32(buffer, 0);
    }

    /// <summary>
    /// 通过二进制数据解析读取 int32
    /// </summary>
    /// <param name="buffer"></param>
    /// <returns></returns>
    public static Int32 ReadInt32(byte[] buffer)
    {
        Array.Reverse(buffer);
        return BitConverter.ToInt32(buffer, 0);
    }


    /// <summary>
    /// 解码 int64 数据
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public static Int64 ReadInt64(Stream stream)
    {
        var buffer = new byte[INT64Offset];
        _ = stream.Read(buffer, 0, INT64Offset);
        Array.Reverse(buffer);
        return BitConverter.ToInt64(buffer, 0);
    }


    /// <summary>
    /// 解码字符串数据, 只支持 UTF-8
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public static string ReadString(Stream stream)
    {
        var lengthBuffer = new byte[INT32Offset];
        _ = stream.Read(lengthBuffer, 0, INT32Offset);
        Array.Reverse(lengthBuffer);
        var length = BitConverter.ToInt32(lengthBuffer, 0);
        var buffer = new byte[length];
        _ = stream.Read(buffer, 0, length);
        return Encoding.UTF8.GetString(buffer, 0, length);
    }


    /// <summary>
    /// 解构数据
    /// todo: 可以考虑返回 bool 和学习 TryGetValue 之类 out 返回
    /// </summary>
    /// <param name="stream"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T ReadStruct<T>(Stream stream) where T : IMessage, new()
    {
        var clazz = new T(); // 动态实例化
        clazz.Deserialization(stream);
        return clazz;
    }

    #endregion



    #region 快速编码

    
    
    /// <summary>
    /// 将消息在缓冲区当中编码成二进制流
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="message"></param>
    /// <returns></returns>
    public static byte[] PackBytes(MemoryStream stream,IMessage message)
    {
        // 重置缓冲区
        stream.SetLength(0);
        stream.Position = 0;
        
        
        // 先填充首位Int32, 作为 body 长度占位和协议ID占位
        WriteInt32(stream, 0); // 4
        WriteInt32(stream, message.GetProtocolId()); // 4
        
        // 结构写入到内存流中
        message.Serialization(stream);
        
        // 计算当前写入长度偏移, 只需要 协议id+body 长度
        var offset = (int)stream.Position - INT32Offset;
        
        // 设置目前缓冲区指向位置0, 覆盖掉之前占位的消息body长度数据
        stream.Position = 0;
        WriteInt32(stream, offset);
        
        // 返回消息二进制流
        return stream.ToArray();
    }
    

    #endregion
}

这里提供登录请求样例:

/// <summary>
/// 玩家请求消息
/// 这里仅仅作为消息结构前置, 如果要形容就是传输 HTML 网页的时候, 分为 header(头信息) 和 body(内容)
/// 数据类 + SerializeUtility 仅仅生成为 body, 还需要构建 header 头信息, 也就是数据内容长度 
/// </summary>
public class ReqLoginMessage : AbstractMessage
{
    /// <summary>
    /// 用户名
    /// </summary>
    public string Username { get; set; }

    /// <summary>
    /// 密码
    /// </summary>
    public string Password { get; set; }

    /// <summary>
    /// 服务器ID
    /// </summary>
    public int ServerId { get; set; }

    /// <summary>
    /// 客户端版本
    /// </summary>
    public int Version { get; set; }

    /// <summary>
    /// 设备信息
    /// </summary>
    public string Device { get; set; }


    /// <summary>
    /// 序列化处理
    /// </summary>
    /// <param name="stream"></param>
    public override void Serialization(Stream stream)
    {
        SerializeUtility.WriteString(stream, Username);
        SerializeUtility.WriteString(stream, Password);
        SerializeUtility.WriteInt32(stream, ServerId);
        SerializeUtility.WriteInt32(stream, Version);
        SerializeUtility.WriteString(stream, Device);
    }

    /// <summary>
    /// 反序列化处理
    /// </summary>
    /// <param name="stream"></param>
    public override void Deserialization(Stream stream)
    {
        // 如果没有返回消息直接弹出: throw new NotImplementedException();
        Username = SerializeUtility.ReadString(stream);
        Password = SerializeUtility.ReadString(stream);
        ServerId = SerializeUtility.ReadInt32(stream);
        Version = SerializeUtility.ReadInt32(stream);
        Device = SerializeUtility.ReadString(stream);
    }


    /// <summary>
    /// 返回消息协议ID
    /// </summary>
    /// <returns></returns>
    public override int GetProtocolId()
    {
        return 10001;
    }


    /// <summary>
    /// 转化为String打印
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return
            $"Username: {Username}, Password: {Password}, ServerId: {ServerId}, Version: {Version}, Device: {Device}";
    }
}


/// <summary>
/// 消息结构打包成网络消息
/// </summary>
public class MessageBuilder : MonoBehaviour
{
    /// <summary>
    /// 发送消息结构
    /// </summary>
    private ReqLoginMessage LoginMessage { get; set; } = new ReqLoginMessage()
    {
        Username = string.Empty, // 4 + N
        Password = string.Empty, // 4 + N
        ServerId = 1, // 服务器ID, 4
        Version = 20241116, // 客户端版本, 4,
        Device = "{}" // JSON数据, 4 + 2 = 6
    };

    /// <summary>
    /// 推送单条消息结构体
    /// </summary>
    byte[] mSendBuffer = new byte[64 * 1024]; // 设置单个消息推送长度为 64k 

    /// <summary>
    /// 内存流
    /// </summary>
    MemoryStream mSendStream;

    /// <summary>
    /// 启动初始化
    /// </summary>
    private void Start()
    {
        // 设定推送缓冲区
        mSendStream = new MemoryStream(mSendBuffer);
    }


    /// <summary>
    /// 测试GUI
    /// </summary>
    private void OnGUI()
    {
        GUILayout.BeginVertical();

        // 账号设置
        GUILayout.BeginHorizontal();
        GUILayout.Label("账号:");
        LoginMessage.Username = GUILayout.TextArea(LoginMessage.Username, 64);
        GUILayout.EndHorizontal();

        // 密码设置
        GUILayout.BeginHorizontal();
        GUILayout.Label("密码:");
        LoginMessage.Password = GUILayout.TextArea(LoginMessage.Password, 64);
        GUILayout.EndHorizontal();


        // 序列化触发
        if (GUILayout.Button("序列化"))
        {
            // 重置缓冲区
            mSendStream.SetLength(0);
            mSendStream.Position = 0;

            // 先填充首位Int32, 作为 body 长度占位
            SerializeUtility.WriteInt32(mSendStream, 0); // 4

            // 之后填充 Int32, 作为 协议ID 占位
            SerializeUtility.WriteInt32(mSendStream, 0); // 4

            // 结构写入到内存流中
            // [14(服务器ID|客户端版本|设备信息)] + [dev123(4+6),账号|密码] * 2 + [4(协议号)] = 38
            LoginMessage.Serialization(mSendStream);


            // 计算当前写入长度偏移, 只需要 协议id+body 长度
            var offset = (int)mSendStream.Position - sizeof(int);

            // 设置目前缓冲区指向位置0, 覆盖掉之前占位数据
            mSendStream.Position = 0;
            SerializeUtility.WriteInt32(mSendStream, offset);

            // 写入协议ID
            SerializeUtility.WriteInt32(mSendStream, LoginMessage.GetProtocolId());


            // 最后得出结构数据
            var data = mSendStream.ToArray();
            Debug.Log($"Offset: {offset}, Bytes: {data.ToHexString()}");


            // 这里账号密码都输入 dev123, 得出以下 Hex(十六进制串):
            // Bytes: 00000026000027110000000664657631323300000006646576313233000000010134DADC000000027B7D
            //
            // 这里主要结构成这样:
            // [00000026][00002711] [其他]
            // [00000026] = [0x00000026] = 38(代表了含ProtoId的数据包二进制长度)
            // [00002711] = [0x0002711] = 10001(代表协议ID)
            // 
            // 服务端首先解包获取首位 int32 确定后续消息长度动态申请缓冲区


            // 测试再转回结构体,
            var reader = new MemoryStream(data);
            var l = SerializeUtility.ReadInt32(reader);
            var c = SerializeUtility.ReadInt32(reader);
            Debug.Log($"Length:{l}, CMD:{c}");

            // 转化结构
            var response = SerializeUtility.ReadStruct<ReqLoginMessage>(reader);
            Debug.Log($"Response:{response}");
        }

        GUILayout.EndVertical();
    }
}

这里就是具体流程, 实际上 GUILayout.Button("序列化") 的方法快也可以简约处理:

// 序列化触发
if (GUILayout.Button("序列化"))
{
    
    // 获取快速消息编码
    var data = SerializeUtility.PackBytes(mSendStream,LoginMessage);
    Debug.Log($"Bytes: {data.ToHexString()}");
    
    
    // 这里账号密码都输入 dev123, 得出以下 Hex(十六进制串):
    // Bytes: 00000026000027110000000664657631323300000006646576313233000000010134DADC000000027B7D
    //
    // 这里主要结构成这样:
    // [00000026][00002711] [其他]
    // [00000026] = [0x00000026] = 38(代表了含ProtoId的数据包二进制长度)
    // [00002711] = [0x0002711] = 10001(代表协议ID)
    // 
    // 服务端首先解包获取首位 int32 确定后续消息长度动态申请缓冲区
    
    
    // 测试再转回结构体,
    var reader = new MemoryStream(data);
    var l = SerializeUtility.ReadInt32(reader);
    var c = SerializeUtility.ReadInt32(reader);
    Debug.Log($"Length:{l}, CMD:{c}");
    
    // 转化结构
    var response = SerializeUtility.ReadStruct<ReqLoginMessage>(reader);
    Debug.Log($"Response:{response}");
}