Estrutura para construção e consumo de serviços de plataforma cruzada no padrão .Net.
Plataforma cruzada, duplex, escalável, configurável e extensível
Xeeny é uma estrutura para construir e consumir serviços em dispositivos e servidores que suportam o padrão .net.
Com o Xeeny você pode hospedar e consumir serviços em qualquer lugar que o padrão .net seja capaz de funcionar (por exemplo, Xamarin android, Windows Server, ...). É multiplataforma, duplex, múltiplos transportes, assíncrono, proxies digitados, configurável e extensível
Install-Package Xeeny
For extensions:
Install-Package Xeeny.Http
Install-Package Xeeny.Extentions.Loggers
Install-Package Xeeny.Serialization.JsonSerializer
Install-Package Xeeny.Serialization.ProtobufSerializer
Recursos atuais:
Vindo:
public interface IService
{
Task < string > Echo ( string message ) ;
} public class Service : IService
{
public Task < string > Echo ( string message )
{
return Task . FromResult ( message ) ;
}
}ServiceHost usando ServiceHostBuilder<TService> onde está a implementação do serviçoAddXXXServer var tcpAddress = "tcp://myhost:9999/myservice" ;
var httpAddress = "http://myhost/myservice" ;
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
. AddTcpServer ( tcpAddress )
. AddWebSocketServer ( httpAddress ) ;
await host . Open ( ) ;ConnctionBuilder<T> var tcpAddress = "tcp://myhost/myservice" ;
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress )
. CreateConnection ( ) ; var msg = await client . Echo ( "Hellow World!" ) ; public interface ICallback
{
Task OnCallback ( string serverMessage ) ;
}OperationContext.Current.GetCallback<T> public Service : IService
{
public Task < string > Join ( string name )
{
CallBackAfter ( TimeSpan . FromSeconds ( 3 ) ) ;
return Task . FromResult ( "You joined" ) ;
}
async void CallBackAfter ( TimeSpan delay )
{
var client = OperationContext . Current . GetCallback < ICallback > ( ) ;
await Task . Delay ( ( int ) delay . TotalMilliseconds ) ;
await client . OnCallBack ( "This is a server callback" ) ;
}
}WithCallback<T> no construtor var host = new ServiceHostBuilder < Service > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address )
. CreateHost ( ) ;
await host . Open ( ) ; public class Callback : ICallback
{
public void OnServerUpdates ( string msg )
{
Console . WriteLine ( $ "Received callback msg: { msg } " ) ;
}
}DuplexConnectionBuilder para criar o cliente duplex, observe que é uma classe genérica, o primeiro argumento genérico é o contrato de serviço, enquanto o outro é a implementação de retorno de chamada e não a interface do contrato, para que o construtor saiba que tipo instanciar quando a solicitação de retorno de chamada for recebido. var address = "tcp://myhost/myservice" ;
var client = await new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . Single )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
await client . Join ( "My Name" ) ; Xeeny define três modos para criar instâncias de serviço
Você define o modo de instância de serviço usando a enumeração InstanceMode ao criar o ServiceHost
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
.. .
. CreateHost ( ) ;
await host . Open ( ) ; Ao criar uma conexão duplex, você passa o tipo de retorno de chamada e InstanceMode para DuplexConnectionBuilder . O InstanceMode atua da mesma forma que para o serviço ao criar ServiceHost
ServiceHostBuilder tem uma sobrecarga que pega uma instância do tipo de serviço. Isso permite que você crie a instância e passe-a para o construtor, o resultado é InstanceMode.Single usando o objeto que você passouServiceHostBuilder , o DuplextConnectionBuilder pega uma instância do tipo de retorno de chamada, permitindo que você mesmo crie o singletonPerCall e PerConnection são criadas pelo framework, você ainda pode inicializá-las após serem construídas e antes de executar qualquer método ouvindo os eventos: Evento ServiceHost<TService>.ServiceInstanceCreated e DuplextConnectionBuilder<TContract, TCallback>.CallbackInstanceCreated host . ServiceInstanceCreated += service =>
{
service . MyProperty = "Something" ;
}
.. .
var builder = new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . PerConnection )
. WithTcpTransport ( tcpAddress ) ;
builder . CallbackInstanceCreated += callback =>
{
callback .. .
}
var client = builder . CreateConnection ( ) ; Operation passando IsOneWay = true no contrato (A interface) public interface IService
{
[ Operation ( IsOneWay = true ) ]
void FireAndForget ( string message ) ;
} Quando você tem sobrecarga de métodos em uma interface (ou uma assinatura de método semelhante em uma interface pai), você deve diferenciá-los usando o atributo Operation definindo a propriedade Name . Isso se aplica a contratos de serviço e de retorno de chamada.
public interface IOtherService
{
[ Operation ( Name = "AnotherEcho" ) ]
Task < string > Echo ( string message ) ;
}
public interface IService : IOhterService
{
Task < string > Echo ( string message ) ;
}
class Service : IService , IOtherService
{
public Task < string > Echo ( string message )
{
return Task . FromResult ( $ "Echo: { message } " ) ;
}
Task < string > IOtherService . Echo ( string message )
{
return Task . FromResult ( $ "This is the other Echo: { message } " ) ;
}
} Você desejará acessar a conexão subjacente para gerenciá-la, como monitorar seu status, ouvir eventos ou gerenciá-la manualmente (fechá-la ou abri-la). A conexão é exposta através da interface IConnection que fornece estas funcionalidades:
State : O estado da conexão: Connecting , Connected , Closing , ClosedStateChanged : Evento disparado sempre que o estado da conexão mudaConnect() : Conecta-se ao endereço remotoClose() : Fecha a conexãoSessionEnded : Evento disparado quando a conexão está fechando ( State alterado para Closing )Dispose() : Descarta a conexãoConnectionId : Guid identifica cada conexão (por enquanto o Id no servidor e no cliente não coincidem)ConnectionName : nome de conexão amigável para facilitar depuração e análise de logsOperationContext.Current.GetConnection() no início do seu método e antes que o método de serviço gere qualquer novo thread.OperationContext.Current.GetConnection() , mas provavelmente chamando OperationContext.Current.GetCallback<TCallback> . A instância retornada é uma instância emitida em tempo de execução e implementa seu contrato de retorno de chamada (definido no parâmetro genérico TCallback ). Este tipo gerado automaticamente implementa IConnection também, então sempre que você quiser acessar as funções de conexão do canal de retorno, basta lançá-lo para IConnection public class ChatService : IChatService
{
ConcurrentDictionary < string , ICallback > _clients = new ConcurrentDictionary < string , ICallback > ( ) ;
ICallback GetCaller ( ) => OperationContext . Current . GetCallback < ICallback > ( ) ;
public void Join ( string id )
{
var caller = GetCaller ( ) ;
_clients . AddOrUpdate ( id , caller , ( k , v ) => caller ) ;
( ( IConnection ) caller ) . SessionEnded += s =>
{
_clients . TryRemove ( id , out ICallback _ ) ;
} ;
}
} Os clientes são instâncias de tipos gerados automaticamente que são emitidos em tempo de execução e implementam sua interface de contrato de serviço. Juntamente com o contrato, o tipo emitido implementa IConnection , o que significa que você pode converter qualquer cliente (Duplex ou não) para IConnection
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
var connection = ( IConnection ) client ;
connection . StateChanged += c => Console . WriteLine ( c . State ) ;
connection . Close ( )CreateConnection usa um parâmetro opcional do tipo boolean que é true por padrão. Este sinalizador indica se a conexão gerada se conectará ao servidor ou não. por padrão, sempre que CreateConnection for chamado, a conexão gerada se conectará automaticamente. Às vezes você deseja criar conexões e conectá-las mais tarde, para fazer isso você passa false para o método CreateConnection e abre sua conexão manualmente quando quiser var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( false ) ;
var connection = ( IConnection ) client ;
.. .
await connection . Connect ( ) ;Todos os construtores expõem opções de conexão quando você inclui Servidor ou Transporte. as opções são:
Timeout : Define o tempo limite da conexão ( padrão 30 segundos )ReceiveTiemout : é o tempo limite remoto inativo ( padrão do servidor: 10 minutos, padrão do cliente: Infinity )KeepAliveInterval : Intervalo de ping para manter ativo ( padrão 30 segundos )KeepAliveRetries : Número de tentativas antes de decidir que a conexão está desativada ( padrão 10 tentativas )SendBufferSize : Tamanho do buffer de envio ( padrão 4096 bytes = 4 KB )ReceiveBufferSize : Tamanho do buffer de recebimento ( padrão 4096 bytes = 4 KB )MaxMessageSize : Tamanho máximo das mensagens ( padrão 1000000 bytes = 1 MB )ConnectionNameFormatter : Delegar para definir ou formatar ConnectionName ( o padrão é null ). (veja Registro)SecuritySettings : configurações de SSL ( o padrão é null ) (consulte Segurança)Você obtém essas opções de ação de configuração no servidor ao chamar AddXXXServer:
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address , options =>
{
options . Timeout = TimeSpan . FromSeconds ( 10 ) ;
} )
. WithConsoleLogger ( )
. CreateHost ( ) ;
await host . Open ( ) ;No lado do cliente, você obtém isso ao chamar WithXXXTransport
var client = await new DuplexConnectionBuilder < IChatService , MyCallback > ( new MyCallback ( ) )
. WithTcpTransport ( address , options =>
{
options . KeepAliveInterval = TimeSpan . FromSeconds ( 5 ) ;
} )
. WithConsoleLogger ( )
. CreateConnection ( ) ; Quando você define Timeout e a solicitação não é concluída durante esse período, a conexão será encerrada e você terá que criar um novo cliente. Se o Timeout estiver definido no lado do servidor, isso definirá o tempo limite do retorno de chamada e a conexão será encerrada quando o retorno de chamada não for concluído durante esse tempo. Lembre-se de que callaback é uma operação unilateral e todas as operações unidirecionais são concluídas quando o outro lado recebe a mensagem e antes que o método remoto seja executado.
O ReceiveTimeout é o " Idle Remote Timeout ". Se você configurá-lo no servidor ele definirá o timeout para o servidor fechar clientes inativos que são os clientes que não estão enviando nenhuma solicitação ou mensagem KeepAlive durante esse tempo.
O ReceiveTimeout no cliente é definido como Infinity por padrão, se você defini-lo no cliente duplex, você está instruindo o cliente a ignorar retornos de chamada que não ocorrem durante esse período, o que é um cenário estranho, mas ainda possível se você optar por fazê-lo .
ReceiveBufferSize é o tamanho do buffer de recebimento. Defini-lo para valores pequenos não afetará a capacidade de receber mensagens grandes, mas se esse tamanho for significativamente pequeno em comparação com as mensagens a serem recebidas, introduza mais operações de IO. É melhor você deixar o valor padrão no início e, se necessário, fazer testes e análises de carga para encontrar o tamanho que funciona bem e ocupa
SendBufferSize é o tamanho do buffer de envio. Defini-lo para valores pequenos não afetará a capacidade de enviar mensagens grandes, mas se esse tamanho for significativamente pequeno em comparação com as mensagens a serem enviadas, introduza mais operações de IO. É melhor deixar o valor padrão no início e, se necessário, fazer testes e análises de carga para encontrar o tamanho que funciona bem e ocupa menos memória.
ReceiveBufferSize de um destinatário deve ser igual ao SendBufferSize do remetente porque alguns transportes como o UDP não funcionarão bem se esses dois tamanhos não forem iguais. Por enquanto, o Xeeny não verifica o tamanho do buffer, mas no futuro estarei modificando o protocolo para incluir essa verificação durante o processamento do Connect.
MaxMessageSize é o número máximo permitido de bytes para recebimento. Este valor não tem nada a ver com buffers, portanto não afeta a memória ou o desempenho. Este valor é importante para validar seus clientes e evitar mensagens enormes de clientes, Xeeny usa protocolo de prefixo de tamanho para que quando uma mensagem chegar ela seja armazenada em buffer em um buffer de tamanho ReceiveBufferSize que deve ser bem menor que MaxMessageSize , depois que a mensagem chegar o size cabeçalho é lido, se o tamanho for maior que MaxMessageSize a mensagem é rejeitada e a conexão é fechada.
Xeeny usa suas próprias mensagens de keep-alive porque nem todos os tipos de transporte possuem mecanismo de keep-alive integrado. Essas mensagens têm fluxo de 5 bytes apenas do cliente para o servidor. O intervalo KeepAliveInterval é de 30 segundos por padrão, quando você o define no cliente, o cliente enviará uma mensagem de ping se não tiver enviado nada com sucesso durante o último KeepAliveInterval .
Você deve definir KeepAliveInterval para ser menor que o ReceiveTimeout do servidor, pelo menos 1/2 ou 1/3 do ReceiveTimeout do servidor porque o servidor atingirá o tempo limite e fechará a conexão se não receber nada durante o ReceiveTimeout
KeepAliveRetries é o número de mensagens keep-alive com falha, uma vez alcançadas, o cliente decide que a conexão foi interrompida e fechada.
Definir KeepAliveInterval ou KeepAliveRetries no servidor não tem efeito.
Para que o Xeeny seja capaz de empacotar parâmetros de métodos e retornar tipos na rede, ele precisa serializá-los. Existem três serializadores já suportados no framework
MessagePackSerializer : A serialização MessagePack é implementada por MsgPack.Cli. É o serializador padrão, pois os dados serializados são pequenos e a implementação para .net na biblioteca fornecida é rápida.JsonSerializer : serializador Json implementado pela NewtonsoftProtobufSerializer : serializador ProtoBuffers do Google implementado pelo Protobuf-net Você pode escolher o serializador usando os construtores chamando WithXXXSerializer , apenas certifique-se de que seus tipos sejam serializáveis usando o serializador selecionado.
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. WithProtobufSerializer ( )
. CreateHost ( ) ;
await host . Open ( ) ;WithSerializer(ISerializer serializer) Xeeny usa TLS 1.2 (apenas sobre TCP por enquanto), você precisa adicionar X509Certificate ao servidor
var host = new ServiceHostBuilder < Service > ( .. . )
. AddTcpServer ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForServer ( x509Certificate2 ) ;
} )
.. . E no cliente você precisa passar o Certificate Name :
await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForClient ( certificateName ) ;
} )
.. . Se quiser validar o certificado remoto, você pode passar o delegado opcional RemoteCertificateValidationCallback para SecuritySettings.CreateForClient
Xeeny usa o mesmo sistema de log encontrado no Asp.Net Core
Para usar loggers, adicione o pacote nuget do logger e chame WithXXXLogger onde você pode passar o LogLevel
Você pode querer nomear conexões para que sejam fáceis de detectar ao depurar ou analisar logs. Você pode fazer isso definindo o delegado da função ConnectionNameFormatter nas opções que são passadas IConnection.ConnectionId como parâmetro e o retorno será atribuído a IConnection.ConnectionName .
var client1 = await new DuplexConnectionBuilder < IChatService , Callback > ( callback1 )
. WithTcpTransport ( address , options =>
{
options . ConnectionNameFormatter = id => $ "First-Connection ( { id } )" ;
} )
. WithConsoleLogger ( LogLevel . Trace )
. CreateConnection ( ) ; O Xeeny foi desenvolvido para ser de alto desempenho e assíncrono. Ter contratos assíncronos permite que a estrutura seja totalmente assíncrona. Tente sempre fazer com que suas operações retornem Task ou Task<T> em vez de void ou T . Isso salvará aquele thread extra que aguardará a conclusão do soquete assíncrono subjacente, caso suas operações não sejam assíncronas.
A sobrecarga no Xeeny é quando ele precisa emitir "Novos" tipos em tempo de execução. Isso acontece quando você cria ServiceHost<TService> (chamando ServiceHostBuilder<TService>.CreateHost() ), mas isso acontece uma vez por tipo, portanto, uma vez que xeeny emitiu o primeiro host de um determinado tipo, a criação de mais hosts desse tipo não apresenta problemas de desempenho. de qualquer forma, esse geralmente é o início do seu aplicativo.
Outro lugar onde os tipos de emissão acontecem é quando você cria o primeiro cliente de um determinado contrato ou tipo de retorno de chamada (chamando CreateConnection ). assim que o primeiro tipo desse proxy for emissor, os próximos clientes serão criados sem sobrecarga. (observe que você ainda está criando um novo soquete e uma nova conexão, a menos que passe false para CreateConnection ).
Chamar OperationContext.Current.GetCallback<T> também emite o tipo de tempo de execução, como todas as outras emissões acima do tipo emitido são armazenadas em cache e a sobrecarga ocorre apenas na primeira chamada. você pode chamar esse método quantas vezes quiser, mas é melhor armazenar em cache o retorno.
Você pode fazer com que todos os recursos da estrutura Xeeny acima funcionem com seu transporte personalizado (digamos que você queira atrás do dispositivo Blueetooth).
XeenyListenerServiceHostBuilder<T>.AddCustomServer() IXeenyTransportFactoryConnectionBuilder<T>.WithCustomTransport() Se você deseja ter seu próprio protocolo do zero, você precisa implementar sua própria conectividade, enquadramento de mensagens, simultaneidade, buffer, tempo limite, keep-alive, ... etc.
IListenerServiceHostBuilder<T>.AddCustomServer() ITransportFactoryConnectionBuilder<T>.WithCustomTransport()