C'est un Simple et facile à utiliser Bibliothèque de réseau.
Il s'agit d'une solution universelle pour les modules réseau. Conçu pour fournir une interface de niveau haut de gamme unifié pour les modules de réseau d'applications.
La bibliothèque de classe entière est divisée en plusieurs DLL. Autrement dit: NetRemoteStandard.dll est la norme, avec uniquement des définitions d'interface; Megumin.remote.dll est une implémentation. Analogise la relation entre dotnetstandard et dotnetcore.
Pourquoi se diviser en plusieurs DLL?
Une implémentation spécifique peut nécessiter une dépendance à l'égard de nombreuses autres DLL, et la définition de l'interface ne nécessite pas ces dépendances. Pour les utilisateurs qui souhaitent uniquement utiliser des interfaces et des implémentations personnalisées, l'introduction de dépendances supplémentaires n'est pas nécessaire. Par exemple, MessageStandard, les utilisateurs ne peuvent se référer à la bibliothèque de sérialisation qu'ils choisissent, sans avoir à se référer à plusieurs bibliothèques de sérialisation.

Oui, utilisez Nuget pour obtenir Megumin.remote. Cependant, notez que vous devez faire correspondre la bibliothèque de sérialisation, et différentes bibliothèques de sérialisation peuvent avoir des exigences supplémentaires. En raison de l'utilisation de la syntaxe C # 7.3, au moins 2018.3 est requise si le code source est utilisé dans l'unité.
Le framework cible netstandard2.1 est recommandé d'être dans la version 2021.2 d'Unity ou supérieure. Le code source peut être utilisé pour des versions trop petites, mais les dépendances doivent être résolues par vous-même.

ou ajoutez "com.megumin.net": "https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net" à Packages/manifest.json .
Si vous souhaitez définir une version cible, utilisez la balise
*.*.*Relexe afin que vous puissiez spécifier une version comme#2.1.0. Par exemple,https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.
Span<T> . Utilisez System.IO.Pipelines comme tampon IO haute performance.MIT许可证 Principe de conception: Le code le plus utilisé est le plus simplifié et toutes les pièces complexes sont encapsulées.发送一个消息,并等待一个消息返回est l'ensemble du contenu de la bibliothèque de classe.
Il est logique de renvoyer une exception de la valeur du résultat:
///实际使用中的例子
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 ) ;
}
} Signature de la méthode:
ValueTask < Result > SendAsyncSafeAwait < Result > ( object message , object options = null , Action < Exception > onException = null ) ; La valeur du résultat est garantie d'avoir une valeur. Si la valeur de résultat est vide ou d'autres exceptions, la fonction de rappel des exceptions ne sera pas lancée, il n'est donc pas nécessaire d'essayer Catch.异步方法的后续部分不会触发, de sorte que la partie suivante peut être éliminée des vérifications vides.
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体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 ) ;
}Bien qu'il ne soit pas recommandé qu'une demande correspond à plusieurs types de réponses, certaines conceptions d'entreprise ont toujours cette exigence. Par exemple, si tous les codes d'erreur sont répondus comme un type indépendant, une demande peut avoir deux types de réponses: réponses correspondantes et code d'erreur.
IMessage接口peut être utilisée comme type pour attendre le retour dans le protocole Protobuf.
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);Fonction de rappel du récepteur
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 ;
} Pour des méthodes de réponse spécifiques, reportez-vous au code source de la fonction PreReceive , reportez-vous à iPrereceiveable, ICMDOPTION, SENDOPTION.ECHO, etc.
Le rythme cardiaque, le RTT, la synchronisation des horodatages et d'autres fonctions sont tous mis en œuvre par ce mécanisme.
PreReceive et appelle GetResponse pour renvoyer le résultat à l'expéditeur. public interface IAutoResponseable : IPreReceiveable
{
ValueTask < object > GetResponse ( object request ) ;
} Planification de threads
Remote utilise bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message) pour déterminer quel thread la fonction de rappel des messages est exécutée. Lorsque cela est vrai, tous les messages sont résumés en megumin.threadscheduler.update.
Vous devez interroger cette fonction pour gérer le rappel reçu, ce qui garantit que le rappel est déclenché dans l'ordre des messages reçus (s'il est hors service, soumettez un bogue). FixedUpdate doit généralement être utilisé dans l'unité.如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么lorsqu'il est faux, le rappel reçu est exécuté à l'aide de la tâche, et vous n'avez pas à attendre dans le sondage, mais il ne peut pas être garanti d'être ordonné et ne peut pas avoir à la fois de poisson et de patte d'ours.
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool . QueueUserWorkItem ( ( A ) =>
{
while ( true )
{
ThreadScheduler . Update ( 0 ) ;
Thread . Yield ( ) ;
}
} ) ; Message.dll
(AOT / IL2CPP) Lorsque la classe sérialisée est importée dans Unity sous la forme de DLL (car la bibliothèque de classe de messages est parfois conçue comme un projet partagé en dehors de l'unité), un fichier de liaison doit être ajouté pour empêcher les méthodes GET et SET des attributs de classe sérialisés d'être coupés par IL2CPP.重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| TOTALLONGTH (valeur comprenant la longueur totale de 4 octets) | RPCID | CMD | Msgide | Corps |
|---|---|---|---|---|
| Longueur totale (la valeur contient 4 octets de la longueur totale elle-même) | ID de message | Texte du message | ||
| Int32 (int) | Int32 (int) | Int16 (court) | Int32 (int) | octet[] |
| 4 octets | 4 octets | 2bye | 4 octets | octet []. |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。 MessagePipeline fait partie des fonctions séparées par Megumin.remote.
Il peut également être compris comme une pile de protocole.
Il détermine quelles étapes spécifiques sont utilisées pour envoyer et recevoir des messages. Vous pouvez personnaliser le MessagePipeline et l'injecter dans la distance pour répondre à certains besoins spéciaux.
Par exemple:
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。La version 2.0 a supprimé MessagePipeline et l'a changée en une fonction réécrit dans plusieurs implémentations distantes. Dans la pratique de l'ingénierie, il a été constaté qu'il n'avait pas de sens de détacher le pipeline de messages à distance et a été sur-conçu. Si vous devez personnaliser le pipeline de trois protocoles à distance en même temps, l'utilisateur peut le diviser par lui-même et le cadre ne sera pas traité.
人生就是反反复复。
La version 3.0 a décidé de revenir à la conception originale, et l'idée de conception de la première version est meilleure.
Après la pratique de l'ingénierie, il a été constaté que la conception de 2.0 n'est pas pratique de réécrire. Les utilisateurs doivent réécrire plusieurs copies du même code de réécriture pour différents protocoles, héritant de TCPremote, UDPremote et KCPremote. Chaque fois qu'ils modifient, plusieurs copies doivent être modifiées en même temps, ce qui est très volumineux.
L'utilisateur réécrit principalement la partie du message de réception et la pièce déconnectée, et la pièce de reconnexion déconnectée est également gérée différemment pour différents protocoles.
Donc, divisez le transport et iDisconnecthandler de la distance.
Essentiellement, la télécommande de 3.0 est égale à MessagePipeline de 1.0. Le transport de 3,0 est égal à la distance de 1,0.
MessageLut (Message Serialize Deserialize Callback Table Table) est la classe principale de MessageStandard. MessagePipeline est sérialisé en recherchant des fonctions enregistrées dans MessageLut.因此在程序最开始你需要进行函数注册.
Fonction d'enregistrement générale:
void RegistIMeguminFormatter < T > ( KeyAlreadyHave key = KeyAlreadyHave . Skip ) where T : class , IMeguminFormatter , new ( ) Le middleware de la bibliothèque de classe de sérialisation fournit plusieurs API simples et faciles à utiliser basées sur MessageLut, générant automatiquement des fonctions de sérialisation et de désérialisation. Vous devez ajouter un msgidAttribute à la classe de protocole pour fournir l'ID utilisé par la table de recherche. Étant donné qu'un ID ne peut correspondre qu'à un seul ensemble de fonctions de sérialisation, chaque classe de protocole ne peut utiliser qu'une seule bibliothèque de sérialisation en même temps.
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 ; }
}
}Un assemblage peut être enregistré directement dans l'environnement 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 ( ) ;
}
} ) ;
} Dans AOT/IL2CPP , il est nécessaire显示l'enregistrement de chaque classe de protocole via des fonctions génériques pour s'assurer que les fonctions génériques correspondantes sont générées lors de l'analyse statique AOT/IL2CPP编译器.
public void TestDefine ( )
{
Protobuf_netLUT . Regist < Login > ( ) ;
Protobuf_netLUT . Regist < LoginResult > ( ) ;
} Avis:序列化库utilise代码生成器生成代码, qui est une fonction de sérialisation qui génère le type réel.
Il s'agit de générer des fonctions génériques pour l'API générale de la bibliothèque de classe sérialisée pendant l'analyse statique.
Par exemple:
ProtoBuf.Serializer.Serialize<T>()est généré commeProtoBuf.Serializer.Serialize<Login>()
Les deux sont différents.
Chaque bibliothèque a ses propres limitations et le support pour IL2CPP est également différent. Le framework rédigera un nouveau MessageLut hérité de MessageStandard / MessageLut pour chaque bibliothèque prise en charge.
Étant donné que chaque bibliothèque de sérialisation prend en charge Span<byte> différemment, il peut y avoir une légère perte de performances dans la couche moyenne.
Il existe trois formes pour les fonctions de sérialisation:
RPC功能: assure une correspondance individuelle des demandes et des messages de retour. RPCID est négatif lors de l'envoi, RPCID * -1 est positif lors du retour, et positif et négatif est utilisé pour distinguer les lignes de haut en bas et de baisse.内存分配: réduisez l'alloc à l'aide内存池.内存池: Pool de mémoire de la bibliothèque standard, ArrayPool<byte>.Shared .序列化: utilisez le type pour effectuer la fonction de recherche de clé.反序列化: utilisez MSGID (INT) pour faire la fonction de recherche de clé.MessageLUT.Regist<T> ajoute manuellement d'autres types.消息类型: essayez de ne pas utiliser de grandes structures personnalisées, car l'ensemble du processus de sérialisation有可能conduire à plusieurs emballages et déballage. Pendant le processus de transfert de paramètres, il sera copié plusieurs fois et les performances sont inférieures à celles de la classe.<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks> .时间和空间上的折衷La taille du message ne peut pas être déterminée avant la sérialisation, donc un tampon suffisamment grand doit être transmis à la couche de sérialisation. Si vous ne copiez pas, l'ensemble du grand tampon sera directement transmis à la couche d'envoi. En raison de la nature asynchrone, il est impossible de connaître avec précision le cycle de vie du processus d'envoi. Un grand nombre de grands tampons peuvent être accumulés dans la couche d'envoi, qui consomme une mémoire grave. Par conséquent, la bibliothèque fait une copie entre la couche de sérialisation et la couche d'envoi.
La version 2.0 résout ce problème à l'aide IBufferWriter<byte> et ReadOnlySequence<byte> , ce qui est plus efficace.
Ceci est la connaissance ou la supposition résumé lors de la bibliothèque d'écriture:
Megumin.remote est atteint dans le but de MMORPG. Ce n'est peut-être pas le meilleur choix pour les jeux non MMORPG. Dans un avenir lointain, différentes implémentations de NetRemoteStandard peuvent être écrites pour différents types de jeux.
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm