Este es un Simple y fácil de usar Biblioteca de red.
Esta es una solución universal para módulos de red. Diseñado para proporcionar una interfaz de alto nivel unificada para módulos de red de aplicaciones.
Toda la biblioteca de clases se divide en múltiples DLL. En pocas palabras: netremotestandard.dll es el estándar, con solo definiciones de interfaz; Megumin.remote.dll es una implementación. Analogiza la relación entre DotnetStandard y DotnetCore.
¿Por qué dividirse en múltiples DLL?
Una implementación específica puede requerir dependencia de muchas otras DLL, y la definición de interfaz no requiere estas dependencias. Para los usuarios que solo desean usar interfaces e implementaciones personalizadas, la introducción de dependencias adicionales es innecesaria. Por ejemplo, MessageStandard, los usuarios solo pueden referirse a la biblioteca de serialización que eligen, sin tener que consultar múltiples bibliotecas de serialización.

Sí, use Nuget para obtener Megumin.remote. Sin embargo, tenga en cuenta que debe coincidir con la biblioteca de serialización, y diferentes bibliotecas de serialización pueden tener requisitos adicionales. Debido al uso de la sintaxis C# 7.3, se requiere al menos 2018.3 si el código fuente se usa en Unity.
Se recomienda que el marco de destino NetStandard2.1 esté en Unity versión 2021.2 o superior. El código fuente se puede usar para versiones demasiado pequeñas, pero las dependencias deben resolverse usted mismo.

O agregue "com.megumin.net": "https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net" a Packages/manifest.json .
Si desea establecer una versión de destino, use la etiqueta de lanzamiento
*.*.*Para que pueda especificar una versión como#2.1.0. Por ejemplo,https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.
Span<T> . Use System.IO.Pipelines como un búfer IO de alto rendimiento.MIT许可证 Principio de diseño: el código más utilizado es el más simplificado y todas las partes complejas están encapsuladas.发送一个消息,并等待一个消息返回es todo el contenido de la biblioteca de clases.
Tiene sentido devolver una excepción del valor del resultado:
///实际使用中的例子
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 ) ;
}
} Firma del método:
ValueTask < Result > SendAsyncSafeAwait < Result > ( object message , object options = null , Action < Exception > onException = null ) ; Se garantiza que el valor del resultado tiene un valor. Si el valor del resultado está vacío u otras excepciones, la función de devolución de llamada de excepción no se lanzará, por lo que no hay necesidad de probar.异步方法的后续部分不会触发, por lo que la parte posterior puede eliminarse de los controles vacíos.
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体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 ) ;
}Aunque no se recomienda que una solicitud corresponda a múltiples tipos de respuesta, algunos diseños comerciales aún tienen este requisito. Por ejemplo, si todos los códigos de error se responden como un tipo independiente, una solicitud puede tener dos tipos de respuestas: respuestas correspondientes y código de error.
IMessage接口se puede usar como el tipo para esperar la devolución en el protocolo 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);Función de devolución de llamada del receptor
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 ;
} Para obtener métodos de respuesta específicos, consulte el código fuente de la función PreReceive , consulte IPereceivable, ICMDOPTION, SendOption.echo, etc.
Heartbeat, RTT, sincronización de marca de tiempo y otras funciones son implementadas por este mecanismo.
PreReceive y llama a GetResponse para devolver el resultado al remitente. public interface IAutoResponseable : IPreReceiveable
{
ValueTask < object > GetResponse ( object request ) ;
} Programación de hilos
El control remoto usa la función bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message) para determinar qué hilo se ejecuta la función de devolución de llamada del mensaje. Cuando es cierto, todos los mensajes se resumen en meguminum.threadscheduler.update.
Debe encuestar esta función para manejar la devolución de llamada recibida, lo que garantiza que la devolución de llamada se active en el orden de los mensajes recibidos (si está fuera de servicio, envíe un error). Se debe usar fijación fija generalmente en la unidad.如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么, cuando falsa, la devolución de llamada recibida se ejecuta usando tarea, y no tiene que esperar en la encuesta, pero no se puede garantizar que sea ordenada y no puede tener la pata de los peces y el oso.
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool . QueueUserWorkItem ( ( A ) =>
{
while ( true )
{
ThreadScheduler . Update ( 0 ) ;
Thread . Yield ( ) ;
}
} ) ; Message.dll
(AOT/IL2CPP) Cuando la clase serializada se importa a la unidad en forma de DLL (porque la biblioteca de clase de mensajes a veces se diseña como un proyecto compartido fuera de la unidad), se debe agregar un archivo de enlace para evitar que los métodos GET y establecer los atributos de clase serializados sean cortados por IL2CPP.重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| Totallength (valor que incluye longitud total 4 byte) | Rpcid | CMD | Noble | Cuerpo |
|---|---|---|---|---|
| Longitud total (el valor contiene 4 bytes de la longitud total en sí) | ID de mensaje | Mensaje de texto | ||
| Int32 (int) | Int32 (int) | Int16 (corto) | Int32 (int) | byte[] |
| 4byte | 4byte | 2byte | 4byte | byte []. Lenght |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。 MessagePipeline es parte de las funciones separadas por Megumin.remote.
También se puede entender como una pila de protocolo.
Determina qué pasos específicos se utilizan para enviar y recibir mensajes. Puede personalizar el MessagePipeline e inyectarlo en remoto para satisfacer algunas necesidades especiales.
Por ejemplo:
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。Versión 2.0 MessagePipeline eliminado y lo cambió a una función regritable en múltiples implementaciones remotas. En la práctica de ingeniería, se descubrió que no tenía sentido separar la tubería de mensajes del control remoto y se diseñó en exceso. Si necesita personalizar la tubería de tres protocolos remotos al mismo tiempo, el usuario puede dividirlo solo y el marco no se procesará.
人生就是反反复复。
La versión 3.0 ha decidido volver al diseño original, y la idea de diseño de la primera versión es mejor.
Después de la práctica de ingeniería, se descubrió que el diseño de 2.0 no es conveniente para reescribir. Los usuarios deben reescribir múltiples copias del mismo código de reescritura para diferentes protocolos, heredando de TCPremote, UDPremote y KCPremote. Cada vez que se modifican, varias copias deben modificarse al mismo tiempo, lo cual es muy voluminoso.
El usuario reescribe principalmente la parte del mensaje receptor y la parte desconectada, y la parte de reconexión desconectada también se maneja de manera diferente para diferentes protocolos.
Así que divida el transporte e idisConnectthandler desde el control remoto.
Esencialmente, el control remoto de 3.0 es igual a MessagePipeline de 1.0. El transporte de 3.0 es igual al control remoto de 1.0.
Messagelut (Message Serialize Deserialize la tabla de búsqueda de devolución de llamada) es la clase central de MessageStandard. MessagePipeline se serializa buscando funciones registradas en Messagelut.因此在程序最开始你需要进行函数注册.
Función de registro general:
void RegistIMeguminFormatter < T > ( KeyAlreadyHave key = KeyAlreadyHave . Skip ) where T : class , IMeguminFormatter , new ( ) El middleware de la biblioteca de clases de serialización proporciona múltiples API simples y fáciles de usar basadas en Messagelut, generando automáticamente funciones de serialización y deserialización. Debe agregar un msgidattribute a la clase de protocolo para proporcionar la identificación utilizada por la tabla de búsqueda. Debido a que una ID solo puede corresponder a un conjunto de funciones de serialización, cada clase de protocolo solo puede usar una biblioteca de serialización al mismo tiempo.
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 ensamblaje se puede registrar directamente en el entorno 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 ( ) ;
}
} ) ;
} En AOT/IL2CPP , es necesario显示el registro de cada clase de protocolo a través de funciones genéricas para garantizar que las funciones genéricas correspondientes se generen durante el análisis estático AOT/IL2CPP编译器.
public void TestDefine ( )
{
Protobuf_netLUT . Regist < Login > ( ) ;
Protobuf_netLUT . Regist < LoginResult > ( ) ;
} Aviso:序列化库utiliza代码生成器生成代码, que es una función de serialización que genera el tipo real.
Esto es para generar funciones genéricas para la API general de la biblioteca de clases serializada durante el análisis estático.
Por ejemplo:
ProtoBuf.Serializer.Serialize<T>()se genera comoProtoBuf.Serializer.Serialize<Login>()
Los dos son diferentes.
Cada biblioteca tiene sus propias limitaciones, y el soporte para IL2CPP también es diferente. El marco escribirá un nuevo Messagelut heredado de Messagestandard/Messagelut para cada biblioteca compatible.
Dado que cada biblioteca de serialización admite Span<byte> de manera diferente, puede haber una ligera pérdida de rendimiento en la capa media.
Hay tres formas para funciones de serialización:
RPC功能: garantiza una coincidencia individual de solicitudes y mensajes de devolución. RPCID es negativo cuando se envía, RPCID*-1 es positivo al regresar, y se usan positivos y negativos para distinguir las líneas hacia arriba y hacia abajo.内存分配: Reduzca el ALSOC utilizando内存池.内存池: grupo de memoria estándar de la biblioteca, ArrayPool<byte>.Shared .序列化: use el tipo para hacer la función de búsqueda clave.反序列化: use msgid (int) para hacer la función de búsqueda clave.MessageLUT.Regist<T> La función agrega manualmente otros tipos.消息类型: intente no usar grandes estructuras personalizadas, ya que todo el proceso de serialización有可能conducir a múltiples empaquetado y desempaquetado. Durante el proceso de transferencia de parámetros, se copiará muchas veces, y el rendimiento es más bajo que el de la clase.<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks> .时间和空间上的折衷El tamaño del mensaje no se puede determinar antes de la serialización, por lo que se debe pasar un búfer lo suficientemente grande a la capa de serialización. Si no copia, todo el búfer grande se pasará directamente a la capa de envío. Debido a la naturaleza asincrónica, es imposible conocer con precisión el ciclo de vida del proceso de envío. Se puede acumular una gran cantidad de buffers grandes en la capa de envío, que consume memoria severa. Por lo tanto, la biblioteca hace una copia entre la capa de serialización y la capa de envío.
La versión 2.0 resuelve este problema usando IBufferWriter<byte> y ReadOnlySequence<byte> , que es más eficiente.
Este es el conocimiento o la suposición resumido durante la biblioteca de escritura:
Megumin.Remote se logra con el objetivo de MMORPG. Puede que no sea la mejor opción para los juegos que no son de MMORPG. En el futuro lejano, se pueden escribir diferentes implementaciones de Netremotestandard para diferentes tipos de juegos.
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm