Dies ist a Einfach und einfach zu bedienen Netzwerkbibliothek.
Dies ist eine universelle Lösung für Netzwerkmodule. Entwickelt, um eine einheitliche Hochlevel -Schnittstelle für Anwendungsnetzwerkmodule bereitzustellen.
Die gesamte Klassenbibliothek ist in mehrere DLLs aufgeteilt. Einfach ausgedrückt: netremotestandard.dll ist der Standard mit nur Schnittstellendefinitionen; Megumin.remote.dll ist eine Implementierung. Analogisiert die Beziehung zwischen DotNetStandard und DotNetCore.
Warum sich in mehrere DLLs teilen?
Eine bestimmte Implementierung kann von vielen anderen DLLs abhängig sein, und die Schnittstellendefinition erfordert diese Abhängigkeiten nicht. Für Benutzer, die nur Schnittstellen und benutzerdefinierte Implementierungen verwenden möchten, ist die Einführung zusätzlicher Abhängigkeiten unnötig. Zum Beispiel können Benutzer Messagestandard nur auf die von ihnen ausgewählte Serialisierungsbibliothek verweisen, ohne sich auf mehrere Serialisierungsbibliotheken beziehen zu müssen.

Ja, verwenden Sie Nuget, um Megumin.remote zu erhalten. Beachten Sie jedoch, dass Sie jedoch die Serialisierungsbibliothek entsprechen müssen, und verschiedene Serialisierungsbibliotheken können zusätzliche Anforderungen haben. Aufgrund der Verwendung von C# 7.3 -Syntax ist mindestens 2018.3 erforderlich, wenn der Quellcode in Einheit verwendet wird.
Das Zielrahmen NetSpandard2.1 wird empfohlen, in der Einheit Version 2021.2 oder höher zu sein. Der Quellcode kann für zu kleine Versionen verwendet werden, aber die Abhängigkeiten müssen von Ihnen selbst behoben werden.

Oder fügen Sie "com.megumin.net": "https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net" zu Packages/manifest.json .
Wenn Sie eine Zielversion festlegen möchten, verwenden Sie das Tag
*.*.*, Damit Sie eine Version wie#2.1.0angeben können. Zum Beispielhttps://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.
Span<T> . Verwenden Sie System.IO.Pipelines als Hochleistungs-IO-Puffer.MIT许可证 Designprinzip: Der am häufigsten verwendete Code ist am meisten vereinfacht, und alle komplexen Teile sind eingekapselt.发送一个消息,并等待一个消息返回Der gesamte Inhalt der Klassenbibliothek ist.
Es ist sinnvoll, eine Ausnahme vom Ergebniswert zurückzugeben:
///实际使用中的例子
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 ) ;
}
} Methodensignatur:
ValueTask < Result > SendAsyncSafeAwait < Result > ( object message , object options = null , Action < Exception > onException = null ) ; Der Ergebniswert hat garantiert einen Wert. Wenn der Ergebniswert leer oder andere Ausnahmen ist, wird die Ausnahme -Rückruffunktion nicht geworfen, sodass es nicht erforderlich ist, Catch zu versuchen.异步方法的后续部分不会触发, sodass der nachfolgende Teil aus leeren Schecks beseitigt werden kann.
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体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 ) ;
}Obwohl nicht empfohlen wird, dass eine Anfrage mehreren Antworttypen entspricht, haben einige Geschäftsentwürfe diese Anforderung. Wenn beispielsweise alle Fehlercodes als unabhängiger Typ beantwortet werden, kann eine Anforderung zwei Antworten haben: entsprechende Antworten und Fehlercode.
IMessage接口kann als Typ verwendet werden, um auf die Rückkehr im Protobuf -Protokoll zu warten.
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 -Funktion
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 ;
} Für spezifische Antwortmethoden finden Sie im Quellcode für den Quellcode für PreReceive auf iPereCeivable, ICMDOption, sendoption.Echo usw.
Herzschlag, RTT, Zeitstempelsynchronisation und andere Funktionen werden durch diesen Mechanismus implementiert.
PreReceive und ruft GetResponse an, um das Ergebnis an den Absender zurückzugeben. public interface IAutoResponseable : IPreReceiveable
{
ValueTask < object > GetResponse ( object request ) ;
} Threadplanung
Remote verwendet bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message) , um zu bestimmen, welcher Thread die Meldung Rückruffunktion ausgeführt wird. Wenn es wahr ist, werden alle Nachrichten in megumin.Threadscheduler.update zusammengefasst.
Sie müssen diese Funktion befragen, um den empfangenen Rückruf zu verarbeiten, der sicherstellt, dass der Rückruf in der Reihenfolge der empfangenen Nachrichten ausgelöst wird (falls außerhalb der Reihenfolge, einen Fehler einreichen). FixedUpdate sollte normalerweise in der Einheit verwendet werden.如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么Wenn der falsche Rückruf empfangen wird, wird如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么mit der Aufgabe ausgeführt, und Sie müssen nicht in der Umfrage warten, aber er kann nicht garantiert ordentlich sein und kann nicht sowohl die Fisch- als auch die Bärenpfote haben.
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool . QueueUserWorkItem ( ( A ) =>
{
while ( true )
{
ThreadScheduler . Update ( 0 ) ;
Thread . Yield ( ) ;
}
} ) ; Message.dll
(AOT/IL2CPP) Wenn die serialisierte Klasse in Form von DLL in eine Einheit importiert wird (da die Nachrichtenklassenbibliothek manchmal als gemeinsames Projekt außerhalb der Einheit ausgelegt ist) muss eine Linkdatei hinzugefügt werden, um zu verhindern, dass die methoden der serialisierten Klassenattribute durch IL2CPP geschnitten werden.重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| Ausgefälle (Wert einschließlich Gesamtlänge 4 Byte) | Rpcid | CMD | Msgid | Körper |
|---|---|---|---|---|
| Gesamtlänge (Wert enthält 4 Bytes der Gesamtlänge selbst) | Nachrichten -ID | Nachrichtentext | ||
| Int32 (int) | Int32 (int) | Int16 (kurz) | Int32 (int) | Byte[] |
| 4Byte | 4Byte | 2Byte | 4Byte | Byte []. Lenght |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。 MessagePipeline ist Teil der von Megumin.Remote getrennten Funktionen.
Es kann auch als Protokollstapel verstanden werden.
Es bestimmt, welche spezifischen Schritte zum Senden und Empfangen von Nachrichten verwendet werden. Sie können die MessagePipeline anpassen und in eine Fernbedienung injizieren, um einige besondere Bedürfnisse zu erfüllen.
Zum Beispiel:
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。Version 2.0 Löschte MessagePipeline und geändert sie in mehreren Remote -Implementierungen in eine umschreibensbare Funktion. In der Ingenieurpraxis wurde festgestellt, dass es bedeutungslos war, die Meldungspipeline von der Fernbedienung zu lösen, und war überentwickelt. Wenn Sie die Pipeline von drei Protokollen gleichzeitig anpassen müssen, kann der Benutzer sie selbst aufteilen und das Framework wird nicht verarbeitet.
人生就是反反复复。
Version 3.0 hat beschlossen, wieder in das ursprüngliche Design zu wechseln, und die Designidee der ersten Version ist besser.
Nach der Ingenieurpraxis wurde festgestellt, dass das Design von 2,0 nicht bequem umgeschrieben ist. Benutzer müssen mehrere Kopien desselben Umschreibecode für verschiedene Protokolle umschreiben und von TCPremote, UDpremote und KCPremote erben. Jedes Mal, wenn sie ändern, müssen mehrere Kopien gleichzeitig geändert werden, was sehr sperrig ist.
Der Benutzer schreibt hauptsächlich den empfangenden Nachrichtenteil und den getrennten Teil um, und der getrennte Wiederverbindungsteil wird auch für verschiedene Protokolle unterschiedlich behandelt.
Teilen Sie also den Transport und den Idisconnecthandler von der Fernbedienung auf.
Im Wesentlichen entspricht die Fernbedienung von 3.0 MessagePipeline von 1,0. Der Transport von 3,0 entspricht einer Fernbedienung von 1,0.
Messagelut (Message Serialize Deserialize Callback-Nachschlagtabelle) ist die Kernklasse von Messagestandard. MessagePipeline wird serialisiert, indem nach Funktionen in Messagelut gesucht wird.因此在程序最开始你需要进行函数注册.
Allgemeine Registrierungsfunktion:
void RegistIMeguminFormatter < T > ( KeyAlreadyHave key = KeyAlreadyHave . Skip ) where T : class , IMeguminFormatter , new ( ) Die Middleware der Serialisierungsklassenbibliothek bietet mehrere einfache und benutzerfreundliche APIs, die auf Messagelut basieren und automatisch Serialisierungs- und Deserialisierungsfunktionen generieren. Sie müssen der Protokollklasse eine msgidattribute hinzufügen, um die von der Nachschlagtabelle verwendete ID bereitzustellen. Da eine ID nur einem Satz von Serialisierungsfunktionen entsprechen kann, kann jede Protokollklasse nur eine Serialisierungsbibliothek gleichzeitig verwenden.
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 ; }
}
}Eine Versammlung kann direkt unter der JIT -Umgebung registriert werden
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 -Umgebung müssen die Registrierung jeder Protokollklasse durch generische Funktionen显示, um sicherzustellen, dass die entsprechenden generischen Funktionen während der statischen Analyse AOT/IL2CPP编译器erzeugt werden.
public void TestDefine ( )
{
Protobuf_netLUT . Regist < Login > ( ) ;
Protobuf_netLUT . Regist < LoginResult > ( ) ;
} Beachten:序列化库verwendet代码生成器生成代码, eine Serialisierungsfunktion, die den tatsächlichen Typ generiert.
Dies dient dazu, generische Funktionen für die allgemeine API der serialisierten Klassenbibliothek während der statischen Analyse zu generieren.
Zum Beispiel:
ProtoBuf.Serializer.Serialize<T>()wird alsProtoBuf.Serializer.Serialize<Login>()erzeugt.
Die beiden sind unterschiedlich.
Jede Bibliothek hat ihre eigenen Einschränkungen, und die Unterstützung für IL2CPP ist ebenfalls unterschiedlich. Das Framework schreibt eine neue Messagelut, die für jede unterstützte Bibliothek von Messagestandard/Messagelut geerbt wurde.
Da jede Serialisierungsbibliothek Span<byte> unterschiedlich unterstützt, kann es in der mittleren Schicht einen leichten Leistungsverlust geben.
Es gibt drei Formen für Serialisierungsfunktionen:
RPC功能: Stellen Sie sicher, dass Eins-zu-Eins-Anpassungen von Anforderungen und Rückgabemeldungen gewährleistet werden. RPCID ist beim Senden negativ, RPCID*-1 ist bei der Rückkehr positiv, und positiv und negativ werden verwendet, um auf und ab zu unterscheiden.内存分配: Reduzieren Sie Alloc mithilfe内存池.内存池: Standardbibliotheksportepool, ArrayPool<byte>.Shared .序列化: Verwenden Sie den Typ, um die wichtigste Lookup -Funktion auszuführen.反序列化: Verwenden Sie MSGID (INT), um die wichtigste Nachschlagfunktion auszuführen.MessageLUT.Regist<T> Funktion fügt manuell andere Typen hinzu.消息类型: Versuchen Sie, keine großen benutzerdefinierten Strukturen zu verwenden, da der gesamte Serialisierungsprozess zu mehreren Packungen und Unboxen führen有可能. Während des Parameterübertragungsprozesses wird es um ein Vielfaches kopiert und die Leistung ist niedriger als die der Klasse.<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks> .时间和空间上的折衷Die Nachrichtengröße kann vor der Serialisierung nicht ermittelt werden, sodass ein groß genuges Puffer an die Serialisierungsschicht übergeben werden muss. Wenn Sie nicht kopieren, wird der gesamte große Puffer direkt an die Sendungsschicht übergeben. Aufgrund der asynchronen Natur ist es unmöglich, den Lebenszyklus des Sendungsprozesses genau zu kennen. Eine große Anzahl großer Puffer kann in der Versandschicht akkumuliert werden, die ein schweres Gedächtnis verbraucht. Daher stellt die Bibliothek eine Kopie zwischen der Serialisierungsschicht und der Sendungsschicht her.
Version 2.0 löst dieses Problem mit IBufferWriter<byte> und ReadOnlySequence<byte> , was effizienter ist.
Dies ist das Wissen oder die Vermutung, die während der Schreibbibliothek zusammengefasst sind:
Megumin.remote wird mit dem Ziel von MMORPG erreicht. Es ist möglicherweise nicht die beste Wahl für Nicht-MMORPG-Spiele. In der fernen Zukunft können verschiedene Implementierungen von Netremotestandard für verschiedene Spieltypen geschrieben werden.
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm