![]() |
VOOZH | about |
dotnet add package Megumin.ProtobufLUT --version 1.4.0
NuGet\Install-Package Megumin.ProtobufLUT -Version 1.4.0
<PackageReference Include="Megumin.ProtobufLUT" Version="1.4.0" />
<PackageVersion Include="Megumin.ProtobufLUT" Version="1.4.0" />Directory.Packages.props
<PackageReference Include="Megumin.ProtobufLUT" />Project file
paket add Megumin.ProtobufLUT --version 1.4.0
#r "nuget: Megumin.ProtobufLUT, 1.4.0"
#:package Megumin.ProtobufLUT@1.4.0
#addin nuget:?package=Megumin.ProtobufLUT&version=1.4.0Install as a Cake Addin
#tool nuget:?package=Megumin.ProtobufLUT&version=1.4.0Install as a Cake Tool
这是一个 网络库。
这是一个网络模块的通用解决方案。设计目的为应用程序网络模块提供统一的HighLevel接口。
整个类库被拆分为多个dll。简单来说:NetRemoteStandard.dll是标准,里面只有接口定义;Megumin.Remote.dll是一种实现。类比于dotnetStandard和dotnetCore的关系。
是的,使用Nuget获取Megumin.Remote。但是注意,需要搭配序列化库,不同的序列化库可能有额外的要求。
目标框架netstandard2.1,在unity中建议unity版本2021.2以上。过小的版本可以使用源码,但需要自行解决依赖关系。
or add "com.megumin.net": "https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net" to Packages/manifest.json.
If you want to set a target version, uses the
*.*.*release tag so you can specify a version like#2.1.0. For examplehttps://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.
Span<T>。使用System.IO.Pipelines作为高性能IO缓冲区。MIT许可证设计原则:最常用的代码最简化,复杂的地方都封装起来。
发送一个消息,并等待一个消息返回 是类库的全部内容。
从结果值返回异常是有意义的:1.省去了try catch ,写法更简单(注意,没有提高处理异常的性能)2.用来支持异常在分布式服务器中传递,避免try catch 控制流。
///实际使用中的例子
IRemote remote = new TCPRemote(); ///省略连接代码……
public async void TestSend()
{
Login login = new Login() { Account = "LiLei", Password = "HanMeiMei" };
/// 泛型类型为期待返回的类型
var (result, exception) = await remote.SendAsync<LoginResult>(login);
///如果没有遇到异常,那么我们可以得到远端发回的返回值
if (exception == null)
{
Console.WriteLine(result.IsSuccess);
}
}
方法签名:
ValueTask<Result> SendAsyncSafeAwait<Result>(object message, object options = null, Action<Exception> onException = null);
结果值是保证有值的,如果结果值为空或其他异常,触发异常回调函数,不会抛出异常,所以不用try catch。异步方法的后续部分不会触发,所以后续部分可以省去空检查。
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体Remote的实现,这是类库的特性。如果你使用了这个接口的其他实现,要确认实现遵守了这个约定。)
IRemote remote = new TCPRemote(); ///省略连接代码……
public async void TestSend()
{
Login login = new Login() { Account = "LiLei", Password = "HanMeiMei" };
/// 泛型类型为期待返回的类型
LoginResult result = await remote.SendAsyncSafeAwait<LoginResult>(login, (ex)=>{});
///后续代码 不用任何判断,也不用担心异常。
Console.WriteLine(result.IsSuccess);
}
虽然不推荐一个请求对应多个回复类型,但是某些业务设计仍然有此需求。比如将所有errorcode作为一个独立类型回复,那么一个请求就有可能有对应回复和errorcode两个回复类型。
protobuf协议中可以使用 IMessage接口 作为等待返回的类型。
class ErrorCode{}
class Resp{}
class Req{}
async void Test(IRemote remote){
Req req = new Req();
///泛型中填写所有期待返回类型的基类,然后根据类型分别处理。
///如果泛型处仅使用一种类型,那么服务器回复另一种类型时,底层会转换为 InvalidCastException 进如异常处理逻辑。
var ret = await remote.SendAsyncSafeAwait<object>(req);
if(ret is ErrorCode ec)
{
}
else if(ret is Resp resp)
{
}
}
ValueTask<object> OnReceive(short cmd, int messageID, object message);接收端回调函数
protected virtual async ValueTask<object> OnReceive(short cmd, int messageID, object message)
{
switch (message)
{
case TestPacket1 packet1:
Console.WriteLine($"接收消息{nameof(TestPacket1)}--{packet1.Value}");
return null;
case Login login:
Console.WriteLine($"接收消息{nameof(Login)}--{login.Account}");
return new LoginResult { IsSuccess = true };
case TestPacket2 packet2:
return new TestPacket1 { Value = packet2.Value };
default:
break;
}
return null;
}
具体响应方式参考PreReceive函数源码,参考IPreReceiveable,ICmdOption,SendOption.Echo等。
Heartbeat,RTT,Timestamp Synchronization等功能都由此机制实现。
PreReceive函数中处理此类消息,并调用GetResponse返回结果到发送端。
public interface IAutoResponseable : IPreReceiveable
{
ValueTask<object> GetResponse(object request);
}
线程调度
Remote 使用bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message)函数决定消息回调函数在哪个线程执行,true时所有消息被汇总到Megumin.ThreadScheduler.Update。
你需要轮询此函数来处理接收回调,它保证了按接收消息顺序触发回调(如果出现乱序,请提交一个BUG)。Unity中通常应该使用FixedUpdate。
如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么 false时接收消息回调使用Task执行,不必在轮询中等待,但无法保证有序,鱼和熊掌不可兼得。
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool.QueueUserWorkItem((A) =>
{
while (true)
{
ThreadScheduler.Update(0);
Thread.Yield();
}
});
Message.dll
(AOT/IL2CPP)当序列化类以dll的形式导入unity时(因为有时会将消息类库设计成unity外的共享工程),必须加入link文件,防止序列化类属性的get,set方法被il2cpp剪裁。重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| TotalLength(value including total length 4 byte) | RpcID | CMD | MSGID | Body |
|---|---|---|---|---|
| 总长度(值包含总长度自身的4个字节) | 消息ID | 消息正文 | ||
| Int32(int) | Int32(int) | Int16(short) | Int32(int) | byte[] |
| 4byte | 4byte | 2byte | 4byte | byte[].Lenght |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。2.0 版本删除MessagePipeline,改为多个Remote实现中可重写的函数,在工程实践中发现,将消息管线与Remote拆离没有意义,是过度设计。如果需要同时定制3个协议Remote的管线,可以由用户自行拆分,框架不做处理。
人生就是反反复复。
3.0版本决定改回最开始设计,第一版本的设计思路更好。
经过工程实践发现,2.0的设计并不方便重写,用户相同的重写代码在针对不同的协议时需要重写多份,分别从TcpRemote,UdpRemote,Kcpremote继承,每次修改时也要同时修改多份,十分笨重。
用户主要重写接收消息部分和断线部分,断线重连部分针对不同协议处理方式也不同。
所以将Transport和IDisconnectHandler从Remote拆分出来。
本质上说,3.0的Remote等于1.0的MessagePipeline。3.0的Transport等于1.0的Remote。
MessageLUT(Message Serialize Deserialize callback look-up table)是MessageStandard的核心类。MessagePipeline 通过查找MessageLUT中注册的函数进行序列化。因此在程序最开始你需要进行函数注册。
通用注册函数:
void RegistIMeguminFormatter<T>(KeyAlreadyHave key = KeyAlreadyHave.Skip) where T : class, IMeguminFormatter, new()
序列化类库的中间件基于MessageLUT提供多个简单易用的API,自动生成序列化和反序列化函数。需要为协议类添加一个MSGIDAttribute来提供查找表使用的ID。因为一个ID只能对应一组序列化函数,因此每一个协议类同时只能使用一个序列化库。
namespace Message
{
[MSGID(1001)] //MSGID 是框架定义的一个特性,注册函数通过反射它取得ID
[ProtoContract] //ProtoContract 是protobuf-net 序列化库的标志
[MessagePackObject] //MessagePackObject 是MessagePack 序列化库的标志
public class Login //同时使用多个序列化类库的特性标记,但程序中每个消息同时只能使用一个序列化库
{
[ProtoMember(1)] //protobuf-net 从 1 开始
[Key(0)] //MessagePack 从 0 开始
public string Account { get; set; }
[ProtoMember(2)]
[Key(1)]
public string Password { get; set; }
}
[MSGID(1002)]
[ProtoContract]
[MessagePackObject]
public class LoginResult
{
[ProtoMember(1)]
[Key(0)]
public bool IsSuccess { get; set; }
}
}
JIT环境下可以直接注册一个程序集
private static async void InitServer()
{
//MessagePackLUT.Regist(typeof(Login).Assembly);
Protobuf_netLUT.Regist(typeof(Login).Assembly);
ThreadPool.QueueUserWorkItem((A) =>
{
while (true)
{
ThreadScheduler.Update(0);
Thread.Yield();
}
});
}
AOT/IL2CPP 环境下需要显示通过泛型函数注册每一个协议类,以确保在AOT/IL2CPP编译器在静态分析时生成对应的泛型函数。
public void TestDefine()
{
Protobuf_netLUT.Regist<Login>();
Protobuf_netLUT.Regist<LoginResult>();
}
注意:
序列化库使用代码生成器生成代码,是生成类型实际的序列化函数。
而这里是为了静态分析时生成序列化类库通用API的泛型函数。
例如:
ProtoBuf.Serializer.Serialize<T>()生成为ProtoBuf.Serializer.Serialize<Login>()
两者不相同。
每个库有各自的限制,对IL2CPP支持也不同。框架会为每个支持的库写一个继承于MessageStandard/MessageLUT的新的MessageLUT.
由于各个序列化库对Span<byte>的支持不同,所以中间层可能会有轻微的性能损失.
对于序列化函数有三种形式:
RPC功能:保证了请求和返回消息一对一匹配。发送时RPCID为负数,返回时RPCID*-1 为正数,用正负区分上下行。
内存分配:通过使用内存池,减少alloc。内存池:标准库内存池,ArrayPool<byte>.Shared。序列化:使用type做Key查找函数。反序列化:使用MSGID(int)做Key查找函数。MessageLUT.Regist<T>函数手动添加其他类型。消息类型:尽量不要使用大的自定义的struct,整个序列化过程有可能导致多次装箱拆箱。在参数传递过程中还会多次复制,性能比class低。<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks>。时间和空间上的折衷
2.0版本 使用IBufferWriter<byte>和ReadOnlySequence<byte>解决了这个问题,效率更高。
这是写类库途中总结到的知识或者猜测:
Megumin.Remote是以MMORPG为目标实现的。对于非MMORPG游戏可能不是最佳选择。 在遥远的未来也许会针对不同游戏类型写出NetRemoteStandard的不同实现。
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 is compatible. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 was computed. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 was computed. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.