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
, Closed
StateChanged
: 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).
XeenyListener
ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<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.
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()