Marco para crear y consumir servicios multiplataforma en .Net Standard.
Multiplataforma, dúplex, escalable, configurable y extensible
Xeeny es un marco para crear y consumir servicios en dispositivos y servidores que admiten el estándar .net.
Con Xeeny puede alojar y consumir servicios en cualquier lugar donde el estándar .net pueda funcionar (por ejemplo, Xamarin Android, Windows Server, ...). Es multiplataforma, dúplex, transportes múltiples, asíncrono, proxies tipificados, configurable y extensible.
Install-Package Xeeny
For extensions:
Install-Package Xeeny.Http
Install-Package Xeeny.Extentions.Loggers
Install-Package Xeeny.Serialization.JsonSerializer
Install-Package Xeeny.Serialization.ProtobufSerializer
Características actuales:
Próximamente:
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>
donde está la implementación del servicioAddXXXServer
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>
en el constructor 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 crear el cliente dúplex, tenga en cuenta que es una clase genérica, el primer argumento genérico es el contrato de servicio, mientras que el otro es la implementación de devolución de llamada, no la interfaz del contrato, por lo que el constructor sabe qué tipo crear una instancia cuando se realiza la solicitud de devolución de llamada. recibió. var address = "tcp://myhost/myservice" ;
var client = await new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . Single )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
await client . Join ( "My Name" ) ;
Xeeny define tres modos para crear instancias de servicio
Usted define el modo de instancia de servicio utilizando la enumeración InstanceMode
al crear ServiceHost.
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
.. .
. CreateHost ( ) ;
await host . Open ( ) ;
Cuando crea una conexión dúplex, pasa el tipo de devolución de llamada y el InstanceMode al DuplexConnectionBuilder
. InstanceMode
actúa de la misma manera que lo hace para el servicio al crear ServiceHost
ServiceHostBuilder
tiene una sobrecarga que toma una instancia del tipo de servicio. Esto le permite crear la instancia y pasarla al constructor, el resultado es InstanceMode.Single
usando el objeto que pasó.ServiceHostBuilder
, DuplextConnectionBuilder
toma una instancia del tipo de devolución de llamada que le permite crear el singleton usted mismo.PerCall
y PerConnection
son creadas por el marco, aún puede inicializarlas después de construirlas y antes de ejecutar cualquier método escuchando los eventos: evento ServiceHost<TService>.ServiceInstanceCreated
y 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
pasando IsOneWay = true en el contrato (La interfaz) public interface IService
{
[ Operation ( IsOneWay = true ) ]
void FireAndForget ( string message ) ;
}
Cuando tiene una sobrecarga de métodos en una interfaz (o una firma de método similar en una interfaz principal), debe diferenciarlos usando el atributo Operation
configurando la propiedad Name
. Esto se aplica tanto a los contratos de Servicio como a los de Devolución de Llamada.
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 } " ) ;
}
}
Querrá acceder a la conexión subyacente para administrarla, como monitorear su estado, escuchar eventos o administrarla manualmente (cerrarla o abrirla). La conexión se expone a través de la interfaz IConnection
que proporciona estas funcionalidades:
State
: El estado de la conexión: Connecting
, Connected
, Closing
, Closed
StateChanged
: evento activado cada vez que cambia el estado de la conexiónConnect()
: Se conecta a la dirección remotaClose()
: Cierra la conexión.SessionEnded
: evento activado cuando la conexión se cierra ( State
cambió a Closing
)Dispose()
: Elimina la conexión.ConnectionId
: Guid identifica cada conexión (por ahora, el ID en el servidor y el cliente no coincide)ConnectionName
: nombre de conexión descriptivo para facilitar la depuración y el análisis de registros.OperationContext.Current.GetConnection()
al comienzo de su método y antes de que el método de servicio genere cualquier hilo nuevo.OperationContext.Current.GetConnection()
, pero probablemente llamando a OperationContext.Current.GetCallback<TCallback>
. La instancia devuelta es una instancia que se emite en tiempo de ejecución e implementa su contrato de devolución de llamada (definido en el parámetro genérico TCallback
). Este tipo generado automáticamente también implementa IConnection
, por lo que cada vez que desee acceder a las funciones de conexión del canal chalback, simplemente transmítalo a 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 _ ) ;
} ;
}
}
Los clientes son instancias de tipos generados automáticamente que se emiten en tiempo de ejecución e implementan la interfaz de su contrato de servicio. Junto con el contrato, el tipo emitido implementa IConnection
, lo que significa que puede transmitir cualquier cliente (dúplex o no) a IConnection
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
var connection = ( IConnection ) client ;
connection . StateChanged += c => Console . WriteLine ( c . State ) ;
connection . Close ( )
CreateConnection
toma un parámetro opcional de tipo booleano que es true
de forma predeterminada. Este indicador indica si la conexión generada se conectará al servidor o no. De forma predeterminada, cada vez que se llama CreateConnection
la conexión generada se conectará automáticamente. A veces desea crear conexiones y desea conectarlas más tarde, para hacerlo, pasa false
al método CreateConnection
y luego abre su conexión manualmente cuando lo desee. var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( false ) ;
var connection = ( IConnection ) client ;
.. .
await connection . Connect ( ) ;
Todos los constructores exponen opciones de conexión cuando agrega Servidor o Transporte. las opciones son:
Timeout
: establece el tiempo de espera de la conexión ( predeterminado 30 segundos )ReceiveTiemout
: es el tiempo de espera remoto inactivo ( valor predeterminado del servidor: 10 minutos, valor predeterminado del cliente: Infinito )KeepAliveInterval
: intervalo de ping para mantener activo ( predeterminado 30 segundos )KeepAliveRetries
: número de reintentos antes de decidir que la conexión está desactivada ( 10 reintentos predeterminados )SendBufferSize
: tamaño del búfer de envío ( predeterminado 4096 bytes = 4 KB )ReceiveBufferSize
: tamaño del búfer de recepción ( predeterminado 4096 bytes = 4 KB )MaxMessageSize
: tamaño máximo de mensajes ( predeterminado 1000000 bytes = 1 MB )ConnectionNameFormatter
: delega para configurar o formatear ConnectionName
( el valor predeterminado es nulo ). (ver Registro)SecuritySettings
: Configuración de SSL ( el valor predeterminado es nulo ) (consulte Seguridad)Obtienes esta acción de configuración de opciones en el servidor cuando llamas a AddXXXServer:
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address , options =>
{
options . Timeout = TimeSpan . FromSeconds ( 10 ) ;
} )
. WithConsoleLogger ( )
. CreateHost ( ) ;
await host . Open ( ) ;
Del lado del cliente lo obtienes al llamar a WithXXXTransport
var client = await new DuplexConnectionBuilder < IChatService , MyCallback > ( new MyCallback ( ) )
. WithTcpTransport ( address , options =>
{
options . KeepAliveInterval = TimeSpan . FromSeconds ( 5 ) ;
} )
. WithConsoleLogger ( )
. CreateConnection ( ) ;
Cuando configura Timeout
y la solicitud no se completa durante ese tiempo, la conexión se cerrará y deberá crear un nuevo cliente. Si el Timeout
se establece en el lado del servidor, eso definirá el tiempo de espera de la devolución de llamada y la conexión se cerrará cuando la devolución de llamada no se complete durante ese tiempo. Recuerde que la devolución de llamada es una operación unidireccional y todas las operaciones unidireccionales se completan cuando la otra parte recibe el mensaje y antes de que se ejecute el método remoto.
El ReceiveTimeout
es el " Tiempo de espera remoto inactivo ". Si lo configura en el servidor, definirá el tiempo de espera para que el servidor cierre los clientes inactivos, que son los clientes que no envían ninguna solicitud o mensaje KeepAlive durante ese tiempo.
El ReceiveTimeout
en el cliente está configurado en Infinity de forma predeterminada; si lo configura en el cliente dúplex, le está indicando al cliente que ignore las devoluciones de llamada que no llegan durante ese tiempo, lo cual es un escenario extraño, pero aún posible si decide hacerlo. .
ReceiveBufferSize
es el tamaño del búfer de recepción. Configurarlo en valores pequeños no afectará la capacidad de recibir mensajes grandes, pero si ese tamaño es significativamente pequeño en comparación con los mensajes a recibir, entonces introduzca más operaciones de IO. Es mejor dejar el valor predeterminado al principio y luego, si es necesario, realizar pruebas de carga y análisis para encontrar el tamaño que funcione bien y ocupe
SendBufferSize
es el tamaño del búfer de envío. Configurarlo en valores pequeños no afectará la capacidad de enviar mensajes grandes, pero si ese tamaño es significativamente pequeño en comparación con los mensajes a enviar, entonces introduzca más operaciones de IO. Será mejor que deje el valor predeterminado al principio y luego, si es necesario, realice pruebas de carga y análisis para encontrar el tamaño que funcione bien y ocupe menos memoria.
ReceiveBufferSize
de un receptor debe ser igual al SendBufferSize
del remitente porque algunos transportes como UDP no funcionarán bien si estos dos tamaños no son iguales. Por ahora, Xeeny no verifica los tamaños de búfer, pero en el futuro modificaré el protocolo para incluir esta verificación durante el procesamiento de Connect.
MaxMessageSize
es el número máximo permitido de bytes para recibir. Este valor no tiene nada que ver con los buffers por lo que no afecta la memoria ni el rendimiento. Sin embargo, este valor es importante para validar a sus clientes y evitar mensajes enormes de los clientes, Xeeny usa el protocolo de prefijo de tamaño, por lo que cuando llega un mensaje, se almacenará en un búfer de tamaño ReceiveBufferSize
que debe ser mucho más pequeño que MaxMessageSize
. Después de que llegue el mensaje, Se lee el encabezado de tamaño, si el tamaño es mayor que MaxMessageSize
el mensaje se rechaza y se cierra la conexión.
Xeeny usa sus propios mensajes de mantenimiento de actividad porque no todos los tipos de transportes tienen un mecanismo de mantenimiento de actividad incorporado. Estos mensajes son un flujo de 5 bytes desde el cliente al servidor únicamente. El intervalo KeepAliveInterval
es de 30 segundos de forma predeterminada; cuando lo configura en el cliente, el cliente enviará un mensaje de ping si no envió nada correctamente durante el último KeepAliveInterval
.
Debe configurar KeepAliveInterval
para que sea menor que el ReceiveTimeout
del servidor, al menos 1/2 o 1/3 del ReceiveTimeout
del servidor porque el servidor expirará y cerrará la conexión si no recibió nada durante su ReceiveTimeout
KeepAliveRetries
es la cantidad de mensajes de mantenimiento de actividad fallidos; una vez alcanzados, el cliente decide que la conexión está interrumpida y se cierra.
Configurar KeepAliveInterval
o KeepAliveRetries
en el servidor no tiene ningún efecto.
Para que Xeeny pueda reunir los parámetros del método y los tipos de retorno en el cable, necesita serializarlos. Ya hay tres serializadores compatibles con el marco.
MessagePackSerializer
: la serialización de MessagePack está implementada por MsgPack.Cli. Es el serializador predeterminado ya que los datos serializados son pequeños y la implementación para .net en la biblioteca dada es rápida.JsonSerializer
: serializador Json implementado por NewtonsoftProtobufSerializer
: serializador ProtoBuffers de Google implementado por Protobuf-net Puede elegir el serializador usando los constructores llamando WithXXXSerializer
, solo asegúrese de que sus tipos sean serializables usando el serializador seleccionado.
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. WithProtobufSerializer ( )
. CreateHost ( ) ;
await host . Open ( ) ;
WithSerializer(ISerializer serializer)
Xeeny usa TLS 1.2 (solo a través de TCP por ahora), necesita agregar X509Certificate
al servidor
var host = new ServiceHostBuilder < Service > ( .. . )
. AddTcpServer ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForServer ( x509Certificate2 ) ;
} )
.. .
Y en el cliente debe pasar el Certificate Name
:
await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForClient ( certificateName ) ;
} )
.. .
Si desea validar el certificado remoto, puede pasar el delegado opcional RemoteCertificateValidationCallback
a SecuritySettings.CreateForClient
Xeeny utiliza el mismo sistema de registro que se encuentra en Asp.Net Core
Para usar registradores, agregue el paquete nuget del registrador, luego llame WithXXXLogger
donde puede pasar LogLevel
Es posible que desee nombrar las conexiones para que sean fáciles de detectar al depurar o analizar registros; puede hacerlo configurando el delegado de función ConnectionNameFormatter
en las opciones, que se pasa IConnection.ConnectionId
como parámetro y el retorno se asignará a IConnection.ConnectionName
.
var client1 = await new DuplexConnectionBuilder < IChatService , Callback > ( callback1 )
. WithTcpTransport ( address , options =>
{
options . ConnectionNameFormatter = id => $ "First-Connection ( { id } )" ;
} )
. WithConsoleLogger ( LogLevel . Trace )
. CreateConnection ( ) ;
Xeeny está diseñado para ser de alto rendimiento y asíncrono, tener contratos asíncronos permite que el marco sea completamente asíncrono. Intente siempre que sus operaciones devuelvan Task
o Task<T>
en lugar de void
o T
. Esto guardará ese subproceso adicional que estará esperando que se complete el socket asíncrono subyacente en caso de que sus operaciones no sean asíncronas.
La sobrecarga en Xeeny es cuando necesita emitir tipos "Nuevos" en tiempo de ejecución. Lo hace cuando crea ServiceHost<TService>
(llamando a ServiceHostBuilder<TService>.CreateHost()
), pero eso sucede una vez por tipo, por lo que una vez que xeeny emite el primer host del tipo dado, la creación de más hosts de ese tipo no tiene problemas de rendimiento. de todos modos, este suele ser el inicio de su aplicación.
Otro lugar donde ocurren los tipos de emisión es cuando crea el primer cliente de un contrato o tipo de devolución de llamada determinado (llamando CreateConnection
). Una vez que el primer tipo de ese proxy sea emisor, los siguientes clientes se crearán sin gastos generales. (tenga en cuenta que todavía está creando un nuevo socket y una nueva conexión a menos que pase false
a CreateConnection
).
Llamar a OperationContext.Current.GetCallback<T>
también emite el tipo de tiempo de ejecución, como todas las demás emisiones por encima del tipo emitido se almacena en caché y la sobrecarga ocurre solo en la primera llamada. Puedes llamar a este método tantos como quieras, pero será mejor que guardes en caché el resultado.
Puede hacer que todas las funciones de Xeeny framwork anteriores funcionen con su transporte personalizado (digamos que lo quiere detrás del dispositivo Bluetooth).
XeenyListener
ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<T>.WithCustomTransport()
Si desea tener su propio protocolo desde cero, necesita implementar su propia conectividad, encuadre de mensajes, concurrencia, almacenamiento en búfer, tiempo de espera, mantenimiento de vida, etc.
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()