Este é um Simples e fácil de usar Biblioteca de rede.
Esta é uma solução universal para módulos de rede. Projetado para fornecer uma interface de nível superior unificado para módulos de rede de aplicativos.
Toda a biblioteca de classes é dividida em várias DLLs. Simplificando: netremotestandard.dll é o padrão, com apenas definições de interface; Megumum.remote.dll é uma implementação. Analoga a relação entre DotnetStandard e Dotnetcore.
Por que dividir em várias DLLs?
Uma implementação específica pode exigir dependência de muitas outras DLLs, e a definição de interface não requer essas dependências. Para usuários que desejam apenas usar interfaces e implementações personalizadas, a introdução de dependências adicionais é desnecessária. Por exemplo, Messagesandard, os usuários só podem se referir à biblioteca de serialização que escolher, sem precisar se referir a várias bibliotecas de serialização.

Sim, use o NUGET para obter megum.remote. No entanto, observe que você precisa corresponder à biblioteca de serialização e diferentes bibliotecas de serialização podem ter requisitos adicionais. Devido ao uso da sintaxe C# 7.3, pelo menos 2018.3 é necessário se o código -fonte for usado na unidade.
Recomenda -se que a estrutura de destino NetStandard2.1 esteja na unidade versão 2021.2 ou acima. O código -fonte pode ser usado para versões muito pequenas, mas as dependências precisam ser resolvidas por você mesmo.

ou adicione "com.megumin.net": "https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net" para Packages/manifest.json .
Se você deseja definir uma versão de destino, use a tag
*.*.*Libere para que você possa especificar uma versão como#2.1.0. Por exemplo,https://github.com/KumoKyaku/Megumin.Net.git?path=UnityPackage/Packages/Net#2.1.0.
Span<T> . Use System.IO.Pipelines como um buffer IO de alto desempenho.MIT许可证 Princípio do design: o código mais usado é o mais simplificado e todas as partes complexas são encapsuladas.发送一个消息,并等待一个消息返回é todo o conteúdo da biblioteca de classes.
Faz sentido retornar uma exceção do valor do 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 ) ;
}
} Assinatura do método:
ValueTask < Result > SendAsyncSafeAwait < Result > ( object message , object options = null , Action < Exception > onException = null ) ; O valor do resultado é garantido para ter um valor. Se o valor do resultado estiver vazio ou outras exceções, a função de retorno de chamada de exceção não será lançada; portanto, não há necessidade de tentar capturar.异步方法的后续部分不会触发; portanto, a parte subsequente pode ser eliminada de verificações vazias.
(注意:这不是语言特性,也不是异步编程特性,这依赖于具体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 ) ;
}Embora não seja recomendável que uma solicitação corresponda a vários tipos de resposta, alguns designs de negócios ainda têm esse requisito. Por exemplo, se todos os códigos de erro forem respondidos como um tipo independente, uma solicitação poderá ter dois tipos de respostas: respostas correspondentes e código de erro.
IMessage接口pode ser usada como o tipo a aguardar o retorno no 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);Função de retorno de chamada do 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 métodos de resposta específicos, consulte o código -fonte da função PreReceive , consulte IPrereceabable, icmDoption, sendOption.echo, etc.
A batida cardíaca, RTT, sincronização de registro de data e hora e outras funções são implementadas por esse mecanismo.
PreReceive e chama o GetResponse para retornar o resultado ao remetente. public interface IAutoResponseable : IPreReceiveable
{
ValueTask < object > GetResponse ( object request ) ;
} Agenda do tópico
O Remote usa a função bool UseThreadSchedule(int rpcID, short cmd, int messageID, object message) para determinar qual encadeamento a função de retorno de chamada da mensagem é executada. Quando é verdade, todas as mensagens são resumidas em megum.threadscheduler.update.
Você precisa pesquisar essa função para lidar com o retorno de chamada recebido, o que garante que o retorno de chamada seja acionado na ordem das mensagens recebidas (se fora de ordem, envie um bug). FILLUPDATE geralmente deve ser usado na unidade.如果你的消息在分布式服务器之间传递,你可能希望消息在中转进程中尽快传递,那么, quando falso, o retorno de chamada recebido é executado usando a tarefa e você não precisa esperar na enquete, mas não pode ser garantido que seja ordenado e não pode ter peixes e pata de urso.
///建立主线程 或指定的任何线程 轮询。(确保在unity中使用主线程轮询)
///ThreadScheduler保证网络底层的各种回调函数切换到主线程执行以保证执行顺序。
ThreadPool . QueueUserWorkItem ( ( A ) =>
{
while ( true )
{
ThreadScheduler . Update ( 0 ) ;
Thread . Yield ( ) ;
}
} ) ; Message.dll
(AOT/IL2CPP) Quando a classe serializada é importada para a unidade na forma de DLL (porque a biblioteca da classe de mensagens às vezes é projetada como um projeto compartilhado fora da unidade), um arquivo de link deve ser adicionado para impedir que os métodos GET e Set dos atributos de classe serializados sejam cortados pelo IL2CPP.重中之重,因为缺失get,set函数不会报错,错误通常会被定位到序列化库的多个不同位置(我在这里花费了16个小时)。
<linker>
<assembly fullname="Message" preserve="all"/>
</linker>
| TotalLength (valor incluindo comprimento total 4 byte) | Rpcid | Cmd | Msgid | Corpo |
|---|---|---|---|---|
| Comprimento total (o valor contém 4 bytes do próprio comprimento total) | ID da mensagem | Texto da mensagem | ||
| Int32 (int) | Int32 (int) | INT16 (curto) | Int32 (int) | byte[] |
| 4byte | 4byte | 2byte | 4byte | byte []. Lenght |
当服务器不使用本库,或者不是C#语言时。满足报头格式,即可支持本库所有特性。 A MessagePiPeline faz parte das funções separadas pelo megumum.remote.
Também pode ser entendido como uma pilha de protocolo.
Ele determina quais etapas específicas são usadas para enviar e receber mensagens. Você pode personalizar a linha de mensagem e injetar -a remota para atender a algumas necessidades especiais.
Por exemplo:
你可以为每个Remote指定一个MessagePipeline实例,如果没有指定,默认使用MessagePipeline.Default。Versão 2.0 MessagePipline excluído e alterou -a para uma função reescrita em várias implementações remotas. Na prática de engenharia, verificou-se que não fazia sentido destacar o pipeline de mensagens do controle remoto e estava em excesso. Se você precisar personalizar o pipeline de três protocolos remotos ao mesmo tempo, o usuário pode dividi -lo sozinho e a estrutura não será processada.
人生就是反反复复。
A versão 3.0 decidiu voltar ao design original, e a idéia de design da primeira versão é melhor.
Após a prática de engenharia, verificou -se que o design do 2.0 não é conveniente para reescrever. Os usuários precisam reescrever várias cópias do mesmo código de reescrita para diferentes protocolos, herdando do TCPremote, Udpremote e KCpremote. Cada vez que modificam, várias cópias devem ser modificadas ao mesmo tempo, o que é muito volumoso.
O usuário reescreve principalmente a parte da mensagem de recebimento e a parte desconectada, e a peça de reconexão desconectada também é tratada de maneira diferente por diferentes protocolos.
Portanto, divida o transporte e o IDisconnecthandler do controle remoto.
Essencialmente, o controle remoto de 3,0 é igual à Pipline MessagePipline de 1.0. O transporte de 3,0 é igual ao controle remoto de 1,0.
Messagelut (Mensagem serializando a tabela de pesquisa de retorno de chamada) é a classe principal de Messagesandard. A MessagePiPine é serializada procurando funções registradas em Messagelut.因此在程序最开始你需要进行函数注册.
Função de registro geral:
void RegistIMeguminFormatter < T > ( KeyAlreadyHave key = KeyAlreadyHave . Skip ) where T : class , IMeguminFormatter , new ( ) O middleware da biblioteca da classe de serialização fornece várias APIs simples e fáceis de usar com base nas funções de serialização e deserialização gerando automaticamente, gerando automaticamente. Você precisa adicionar um msgidattribute à classe de protocolo para fornecer o ID usado pela tabela de pesquisa. Como um ID pode corresponder apenas a um conjunto de funções de serialização, cada classe de protocolo pode usar apenas uma biblioteca de serialização ao mesmo tempo.
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 ; }
}
}Uma assembléia pode ser registrada diretamente sob o ambiente 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 ( ) ;
}
} ) ;
} No ambiente AOT/IL2CPP , é necessário显示o registro de cada classe de protocolo por meio de funções genéricas para garantir que as funções genéricas correspondentes sejam geradas durante a análise estática AOT/IL2CPP编译器.
public void TestDefine ( )
{
Protobuf_netLUT . Regist < Login > ( ) ;
Protobuf_netLUT . Regist < LoginResult > ( ) ;
} Perceber:序列化库usa代码生成器生成代码, que é uma função de serialização que gera o tipo real.
Isso é para gerar funções genéricas para a API geral da biblioteca de classes serializadas durante a análise estática.
Por exemplo:
ProtoBuf.Serializer.Serialize<T>()é gerado comoProtoBuf.Serializer.Serialize<Login>()
Os dois são diferentes.
Cada biblioteca tem suas próprias limitações, e o suporte ao IL2CPP também é diferente. A estrutura escreverá um novo Messagelut herdado da Messagesandard/Messagelut para cada biblioteca suportada.
Como cada biblioteca de serialização suporta Span<byte> de maneira diferente, pode haver uma ligeira perda de desempenho na camada média.
Existem três formas para funções de serialização:
RPC功能: garante uma correspondência individual de solicitações e mensagens de retorno. O RPCID é negativo ao enviar, o RPCID*-1 é positivo ao retornar, e positivo e negativo são usados para distinguir as linhas para cima e para baixo.内存分配: reduza o aloc usando内存池.内存池: pool de memória da biblioteca padrão, ArrayPool<byte>.Shared .序列化: use o tipo para fazer a principal função de pesquisa.反序列化: use o msgid (int) para fazer a principal função de pesquisa.MessageLUT.Regist<T> A função adiciona manualmente outros tipos.消息类型: Tente não usar grandes estruturas personalizadas, pois todo o processo de serialização有可能levar a múltiplas embalagens e unboxing. Durante o processo de transferência de parâmetros, ele será copiado muitas vezes e o desempenho é menor que o da classe.<TargetFrameworks>netstandard2.0;netstandard2.1;net5;net6</TargetFrameworks> .时间和空间上的折衷O tamanho da mensagem não pode ser determinado antes da serialização; portanto, um buffer grande o suficiente precisa ser passado para a camada de serialização. Se você não copiar, todo o buffer grande será passado diretamente para a camada de envio. Devido à natureza assíncrona, é impossível conhecer com precisão o ciclo de vida do processo de envio. Um grande número de buffers grandes pode ser acumulado na camada de envio, que consome memória severa. Portanto, a biblioteca faz uma cópia entre a camada de serialização e a camada de envio.
A versão 2.0 resolve esse problema usando IBufferWriter<byte> e ReadOnlySequence<byte> , o que é mais eficiente.
Este é o conhecimento ou adivinhação resumido durante a biblioteca de redação:
O megum.remote é alcançado com o objetivo do MMORPG. Pode não ser a melhor escolha para jogos que não são do MMORPG. No futuro distante, diferentes implementações do NetRemotestandard podem ser escritas para diferentes tipos de jogos.
Where the data stores before we invoke 'socket.read(buffer, offset, count)'?Doubt regarding Winsock Kernel Buffer and Nagle algorithm