Framework zum Erstellen und Nutzen plattformübergreifender Dienste im .Net-Standard.
Plattformübergreifend, duplex, skalierbar, konfigurierbar und erweiterbar
Xeeny ist ein Framework zum Erstellen und Nutzen von Diensten auf Geräten und Servern, die den .net-Standard unterstützen.
Mit Xeeny können Sie Dienste überall dort hosten und nutzen, wo der .net-Standard funktionieren kann (z. B. Xamarin Android, Windows Server, ...). Es ist plattformübergreifend, duplex, mehrere Transporte, asynchron, typisierte Proxys, konfigurierbar und erweiterbar
Install-Package Xeeny
For extensions:
Install-Package Xeeny.Http
Install-Package Xeeny.Extentions.Loggers
Install-Package Xeeny.Serialization.JsonSerializer
Install-Package Xeeny.Serialization.ProtobufSerializer
Aktuelle Funktionen:
Kommt:
public interface IService
{
Task < string > Echo ( string message ) ;
}
public class Service : IService
{
public Task < string > Echo ( string message )
{
return Task . FromResult ( message ) ;
}
}
ServiceHost
mit ServiceHostBuilder<TService>
wobei sich die Dienstimplementierung befindetAddXXXServer
-Methoden Server hinzu 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>
ab 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>
im Builder auf 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
, um den Duplex-Client zu erstellen. Beachten Sie, dass es sich um eine generische Klasse handelt. Das erste generische Argument ist der Dienstvertrag, während das andere die Rückrufimplementierung und nicht die Vertragsschnittstelle ist, sodass der Builder weiß, welcher Typ bei der Rückrufanforderung instanziiert werden soll erhalten. var address = "tcp://myhost/myservice" ;
var client = await new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . Single )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
await client . Join ( "My Name" ) ;
Xeeny definiert drei Modi zum Erstellen von Serviceinstanzen
Sie definieren den Dienstinstanzmodus mithilfe der InstanceMode
Enumeration beim Erstellen des ServiceHost
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
.. .
. CreateHost ( ) ;
await host . Open ( ) ;
Wenn Sie eine Duplexverbindung erstellen, übergeben Sie den Rückruftyp und InstanceMode an DuplexConnectionBuilder
. Der InstanceMode
verhält sich genauso wie für den Dienst beim Erstellen von ServiceHost
ServiceHostBuilder
-Konstruktor verfügt über eine Überladung, die eine Instanz des Diensttyps übernimmt. Dadurch können Sie die Instanz erstellen und an den Builder übergeben. Das Ergebnis ist InstanceMode.Single
unter Verwendung des von Ihnen übergebenen ObjektsServiceHostBuilder
nimmt DuplextConnectionBuilder
eine Instanz des Callback-Typs entgegen, sodass Sie den Singleton selbst erstellen könnenPerCall
und PerConnection
sind, werden vom Framework erstellt. Sie können sie nach der Erstellung und vor der Ausführung einer Methode noch initialisieren, indem Sie die folgenden Ereignisse abhören: ServiceHost<TService>.ServiceInstanceCreated
-Ereignis und 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
Attributs zuordnen, indem Sie IsOneWay = true im Vertrag (der Schnittstelle) übergeben. public interface IService
{
[ Operation ( IsOneWay = true ) ]
void FireAndForget ( string message ) ;
}
Wenn es in einer Schnittstelle eine Methodenüberladung gibt (oder eine ähnliche Methodensignatur in einer übergeordneten Schnittstelle), müssen Sie diese mithilfe Operation
Attributs unterscheiden, indem Sie Name
Eigenschaft festlegen. Dies gilt sowohl für Service- als auch für Rückrufverträge.
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 } " ) ;
}
}
Sie möchten auf die zugrunde liegende Verbindung zugreifen, um sie zu verwalten, z. B. ihren Status zu überwachen, Ereignisse abzuhören oder sie manuell zu verwalten (schließen oder öffnen). Die Verbindung wird über IConnection
Schnittstelle bereitgestellt, die folgende Funktionalitäten bereitstellt:
State
: Der Verbindungsstatus: Connecting
, Connected
, Closing
, Closed
StateChanged
: Ereignis wird immer dann ausgelöst, wenn sich der Verbindungsstatus ändertConnect()
: Stellt eine Verbindung zur Remote-Adresse herClose()
: Schließt die VerbindungSessionEnded
: Ereignis wird ausgelöst, wenn die Verbindung geschlossen wird ( State
wird in Closing
“ geändert)Dispose()
: Verwirft die VerbindungConnectionId
: Guid identifiziert jede Verbindung (im Moment stimmen die Ids auf dem Server und dem Client nicht überein)ConnectionName
: Freundlicher Verbindungsname für einfacheres Debuggen und ProtokollanalysenOperationContext.Current.GetConnection()
am Anfang Ihrer Methode und bevor die Dienstmethode einen neuen Thread erzeugt.OperationContext.Current.GetConnection()
, höchstwahrscheinlich jedoch durch Aufrufen von OperationContext.Current.GetCallback<TCallback>
. Die zurückgegebene Instanz ist eine Instanz, die zur Laufzeit ausgegeben wird und Ihren Rückrufvertrag implementiert (definiert im generischen Parameter TCallback
). Dieser automatisch generierte Typ implementiert auch IConnection
. Wenn Sie also auf Verbindungsfunktionen des Callback-Kanals zugreifen möchten, wandeln Sie ihn einfach in IConnection
um 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 _ ) ;
} ;
}
}
Clients sind Instanzen automatisch generierter Typen, die zur Laufzeit ausgegeben werden und Ihre Servicevertragsschnittstelle implementieren. Zusammen mit dem Vertrag implementiert der ausgegebene Typ IConnection
was bedeutet, dass Sie jeden Client (Duplex oder nicht) in IConnection
umwandeln können
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
var connection = ( IConnection ) client ;
connection . StateChanged += c => Console . WriteLine ( c . State ) ;
connection . Close ( )
CreateConnection
akzeptiert einen optionalen Parameter vom Typ „boolean“, der standardmäßig true
ist. Dieses Flag gibt an, ob die generierte Verbindung eine Verbindung zum Server herstellt oder nicht. Standardmäßig stellt die generierte Verbindung bei jedem Aufruf CreateConnection
automatisch eine Verbindung her. Manchmal möchten Sie Verbindungen erstellen und diese später verbinden. Dazu übergeben Sie false
an die CreateConnection
-Methode und öffnen dann Ihre Verbindung manuell, wenn Sie möchten var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( false ) ;
var connection = ( IConnection ) client ;
.. .
await connection . Connect ( ) ;
Alle Builder stellen Verbindungsoptionen bereit, wenn Sie Server oder Transport hinzufügen. Die Optionen sind:
Timeout
: Legt das Verbindungszeitlimit fest ( Standard: 30 Sekunden ).ReceiveTiemout
: Ist das Leerlauf-Remote-Timeout ( Server-Standard: 10 Minuten, Client-Standard: Unendlich ).KeepAliveInterval
: Keep-Alive-Ping-Intervall ( Standard 30 Sekunden )KeepAliveRetries
: Anzahl der Wiederholungsversuche, bevor entschieden wird, dass die Verbindung getrennt ist ( Standard: 10 Wiederholungsversuche )SendBufferSize
: Größe des Sendepuffers ( Standard 4096 Byte = 4 KB )ReceiveBufferSize
: Empfangspuffergröße ( Standard 4096 Byte = 4 KB )MaxMessageSize
: Maximale Größe der Nachrichten ( Standard 1000000 Byte = 1 MB )ConnectionNameFormatter
: Delegieren, um ConnectionName
festzulegen oder zu formatieren ( Standard ist null ). (siehe Protokollierung)SecuritySettings
: SSL-Einstellungen ( Standard ist null ) (siehe Sicherheit)Sie erhalten diese Optionskonfigurationsaktion auf dem Server, wenn Sie AddXXXServer aufrufen:
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address , options =>
{
options . Timeout = TimeSpan . FromSeconds ( 10 ) ;
} )
. WithConsoleLogger ( )
. CreateHost ( ) ;
await host . Open ( ) ;
Auf der Clientseite erhalten Sie es, wenn Sie WithXXXTransport aufrufen
var client = await new DuplexConnectionBuilder < IChatService , MyCallback > ( new MyCallback ( ) )
. WithTcpTransport ( address , options =>
{
options . KeepAliveInterval = TimeSpan . FromSeconds ( 5 ) ;
} )
. WithConsoleLogger ( )
. CreateConnection ( ) ;
Wenn Sie Timeout
festlegen und die Anfrage während dieser Zeit nicht abgeschlossen wird, wird die Verbindung geschlossen und Sie müssen einen neuen Client erstellen. Wenn das Timeout
auf der Serverseite festgelegt ist, wird das Callback-Timeout definiert und die Verbindung wird geschlossen, wenn der Callback in dieser Zeit nicht abgeschlossen ist. Denken Sie daran, dass Callaback eine unidirektionale Operation ist und alle unidirektionalen Operationen abgeschlossen sind, wenn die andere Seite die Nachricht empfängt und bevor die Remote-Methode ausgeführt wird.
Das ReceiveTimeout
“ ist das „ Idle Remote Timeout “. Wenn Sie es auf dem Server festlegen, definiert es das Timeout für den Server, um inaktive Clients zu schließen, bei denen es sich um Clients handelt, die während dieser Zeit keine Anfragen oder KeepAlive-Nachrichten senden.
Der ReceiveTimeout
auf dem Client ist standardmäßig auf „Unendlich“ eingestellt. Wenn Sie ihn auf dem Duplex-Client festlegen, weisen Sie den Client an, Rückrufe zu ignorieren, die in dieser Zeit nicht eingehen. Dies ist ein seltsames Szenario, aber dennoch möglich, wenn Sie sich dafür entscheiden .
ReceiveBufferSize
ist die Größe des Empfangspuffers. Das Festlegen auf kleine Werte hat keinen Einfluss auf die Fähigkeit, große Nachrichten zu empfangen. Wenn diese Größe jedoch im Vergleich zu den zu empfangenden Nachrichten erheblich klein ist, werden weitere E/A-Vorgänge eingeführt. Behalten Sie lieber den Standardwert am Anfang bei und führen Sie dann bei Bedarf Lasttests und -analysen durch, um die Größe zu ermitteln, die eine gute Leistung erbringt und belegt
SendBufferSize
ist die Größe des Sendepuffers. Das Festlegen kleiner Werte hat keinen Einfluss auf die Fähigkeit, große Nachrichten zu senden. Wenn diese Größe jedoch im Vergleich zu den zu sendenden Nachrichten erheblich klein ist, werden weitere E/A-Vorgänge eingeführt. Behalten Sie lieber den Standardwert am Anfang bei und führen Sie dann bei Bedarf Lasttests und -analysen durch, um die Größe zu finden, die eine gute Leistung erbringt und weniger Speicher belegt.
ReceiveBufferSize
eines Empfängers sollte mit der SendBufferSize
des Senders übereinstimmen, da einige Transporte wie UDP nicht gut funktionieren, wenn diese beiden Größen nicht gleich sind. Im Moment überprüft Xeeny die Puffergrößen nicht, aber in Zukunft ändere ich das Protokoll, um diese Prüfung während der Connect-Verarbeitung einzubeziehen.
MaxMessageSize
ist die maximal zulässige Anzahl zu empfangender Bytes. Dieser Wert hat nichts mit Puffern zu tun und hat daher keinen Einfluss auf den Speicher oder die Leistung. Dieser Wert ist jedoch MaxMessageSize
, um Ihre Clients zu validieren und große Nachrichten von Clients ReceiveBufferSize
verhindern size-Header wird gelesen, wenn die Größe größer als MaxMessageSize
ist, wird die Nachricht abgelehnt und die Verbindung geschlossen.
Xeeny verwendet eigene Keep-Alive-Nachrichten, da nicht alle Transportarten über einen integrierten Keep-Alive-Mechanismus verfügen. Diese Nachrichten sind nur 5 Byte groß und fließen nur vom Client zum Server. Das Intervall KeepAliveInterval
beträgt standardmäßig 30 Sekunden. Wenn Sie es auf dem Client festlegen, sendet der Client eine Ping-Nachricht, wenn während des letzten KeepAliveInterval
nichts erfolgreich gesendet wurde.
Sie müssen KeepAliveInterval
auf einen Wert setzen, der kleiner ist als der ReceiveTimeout
des Servers, mindestens 1/2 oder 1/3 des ReceiveTimeout
des Servers, da der Server eine Zeitüberschreitung erleidet und die Verbindung schließt, wenn er während des ReceiveTimeout
nichts empfangen hat
KeepAliveRetries
ist die Anzahl der fehlgeschlagenen Keep-Alive-Nachrichten. Sobald sie erreicht sind, entscheidet der Client, dass die Verbindung unterbrochen ist, und schließt.
Das Festlegen von KeepAliveInterval
oder KeepAliveRetries
auf dem Server hat keine Auswirkung.
Damit Xeeny Methodenparameter marshallen und Typen auf der Leitung zurückgeben kann, muss es diese serialisieren. Das Framework unterstützt bereits drei Serialisierer
MessagePackSerializer
: Ist die MessagePack-Serialisierung, die von MsgPack.Cli implementiert wird. Dies ist der Standard-Serialisierer, da die serialisierten Daten klein sind und die Implementierung für .net in der angegebenen Bibliothek schnell ist.JsonSerializer
: Von Newtonsoft implementierter Json-SerializerProtobufSerializer
: Googles ProtoBuffers-Serializer, implementiert von Protobuf-net Sie können den Serializer mithilfe der Builder auswählen, indem Sie WithXXXSerializer
aufrufen. Stellen Sie lediglich sicher, dass Ihre Typen mit dem ausgewählten Serializer serialisierbar sind.
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. WithProtobufSerializer ( )
. CreateHost ( ) ;
await host . Open ( ) ;
WithSerializer(ISerializer serializer)
aufrufen. Xeeny verwendet TLS 1.2 (vorerst nur über TCP), Sie müssen X509Certificate
zum Server hinzufügen
var host = new ServiceHostBuilder < Service > ( .. . )
. AddTcpServer ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForServer ( x509Certificate2 ) ;
} )
.. .
Und auf dem Client müssen Sie den Certificate Name
übergeben:
await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForClient ( certificateName ) ;
} )
.. .
Wenn Sie das Remote-Zertifikat validieren möchten, können Sie den optionalen Delegaten RemoteCertificateValidationCallback
an SecuritySettings.CreateForClient
übergeben
Xeeny verwendet dasselbe Protokollierungssystem wie Asp.Net Core
Um Logger zu verwenden, fügen Sie das Nuget-Paket des Loggers hinzu und rufen Sie dann WithXXXLogger
auf, wo Sie den LogLevel
übergeben können
Möglicherweise möchten Sie Verbindungen benennen, damit sie beim Debuggen oder Analysieren von Protokollen leicht zu erkennen sind. Dies können Sie erreichen, indem Sie den Funktionsdelegaten ConnectionNameFormatter
in den Optionen festlegen, der IConnection.ConnectionId
als Parameter übergeben wird und dessen Rückgabe IConnection.ConnectionName
zugewiesen wird.
var client1 = await new DuplexConnectionBuilder < IChatService , Callback > ( callback1 )
. WithTcpTransport ( address , options =>
{
options . ConnectionNameFormatter = id => $ "First-Connection ( { id } )" ;
} )
. WithConsoleLogger ( LogLevel . Trace )
. CreateConnection ( ) ;
Xeeny ist auf hohe Leistung und Asynchronität ausgelegt. Durch asynchrone Verträge ist das Framework vollständig asynchron. Versuchen Sie, dass Ihre Vorgänge immer Task
oder Task<T>
anstelle von void
oder T
zurückgeben. Dadurch wird ein zusätzlicher Thread eingespart, der auf den Abschluss des zugrunde liegenden asynchronen Sockets wartet, falls Ihre Vorgänge nicht asynchron sind.
Der Mehraufwand in Xeeny entsteht, wenn zur Laufzeit „Neue“ Typen ausgegeben werden müssen. Dies geschieht, wenn Sie ServiceHost<TService>
erstellen ( ServiceHostBuilder<TService>.CreateHost()
aufrufen), aber das geschieht einmal pro Typ. Wenn Xeeny also den ersten Host des angegebenen Typs ausgegeben hat, treten beim Erstellen weiterer Hosts dieses Typs keine Leistungsprobleme auf. Normalerweise ist dies jedoch der Beginn Ihrer Bewerbung.
Ein anderer Ort, an dem Typen ausgegeben werden, ist, wenn Sie den ersten Client eines bestimmten Vertrags oder Rückruftyps erstellen (Aufruf von CreateConnection
). Sobald der erste Typ dieses Proxys Emitter ist, werden die nächsten Clients ohne Overhead erstellt. (Beachten Sie, dass Sie immer noch einen neuen Socket und eine neue Verbindung erstellen, es sei denn, Sie übergeben false
an CreateConnection
.)
Der Aufruf von OperationContext.Current.GetCallback<T>
gibt auch einen Laufzeittyp aus, wie alle anderen Emissionen oben wird der ausgegebene Typ zwischengespeichert und der Overhead entsteht nur beim ersten Aufruf. Sie können diese Methode so oft aufrufen, wie Sie möchten, aber Sie sollten die Rückgabe besser zwischenspeichern.
Sie können alle oben genannten Xeeny-Framework-Funktionen nutzen, um mit Ihrem benutzerdefinierten Transport zu arbeiten (Angenommen, Sie möchten es hinter dem Bluetooth-Gerät des Geräts haben).
XeenyListener
ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<T>.WithCustomTransport()
Wenn Sie Ihr eigenes Protokoll von Grund auf haben möchten, müssen Sie Ihre eigene Konnektivität, Nachrichten-Framing, Parallelität, Pufferung, Zeitüberschreitung, Keep-Alive usw. implementieren.
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()