Home

Awesome

这是什么?

这是一个 简单易用的 网络库。
这是一个网络模块的通用解决方案。设计目的为应用程序网络模块提供统一的HighLevel接口。

整个类库被拆分为多个dll。简单来说:NetRemoteStandard.dll是标准,里面只有接口定义;Megumin.Remote.dll是一种实现。类比于dotnetStandard和dotnetCore的关系。

为什么要拆分为多个dll?
具体实现可能需要依赖很多其他dll,而接口定义并不需要这些依赖。对于只想使用接口,自定义实现的用户来说,引入额外的依赖是不必要的。例如MessageStandard,用户仅引用自己选择的序列化库即可,而不必引用多个序列化库。

Dll依赖关系与架构

依赖关系



它是开箱即用的么?

是的,使用Nuget获取Megumin.Remote。但是注意,需要搭配序列化库,不同的序列化库可能有额外的要求。
由于使用了C# 7.3语法,在unity中如果使用源码至少需要2018.3。
目标框架netstandard2.1,在unity中建议unity版本2021.2以上。过小的版本可以使用源码,但需要自行解决依赖关系。

UPM Package

Install via git URL

image

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 example https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.

快速入门



优势

劣势



核心方法3个

设计原则:最常用的代码最简化,复杂的地方都封装起来。
发送一个消息,并等待一个消息返回 是类库的全部内容。


1. ISendAsyncable.SendAsync

从结果值返回异常是有意义的:

///实际使用中的例子

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);
    }
}

2. ISendAsyncable.SendAsyncSafeAwait

方法签名:

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)
    {

    }
}

3. 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;
}

注意: 异步发送方法等待的返回值虽然也是接收到的消息,但是会被分发到异步函数回调中,不会触发本函数。即使异步发送方法没有使用await关键字而导致异步后续没有注册,返回消息也不会触发本函数,返回消息将被忽略。 (事实上,很难实现找不到异步后续时将消息分发到此函数中。因为不持有返回的Task引用时,想要将消息转送到本回调函数,需要对Task增加额外的标记,生命周期难以控制,控制流会变得更难以理解。详细情况参阅源码RpcCallbackPool.CreateCheckTimeout)


4. 由发送端和消息协议控制的的响应机制

具体响应方式参考PreReceive函数源码,参考IPreReceiveable,ICmdOption,SendOption.Echo等。
Heartbeat,RTT,Timestamp Synchronization等功能都由此机制实现。



重要

报头

TotalLength(value including total length 4 byte)RpcIDCMDMSGIDBody
总长度(值包含总长度自身的4个字节)消息ID消息正文
Int32(int)Int32(int)Int16(short)Int32(int)byte[]
4byte4byte2byte4bytebyte[].Lenght

MessagePipeline是什么?

MessagePipeline 是 Megumin.Remote 分离出来的一部分功能。
它也可以理解为一个协议栈。
它决定了消息收发具体经过了哪些步骤,可以自定义MessagePipeline并注入到Remote,用来满足一些特殊需求。
例如:

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是什么?

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; }
    }
}

支持的序列化库(陆续添加中)

每个库有各自的限制,对IL2CPP支持也不同。框架会为每个支持的库写一个继承于MessageStandard/MessageLUT的新的MessageLUT.
由于各个序列化库对Span<byte>的支持不同,所以中间层可能会有轻微的性能损失.

对于序列化函数有三种形式:

  1. 代码生成器生成代码
    { protobuf ,MessagePack mpc.exe }
  2. 通过反射每个字段组合
    { protobuf-net .NET Standard 1.0 }
  3. JIT 生成
    { protobuf-net , MessagePack }

protobuf-net

protobuf

MessagePack



一些细节

3.0版本

时间和空间上的折衷

序列化之前无法确定消息大小,因此需要传递一个足够大的buffer到序列化层。如果不进行拷贝,直接将整个大buffer传递到发送层,由于异步特性,无法准确得知发送过程的生命周期,可能在发送层积累大量的大buffer,严重消耗内存,因此类库在序列化层和发送层之间做了一次拷贝。
2.0版本 使用IBufferWriter<byte>ReadOnlySequence<byte>解决了这个问题,效率更高。

效率

其他信息

这是写类库途中总结到的知识或者猜测:

Megumin.Remote是以MMORPG为目标实现的。对于非MMORPG游戏可能不是最佳选择。 在遥远的未来也许会针对不同游戏类型写出NetRemoteStandard的不同实现。

工程实现需要,但本库没有实现的功能

internal buffer / kernel buffer



友情链接

参考