Megumin.Net
应用程序和游戏网络模块解决方案
Install / Use
/learn @KumoKyaku/Megumin.NetREADME
这是什么?
这是一个 ~~简单易用的~~ 网络库。
这是一个网络模块的通用解决方案。设计目的为应用程序网络模块提供统一的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

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.
快速入门
- bilibili视频教程
<img src="http://i2.hdslb.com/bfs/archive/7f79a0fe4f98624abb1619dfb6a34090fcca4967.png" width = "480" height = "270">
优势
- 支持Tcp,Udp,Kcp。
- 使用内存池和多线程处理收发,可配置线程调度,无需担心网络模块性能问题。
- 内置Rpc。
- 可以搭配不同的序列化类库,甚至不用序列化库。
- AOT/IL2CPP可用。
- 可重写的消息管线,专业程序员可以针对具体功能进一步优化。
- 接口分离。[Dependency injection] 应用程序可以仅使用NetRemoteStandard.dll编码,然后使用Megumin.Remote.dll的具体实现类注入,当需要切换协议或者序列化类库时,应用程序逻辑无需改动。
- IOCP开销和消息调度转发延迟之间有很好的平衡。
- 自定义MiniTask池,针对网络功能对Task重新实现,性能更高,仅初始化时alloc。
- 支持
Span<T>。使用System.IO.Pipelines作为高性能IO缓冲区。 - 纯C#实现,这是学习网络功能一个好的起点。
- 3.0 版本 API设计经过真实业务需求改良。
MIT许可证
劣势
- ~~目前为止类库还很年青,没有经过足够的商业项目测试。~~
- 不持支 WebGL Networking
- 对于非程序人员仍然需要一些学习成本。独立游戏作者用起来还是有一定难度的。
- 类库没有解决操作系统的时间精度问题。这个问题非常复杂,需要专人定制。
核心方法3个
设计原则:最常用的代码最简化,复杂的地方都封装起来。
发送一个消息,并等待一个消息返回 是类库的全部内容。
1. ISendAsyncable.SendAsync
从结果值返回异常是有意义的:
- 省去了try catch ,写法更简单,避免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);
}
}
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等功能都由此机制实现。
- 在某些时候,比如测试消息是否正常收发,发送端可能希望远端做出特定方式的响应,比如echo,将消息原样返回。
这种需求不是针对某一个特定消息类型的,也不是对于某个消息类型永远做出这样的响应,可能仅仅是针对某个时刻的某条消息。
对于这样的需求,在OnReceive函数中实现并不合适,没有办法根据消息类型进行抽象。- 通过CmdID == 1 << 0来实现。发送时指定option参数,option实现ICmdOption,将CmdID传递到底层报头中。
- 通过IPreReceiveable.PreReceiveType == 1来实现,发送的消息协议实现IPreReceiveable接口,并且PreReceiveType的值等于1。
- 接收端在PreReceive函数中处理,并决定此消息是否继续传递到OnReceive函数中。
- 在另一些时候,更通用的是,发送端发出一个消息,但是处于一些特殊原因,不希望将响应函数写在OnReceive函数中。
可以通过消息协议继承IAutoResponseable接口实现,并且PreReceiveType == 2。
接收端PreReceive函数中处理此类消息,并调用GetResponse返回结果到发送端。public interface IAutoResponseable : IPreReceiveable { ValueTask<object> GetResponse(object request); }- 比如消息协议是跨项目的,但是OnReceive函数不是。
- 比如一些简单并且通用的基础函数调用,不想污染OnReceive函数。GetTime,GetSystemInfo等。但又不想将这些功能内置到网络模块中。
重要
-
线程调度
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>
报头
- Udp,Kcp 不用处理粘包,所以报头不含有TotalLength,TotalLength改为1字节的消息种类识别码,具体参照源码。
- 使用小端字节序写入报头。BinaryPrimitives.WriteInt32LittleEndian。
- TotalLength = 4 + 4 + 2 + 4 + bodyLength。
| 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#语言时。满足报头格式,即可支持本库所有特性。
~~MessagePipeline是什么?~~
~~MessagePipeline 是 Megumin.Remote 分离出来的一部分功能。
它也可以理解为一个协议栈。
它决定了消息收发具体经过了哪些步骤,可以自定义MessagePipeline并注入到Remote,用来满足一些特殊需求。
例如:~~
- ~~消息反序列化前转发。~~
- ~~使用返回消息池来实现接收过程构造返回消息实例无Alloc(这需要序列化类库的支持和明确的生命周期管理)。
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。~~
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; }
}
}
-
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);
Related Skills
node-connect
339.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
339.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.8kCommit, push, and open a PR
