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
这里主要用到 Encoding 和 BitConverter 方法来处理转化, 没什么需要说的.
这里定义结构接口处理网络:
/// <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}");
}