This is a Simple and easy to use Network library.
This is a universal solution for network modules. Designed to provide a unified HighLevel interface for application network modules.
The entire class library is split into multiple dlls. Simply put: NetRemoteStandard.dll is the standard, with only interface definitions; Megumin.Remote.dll is an implementation. Analogizes the relationship between dotnetStandard and dotnetCore.
Why split into multiple dlls?
A specific implementation may require dependence on many other dlls, and the interface definition does not require these dependencies. For users who only want to use interfaces and custom implementations, introducing additional dependencies is unnecessary. For example, MessageStandard, users can only refer to the serialization library they choose, without having to refer to multiple serialization libraries.

Yes, use Nuget to get Megumin.Remote. However, note that you need to match the serialization library, and different serialization libraries may have additional requirements. Due to the use of C# 7.3 syntax, at least 2018.3 is required if the source code is used in unity.
The target framework netstandard2.1 is recommended to be in unity version 2021.2 or above. The source code can be used for too small versions, but the dependencies need to be resolved by yourself.

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, use 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> . Use System.IO.Pipelines as a high-performance IO buffer.MIT许可证 Design principle: The most commonly used code is the most simplified, and all the complex parts are encapsulated.发送一个消息,并等待一个消息返回is the entire content of the class library.
It makes sense to return an exception from the result value:
///实际使用中的例子
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 ) ;
}
} Method signature:
ValueTask < Result > SendAsyncSafeAwait < Result > ( object message , object options = null , Action < Exception > onException = null ) ; The result value is guaranteed to have a value. If the result value is empty or other exceptions, the exception callback function will not be thrown, so there is no need to try catch.异步方法的后续部分不会触发, so the subsequent part can be eliminated from empty checks.
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体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 ) ;
}Although it is not recommended that one request corresponds to multiple reply types, some business designs still have this requirement. For example, if all errorcodes are replied as an independent type, a request may have two replies types: corresponding replies and errorcode.
IMessage接口can be used as the type to wait for return in the protobuf protocol.
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);Receiver callback function
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 ;
} For specific response methods, refer to the PreReceive function source code, refer to IPreReceiveable, ICmdOption, SendOption.Echo, etc.
Heartbeat, RTT, Timestamp Synchronization and other functions are all implemented by this mechanism.
PreReceive function, and calls GetResponse to return the result to the sender. public interface IAutoResponseable : IPreReceiveable
{
ValueTask < object > GetResponse ( object request ) ;
} Thread Scheduling
Remote uses bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message) function to determine which thread the message callback function is executed. When true, all messages are summarized into Megumin.ThreadScheduler.Update.
You need to poll this function to handle the received callback, which ensures that the callback is triggered in the order of received messages (if out of order, submit a bug). FixedUpdate should usually be used in Unity.如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么when false, the callback received is executed using Task, and you do not have to wait in the poll, but it cannot be guaranteed to be orderly and cannot have both fish and bear's paw.
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool . QueueUserWorkItem ( ( A ) =>
{
while ( true )
{
ThreadScheduler . Update ( 0 ) ;
Thread . Yield ( ) ;
}
} ) ; Message.dll
(AOT/IL2CPP) When the serialized class is imported into unity in the form of dll (because the message class library is sometimes designed as a shared project outside the unity), a link file must be added to prevent the get and set methods of the serialized class attributes from being cut by il2cpp.重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| TotalLength(value including total length 4 byte) | RpcID | CMD | MSGID | Body |
|---|---|---|---|---|
| Total length (value contains 4 bytes of the total length itself) | Message ID | Message text | ||
| Int32 (int) | Int32 (int) | Int16(short) | Int32 (int) | byte[] |
| 4byte | 4byte | 2byte | 4byte | byte[].Lenght |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。 MessagePipeline is a part of the functions separated by Megumin.Remote.
It can also be understood as a protocol stack.
It determines what specific steps are used for sending and receiving messages. You can customize the MessagePipeline and inject it into Remote to meet some special needs.
For example:
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。Version 2.0 deleted MessagePipeline and changed it to a rewritable function in multiple Remote implementations. In engineering practice, it was found that it was meaningless to detach the message pipeline from Remote and was over-designed. If you need to customize the pipeline of three protocols Remote at the same time, the user can split it up by himself and the framework will not be processed.
人生就是反反复复。
Version 3.0 has decided to change back to the original design, and the design idea of the first version is better.
After engineering practice, it was found that the design of 2.0 is not convenient to rewrite. Users need to rewrite multiple copies of the same rewrite code for different protocols, inheriting from TcpRemote, UdpRemote, and Kcpremote. Each time they modify, multiple copies must be modified at the same time, which is very bulky.
The user mainly rewrites the receiving message part and the disconnected part, and the disconnected reconnection part is also handled differently for different protocols.
So split the Transport and IDisconnectHandler from Remote.
Essentially, Remote of 3.0 is equal to MessagePipeline of 1.0. Transport of 3.0 is equal to Remote of 1.0.
MessageLUT (Message Serialize Deserialize callback look-up table) is the core class of MessageStandard. MessagePipeline is serialized by looking for functions registered in MessageLUT.因此在程序最开始你需要进行函数注册.
General registration function:
void RegistIMeguminFormatter < T > ( KeyAlreadyHave key = KeyAlreadyHave . Skip ) where T : class , IMeguminFormatter , new ( ) The middleware of the serialization class library provides multiple simple and easy-to-use APIs based on MessageLUT, automatically generating serialization and deserialization functions. You need to add an MSGIDAttribute to the protocol class to provide the ID used by the lookup table. Because an ID can only correspond to one set of serialization functions, each protocol class can only use one serialization library at the same time.
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 ; }
}
}A assembly can be registered directly under the JIT environment
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 ( ) ;
}
} ) ;
} In AOT/IL2CPP environment, it is necessary显示the registration of each protocol class through generic functions to ensure that the corresponding generic functions are generated during static analysis AOT/IL2CPP编译器.
public void TestDefine ( )
{
Protobuf_netLUT . Regist < Login > ( ) ;
Protobuf_netLUT . Regist < LoginResult > ( ) ;
} Notice:序列化库uses代码生成器生成代码, which is a serialization function that generates the actual type.
This is to generate generic functions for the general API of serialized class library during static analysis.
For example:
ProtoBuf.Serializer.Serialize<T>()is generated asProtoBuf.Serializer.Serialize<Login>()
The two are different.
Each library has its own limitations, and the support for IL2CPP is also different. The framework will write a new MessageLUT inherited from MessageStandard/MessageLUT for each supported library.
Since each serialization library supports Span<byte> differently, there may be a slight performance loss in the middle layer.
There are three forms for serialization functions:
RPC功能: Ensures one-to-one matching of requests and return messages. RPCID is negative when sending, RPCID*-1 is positive when returning, and positive and negative are used to distinguish up and down lines.内存分配: reduce alloc by using内存池.内存池: Standard library memory pool, ArrayPool<byte>.Shared .序列化: Use type to do the Key lookup function.反序列化: Use MSGID(int) to do the Key lookup function.MessageLUT.Regist<T> function manually adds other types.消息类型: Try not to use large custom structs, as the entire serialization process有可能lead to multiple packing and unboxing. During the parameter transfer process, it will be copied many times, and the performance is lower than that of class.<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks> .时间和空间上的折衷The message size cannot be determined before serialization, so a buffer large enough needs to be passed to the serialization layer. If you do not copy, the entire large buffer will be directly passed to the sending layer. Due to the asynchronous nature, it is impossible to accurately know the life cycle of the sending process. A large number of large buffers may be accumulated in the sending layer, which consumes severe memory. Therefore, the library makes a copy between the serialization layer and the sending layer.
Version 2.0 solves this problem using IBufferWriter<byte> and ReadOnlySequence<byte> , which is more efficient.
This is the knowledge or guess summarized during the writing library:
Megumin.Remote is achieved with the goal of MMORPG. It may not be the best choice for non-MMORPG games. In the distant future, different implementations of NetRemoteStandard may be written for different game types.
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm