這是一個簡單易用的網絡庫。
這是一個網絡模塊的通用解決方案。設計目的為應用程序網絡模塊提供統一的HighLevel接口。
整個類庫被拆分為多個dll。簡單來說:NetRemoteStandard.dll是標準,裡面只有接口定義;Megumin.Remote.dll是一種實現。類比於dotnetStandard和dotnetCore的關係。
為什麼要拆分為多個dll?
具體實現可能需要依賴很多其他dll,而接口定義並不需要這些依賴。對於只想使用接口,自定義實現的用戶來說,引入額外的依賴是不必要的。例如MessageStandard,用戶僅引用自己選擇的序列化庫即可,而不必引用多個序列化庫。

是的,使用Nuget獲取Megumin.Remote。但是注意,需要搭配序列化庫,不同的序列化庫可能有額外的要求。由於使用了C# 7.3語法,在unity中如果使用源碼至少需要2018.3。
目標框架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许可证設計原則:最常用的代碼最簡化,複雜的地方都封裝起來。发送一个消息,并等待一个消息返回是類庫的全部內容。
從結果值返回異常是有意義的:
///实际使用中的例子
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#语言时。满足报头格式,即可支持本库所有特性。 MessagePipeline 是Megumin.Remote 分離出來的一部分功能。
它也可以理解為一個協議棧。
它決定了消息收發具體經過了哪些步驟,可以自定義MessagePipeline並註入到Remote,用來滿足一些特殊需求。
例如:
你可以为每个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(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> 。时间和空间上的折衷序列化之前無法確定消息大小,因此需要傳遞一個足夠大的buffer到序列化層。如果不進行拷貝,直接將整個大buffer傳遞到發送層,由於異步特性,無法準確得知發送過程的生命週期,可能在發送層積累大量的大buffer,嚴重消耗內存,因此類庫在序列化層和發送層之間做了一次拷貝。
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