Cadre pour la création et la consommation de services multiplateformes dans la norme .Net.
Multiplateforme, duplex, évolutif, configurable et extensible
Xeeny est un framework permettant de créer et de consommer des services sur des appareils et des serveurs prenant en charge la norme .net.
Avec Xeeny, vous pouvez héberger et consommer des services partout où la norme .net est capable de fonctionner (par exemple Xamarin Android, Windows Server, ...). Il est multiplateforme, duplex, à transports multiples, asynchrone, proxy typé, configurable et 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
Caractéristiques actuelles :
À venir :
public interface IService
{
Task < string > Echo ( string message ) ;
}
public class Service : IService
{
public Task < string > Echo ( string message )
{
return Task . FromResult ( message ) ;
}
}
ServiceHost
à l'aide de ServiceHostBuilder<TService>
où se trouve l'implémentation du serviceAddXXXServer
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>
sur le constructeur 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
pour créer le client duplex, notez qu'il s'agit d'une classe générique, le premier argument générique est le contrat de service, tandis que l'autre est l'implémentation de rappel et non l'interface de contrat, afin que le constructeur sache quel type instancier lorsque la demande de rappel est reçu. var address = "tcp://myhost/myservice" ;
var client = await new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . Single )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
await client . Join ( "My Name" ) ;
Xeeny définit trois modes de création d'instances de service
Vous définissez le mode d'instance de service à l'aide de l'énumération InstanceMode
lors de la création du ServiceHost
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
.. .
. CreateHost ( ) ;
await host . Open ( ) ;
Lorsque vous créez une connexion duplex, vous transmettez le type de rappel et InstanceMode au DuplexConnectionBuilder
. InstanceMode
agit de la même manière qu'il le fait pour le service lors de la création de ServiceHost
ServiceHostBuilder
a une surcharge qui prend une instance du type de service. Cela vous permet de créer l'instance et de la transmettre au constructeur. Le résultat est InstanceMode.Single
utilisant l'objet que vous avez passé.ServiceHostBuilder
, DuplextConnectionBuilder
prend une instance du type de rappel vous permettant de créer vous-même le singleton.PerCall
et PerConnection
sont créées par le framework, vous pouvez toujours les initialiser après avoir été construites et avant d'exécuter une méthode en écoutant les événements : événement ServiceHost<TService>.ServiceInstanceCreated
et 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
en passant IsOneWay = true dans le contrat (L'interface) public interface IService
{
[ Operation ( IsOneWay = true ) ]
void FireAndForget ( string message ) ;
}
Lorsque vous avez une surcharge de méthodes dans une interface (ou une signature de méthode similaire dans une interface parent), vous devez les distinguer à l'aide de l'attribut Operation
en définissant la propriété Name
. Cela s'applique aussi bien aux contrats de service qu'aux contrats de rappel.
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 } " ) ;
}
}
Vous souhaiterez accéder à la connexion sous-jacente pour la gérer, comme surveiller son statut, écouter des événements ou la gérer manuellement (la fermer ou l'ouvrir). La connexion est exposée via l'interface IConnection
qui fournit ces fonctionnalités :
State
: L'état de la connexion : Connecting
, Connected
, Closing
, Closed
StateChanged
: événement déclenché à chaque fois que l'état de la connexion changeConnect()
: Se connecte à l'adresse distanteClose()
: Ferme la connexionSessionEnded
: événement déclenché lors de la fermeture de la connexion ( State
modifié en Closing
)Dispose()
: Supprime la connexionConnectionId
: Guid identifie chaque connexion (pour l'instant les Id sur le serveur et le client ne correspondent pas)ConnectionName
: nom de connexion convivial pour un débogage et une analyse des journaux plus facilesOperationContext.Current.GetConnection()
au début de votre méthode et avant que la méthode de service ne génère un nouveau thread.OperationContext.Current.GetConnection()
, mais très probablement en appelant OperationContext.Current.GetCallback<TCallback>
. L'instance renvoyée est une instance émise au moment de l'exécution et qui implémente votre contrat de rappel (défini dans le paramètre générique TCallback
). Ce type généré automatiquement implémente également IConnection
, donc chaque fois que vous souhaitez accéder aux fonctions de connexion du canal de rappel, transmettez-le simplement sur 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 _ ) ;
} ;
}
}
Les clients sont des instances de types générés automatiquement qui sont émis au moment de l'exécution et implémentent votre interface de contrat de service. Avec le contrat, le type émis implémente IConnection
, ce qui signifie que vous pouvez convertir n'importe quel client (Duplex ou non) en IConnection
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
var connection = ( IConnection ) client ;
connection . StateChanged += c => Console . WriteLine ( c . State ) ;
connection . Close ( )
CreateConnection
prend un paramètre facultatif de type booléen qui est true
par défaut. Cet indicateur indique si la connexion générée se connectera au serveur ou non. par défaut, chaque fois que CreateConnection
est appelé, la connexion générée se connectera automatiquement. Parfois, vous souhaitez créer des connexions et souhaitez les connecter plus tard, pour ce faire, vous transmettez false
à la méthode CreateConnection
puis ouvrez votre connexion manuellement lorsque vous le souhaitez. var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( false ) ;
var connection = ( IConnection ) client ;
.. .
await connection . Connect ( ) ;
Tous les générateurs exposent les options de connexion lorsque vous ajoutez un serveur ou un transport. les options sont :
Timeout
: définit le délai d'expiration de la connexion ( 30 secondes par défaut )ReceiveTiemout
: est le délai d'expiration à distance d'inactivité ( par défaut du serveur : 10 minutes, par défaut du client : Infinity )KeepAliveInterval
: intervalle de ping de maintien en vie ( 30 secondes par défaut )KeepAliveRetries
: Nombre de tentatives avant de décider que la connexion est désactivée ( 10 tentatives par défaut )SendBufferSize
: Taille du tampon d'envoi ( par défaut 4096 octets = 4 Ko )ReceiveBufferSize
: taille du tampon de réception ( par défaut 4096 octets = 4 Ko )MaxMessageSize
: Taille maximale des messages ( par défaut 1 000 000 octets = 1 Mo )ConnectionNameFormatter
: délégué pour définir ou formater ConnectionName
( la valeur par défaut est null ). (voir Journalisation)SecuritySettings
: paramètres SSL ( la valeur par défaut est null ) (voir Sécurité)Vous obtenez l'action de configuration de ces options sur le serveur lorsque vous appelez AddXXXServer :
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address , options =>
{
options . Timeout = TimeSpan . FromSeconds ( 10 ) ;
} )
. WithConsoleLogger ( )
. CreateHost ( ) ;
await host . Open ( ) ;
Côté client, vous l'obtenez en appelant WithXXXTransport
var client = await new DuplexConnectionBuilder < IChatService , MyCallback > ( new MyCallback ( ) )
. WithTcpTransport ( address , options =>
{
options . KeepAliveInterval = TimeSpan . FromSeconds ( 5 ) ;
} )
. WithConsoleLogger ( )
. CreateConnection ( ) ;
Lorsque vous définissez Timeout
et que la demande ne se termine pas pendant ce temps, la connexion sera fermée et vous devrez créer un nouveau client. Si le Timeout
est défini côté serveur, cela définira le délai d'expiration du rappel et la connexion sera fermée lorsque le rappel n'est pas terminé pendant cette période. N'oubliez pas que le rappel est une opération à sens unique et que toutes les opérations à sens unique se terminent lorsque l'autre côté reçoit le message et avant l'exécution de la méthode distante.
Le ReceiveTimeout
est le " Idle Remote Timeout ". Si vous le définissez sur le serveur, il définira le délai d'attente pour que le serveur ferme les clients inactifs qui sont les clients qui n'envoient aucune demande ou message KeepAlive pendant cette période.
Le ReceiveTimeout
sur le client est défini sur Infinity par défaut. Si vous le définissez sur le client duplex, vous demandez au client d'ignorer les rappels qui ne surviennent pas pendant cette période, ce qui est un scénario étrange mais toujours possible si vous choisissez de le faire. .
ReceiveBufferSize
est la taille du tampon de réception. Le définir sur de petites valeurs n'affectera pas la capacité de recevoir de gros messages, mais si cette taille est significativement petite par rapport aux messages à recevoir, introduisez davantage d'opérations d'E/S. Vous feriez mieux de laisser la valeur par défaut au début, puis si nécessaire, effectuez vos tests et analyses de charge pour trouver la taille qui fonctionne bien et occupe
SendBufferSize
est la taille du tampon d'envoi. Le définir sur de petites valeurs n'affectera pas la capacité d'envoyer de gros messages, mais si cette taille est significativement petite par rapport aux messages à envoyer, introduisez davantage d'opérations d'E/S. Vous feriez mieux de laisser la valeur par défaut au début, puis si nécessaire, effectuez vos tests et analyses de charge pour trouver la taille qui fonctionne bien et occupe moins de mémoire.
ReceiveBufferSize
d'un destinataire doit être égal au SendBufferSize
de l'expéditeur car certains transports comme UDP ne fonctionneront pas bien si ces deux tailles ne sont pas égales. Pour l'instant, Xeeny ne vérifie pas la taille des tampons mais à l'avenir je modifierai le protocole pour inclure cette vérification lors du traitement Connect.
MaxMessageSize
est le nombre maximum autorisé d'octets à recevoir. Cette valeur n'a rien à voir avec les tampons et n'affecte donc pas la mémoire ou les performances. Cette valeur est importante cependant pour valider vos clients et empêcher les messages volumineux de la part des clients, Xeeny utilise le protocole de préfixe de taille. Ainsi, lorsqu'un message arrive, il sera mis en mémoire tampon sur un tampon de taille ReceiveBufferSize
qui doit être bien plus petit que MaxMessageSize
. Une fois le message arrivé, le l'en-tête size est lu, si la taille est supérieure à MaxMessageSize
le message est rejeté et la connexion est fermée.
Xeeny utilise ses propres messages keep-alive car tous les types de transports n'ont pas de mécanisme keep-alive intégré. Ces messages font 5 octets et circulent uniquement du client vers le serveur. L'intervalle KeepAliveInterval
est de 30 secondes par défaut, lorsque vous le définissez sur le client, le client enverra un message ping s'il n'a rien envoyé avec succès lors du dernier KeepAliveInterval
.
Vous devez définir KeepAliveInterval
pour qu'il soit inférieur au ReceiveTimeout
du serveur, au moins 1/2 ou 1/3 du ReceiveTimeout
du serveur, car le serveur expirera et fermera la connexion s'il n'a rien reçu pendant son ReceiveTimeout
KeepAliveRetries
est le nombre de messages Keep-Alive ayant échoué. Une fois atteint, le client décide que la connexion est rompue et se ferme.
La définition KeepAliveInterval
ou KeepAliveRetries
sur le serveur n'a aucun effet.
Pour que Xeeny puisse rassembler les paramètres de méthode et renvoyer les types sur le fil, il doit les sérialiser. Il existe trois sérialiseurs déjà pris en charge dans le framework
MessagePackSerializer
: La sérialisation MessagePack est-elle implémentée par MsgPack.Cli. Il s'agit du sérialiseur par défaut car les données sérialisées sont petites et l'implémentation de .net dans la bibliothèque donnée est rapide.JsonSerializer
: sérialiseur Json implémenté par NewtonsoftProtobufSerializer
: le sérialiseur ProtoBuffers de Google implémenté par Protobuf-net Vous pouvez choisir le sérialiseur à l'aide des constructeurs en appelant WithXXXSerializer
, assurez-vous simplement que vos types sont sérialisables à l'aide du sérialiseur sélectionné.
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. WithProtobufSerializer ( )
. CreateHost ( ) ;
await host . Open ( ) ;
WithSerializer(ISerializer serializer)
Xeeny utilise TLS 1.2 (sur TCP uniquement pour l'instant), vous devez ajouter X509Certificate
au serveur
var host = new ServiceHostBuilder < Service > ( .. . )
. AddTcpServer ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForServer ( x509Certificate2 ) ;
} )
.. .
Et sur le client vous devez transmettre le Certificate Name
:
await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForClient ( certificateName ) ;
} )
.. .
Si vous souhaitez valider le certificat distant, vous pouvez transmettre le délégué facultatif RemoteCertificateValidationCallback
à SecuritySettings.CreateForClient
Xeeny utilise le même système de journalisation que celui trouvé dans Asp.Net Core
Pour utiliser les enregistreurs, ajoutez le package nuget de l'enregistreur, puis appelez WithXXXLogger
où vous pouvez transmettre le LogLevel
Vous souhaiterez peut-être nommer les connexions afin qu'elles soient faciles à repérer lors du débogage ou de l'analyse des journaux, vous pouvez le faire en définissant le délégué de la fonction ConnectionNameFormatter
dans les options qui est transmis à IConnection.ConnectionId
en tant que paramètre et le retour sera attribué à 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 conçu pour être hautes performances et asynchrone, le fait d'avoir des contrats asynchrones permet au framework d'être entièrement asynchrone. Essayez toujours de faire en sorte que vos opérations renvoient Task
ou Task<T>
au lieu de void
ou T
. Cela permettra d'économiser un thread supplémentaire qui attendra la fin du socket asynchrone sous-jacent au cas où vos opérations ne seraient pas asynchrones.
La surcharge dans Xeeny se produit lorsqu'il doit émettre des types "Nouveaux" au moment de l'exécution. Il le fait lorsque vous créez ServiceHost<TService>
(en appelant ServiceHostBuilder<TService>.CreateHost()
) mais cela se produit une fois par type, donc une fois que xeeny a émis le premier hôte du type donné, la création de plusieurs hôtes de ce type n'a aucun problème de performances. de toute façon, c'est généralement le début de votre application.
Un autre endroit où les types émetteurs se produisent est lorsque vous créez le premier client d'un contrat ou d'un type de rappel donné (appelant CreateConnection
). une fois que le premier type de proxy est émetteur, les clients suivants seront créés sans surcharge. (notez que vous êtes toujours en train de créer un nouveau socket et une nouvelle connexion, sauf si vous transmettez false
à CreateConnection
).
L’appel de OperationContext.Current.GetCallback<T>
émet également un type d’exécution, comme toutes les autres émissions supérieures au type émis, qui sont mises en cache et la surcharge ne se produit qu’au premier appel. vous pouvez appeler cette méthode autant que vous le souhaitez, mais il vaut mieux mettre en cache le retour.
Vous pouvez obtenir toutes les fonctionnalités du framework Xeeny ci-dessus pour fonctionner avec votre transport personnalisé (disons que vous le souhaitez derrière l'appareil Bluetooth).
XeenyListener
ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<T>.WithCustomTransport()
Si vous souhaitez disposer de votre propre protocole à partir de zéro, vous devez implémenter votre propre connectivité, cadrage des messages, concurrence, mise en mémoire tampon, délai d'attente, maintien en vie, ... etc.
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()