在 .Net 标准中构建和使用跨平台服务的框架。
跨平台、双工、可扩展、可配置和可扩展
Xeeny 是用于在支持 .net 标准的设备和服务器上构建和使用服务的框架。
借助 Xeeny,您可以在 .net 标准能够运行的任何地方托管和使用服务(例如 Xamarin android、Windows Server...)。它是跨平台、双工、多种传输、异步、类型化代理、可配置和可扩展的
Install-Package Xeeny
For extensions:
Install-Package Xeeny.Http
Install-Package Xeeny.Extentions.Loggers
Install-Package Xeeny.Serialization.JsonSerializer
Install-Package Xeeny.Serialization.ProtobufSerializer
目前的特点:
即将到来:
public interface IService
{
Task < string > Echo ( string message ) ;
}
public class Service : IService
{
public Task < string > Echo ( string message )
{
return Task . FromResult ( message ) ;
}
}
ServiceHostBuilder<TService>
创建ServiceHost
,其中是服务实现AddXXXServer
方法将服务器添加到主机 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>
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
创建双工客户端,注意它是泛型类,第一个泛型参数是服务契约,而另一个是回调实现而不是契约接口,因此构建器知道当回调请求时要实例化什么类型已收到。 var address = "tcp://myhost/myservice" ;
var client = await new DuplexConnectionBuilder < IService , Callback > ( InstanceMode . Single )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
await client . Join ( "My Name" ) ;
Xeeny定义了三种创建服务实例的模式
创建 ServiceHost 时使用InstanceMode
枚举定义服务实例模式
var host = new ServiceHostBuilder < Service > ( InstanceMode . PerCall )
.. .
. CreateHost ( ) ;
await host . Open ( ) ;
创建双工连接时,您将回调类型和 InstanceMode 传递给DuplexConnectionBuilder
。 InstanceMode
行为方式与创建 ServiceHost 时服务的行为方式相同
ServiceHostBuilder
构造函数有一个重载,它采用服务类型的实例,这允许您创建实例并将其传递给构建器,结果是使用您传递的对象的InstanceMode.Single
ServiceHostBuilder
类似, DuplextConnectionBuilder
采用回调类型的实例,允许您自己创建单例PerCall
和PerConnection
实例是由框架创建的,您仍然可以在构造之后和执行任何方法之前通过侦听事件来初始化它们: ServiceHost<TService>.ServiceInstanceCreated
事件和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
属性在契约中传递IsOneWay = true (接口)来将其属性化 public interface IService
{
[ Operation ( IsOneWay = true ) ]
void FireAndForget ( string message ) ;
}
当一个接口(或父接口中类似的方法签名)中存在方法重载时,您必须通过设置Name
属性,使用Operation
属性来区分它们。这适用于服务和回调合同。
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 } " ) ;
}
}
您将需要访问底层连接来管理它,例如监视其状态、侦听事件或手动管理它(关闭或打开它)。连接通过IConnection
接口公开,该接口提供以下功能:
State
: 连接状态: Connecting
、 Connected
、 Closing
、 Closed
StateChanged
:只要连接状态发生变化就会触发事件Connect()
:连接到远程地址Close()
:关闭连接SessionEnded
:连接关闭时触发事件( State
更改为Closing
)Dispose()
:释放连接ConnectionId
:Guid 标识每个连接(目前服务器和客户端上的 Id 不匹配)ConnectionName
:友好的连接名称,以便于调试和日志分析OperationContext.Current.GetConnection()
获取连接。OperationContext.Current.GetConnection()
来获取连接,但最有可能通过调用OperationContext.Current.GetCallback<TCallback>
来获取连接。返回的实例是在运行时发出并实现回调协定(在通用参数TCallback
中定义)的实例。此自动生成的类型也实现了IConnection
,因此任何时候您想要访问回拨通道的连接函数,只需将其强制转换为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 _ ) ;
} ;
}
}
客户端是自动生成类型的实例,它们在运行时发出并实现您的服务契约接口。与合约一起,发出的类型实现了IConnection
这意味着您可以将任何客户端(双工或非双工)强制转换为IConnection
var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( ) ;
var connection = ( IConnection ) client ;
connection . StateChanged += c => Console . WriteLine ( c . State ) ;
connection . Close ( )
CreateConnection
方法采用一个布尔类型的可选参数,默认情况下为true
。该标志指示生成的连接是否将连接到服务器。默认情况下,每当调用CreateConnection
时,生成的连接都会自动连接。有时您想要创建连接并希望稍后连接它们,为此,您可以将false
传递给CreateConnection
方法,然后在需要时手动打开连接 var client = await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( address )
. CreateConnection ( false ) ;
var connection = ( IConnection ) client ;
.. .
await connection . Connect ( ) ;
当您添加服务器或传输时,所有构建器都会公开连接选项。选项有:
Timeout
:设置连接超时(默认30秒)ReceiveTiemout
:是空闲远程超时(服务器默认:10分钟,客户端默认:Infinity )KeepAliveInterval
:保持活动 ping 间隔(默认 30 秒)KeepAliveRetries
:决定连接关闭之前的重试次数(默认重试 10 次)SendBufferSize
:发送缓冲区大小(默认4096字节= 4 KB )ReceiveBufferSize
:接收缓冲区大小(默认4096字节= 4 KB )MaxMessageSize
:消息的最大大小(默认 1000000 字节 = 1 MB )ConnectionNameFormatter
:委托设置或格式化ConnectionName
(默认为 null )。 (参见日志记录)SecuritySettings
:SSL 设置(默认为 null )(请参阅安全性)当您调用 AddXXXServer 时,您将在服务器上获得这些选项配置操作:
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. AddTcpServer ( address , options =>
{
options . Timeout = TimeSpan . FromSeconds ( 10 ) ;
} )
. WithConsoleLogger ( )
. CreateHost ( ) ;
await host . Open ( ) ;
在客户端,您可以在调用 WithXXXTransport 时获得它
var client = await new DuplexConnectionBuilder < IChatService , MyCallback > ( new MyCallback ( ) )
. WithTcpTransport ( address , options =>
{
options . KeepAliveInterval = TimeSpan . FromSeconds ( 5 ) ;
} )
. WithConsoleLogger ( )
. CreateConnection ( ) ;
当您设置Timeout
并且请求在此期间未完成时,连接将关闭,您必须创建新的客户端。如果在服务器端设置了Timeout
,则将定义回调超时,并且当回调在此期间未完成时,连接将被关闭。请记住,回调是单向操作,并且所有单向操作在另一方收到消息且执行远程方法之前完成。
ReceiveTimeout
是“空闲远程超时”,如果您在服务器上设置它,它将定义服务器关闭不活动客户端(即在此期间未发送任何请求或 KeepAlive 消息的客户端)的超时。
默认情况下,客户端上的ReceiveTimeout
设置为“Infinity” ,如果您在双工客户端上设置它,您将指示客户端忽略在此期间未出现的回调,这是一个奇怪的情况,但如果您选择这样做,仍然有可能。
ReceiveBufferSize
是接收缓冲区的大小。将其设置为较小的值不会影响接收大消息的能力,但如果该大小与要接收的消息相比明显较小,则引入更多 IO 操作。您最好在开始时保留默认值,然后根据需要进行负载测试和分析,以找到性能良好且占用的大小
SendBufferSize
是发送缓冲区的大小。将其设置为较小的值不会影响发送大消息的能力,如果该大小与要发送的消息相比明显较小,则引入更多 IO 操作。您最好在开始时保留默认值,然后根据需要进行负载测试和分析,以找到性能良好且占用内存较少的大小。
接收方的ReceiveBufferSize
应等于发送方的SendBufferSize
,因为如果这两个大小不相等,某些传输(如 UDP)将无法正常工作。目前 Xeeny 不检查缓冲区大小,但将来我将修改协议以在连接处理期间包含此检查。
MaxMessageSize
是允许接收的最大字节数。该值与缓冲区无关,因此不会影响内存或性能。这个值对于验证客户端和防止来自客户端的巨大消息很重要,Xeeny 使用大小前缀协议,因此当消息到达时,它将缓冲在大小为ReceiveBufferSize
的缓冲区中,该缓冲区必须小于MaxMessageSize
,消息到达后读取 size 标头,如果大小大于MaxMessageSize
则消息将被拒绝并关闭连接。
Xeeny 使用它自己的保持活动消息,因为并非所有类型的传输都具有内置的保持活动机制。这些消息仅从客户端到服务器的 5 字节流。 KeepAliveInterval
间隔默认为 30 秒,当您在客户端上设置它时,如果客户端在最后一个KeepAliveInterval
期间没有成功发送任何内容,则会发送一条 ping 消息。
您必须将KeepAliveInterval
设置为小于服务器的ReceiveTimeout
,至少是服务器的ReceiveTimeout
的 1/2 或 1/3,因为如果服务器在ReceiveTimeout
期间没有收到任何内容,它将超时并关闭连接
KeepAliveRetries
是失败的保持活动消息的数量,一旦到达,客户端就会决定连接断开并关闭。
在服务器上设置KeepAliveInterval
或KeepAliveRetries
无效。
为了使 Xeeny 能够在线路上封送方法参数和返回类型,它需要将它们序列化。框架已经支持三个序列化器
MessagePackSerializer
:是由 MsgPack.Cli 实现的 MessagePack 序列化,它是默认序列化程序,因为序列化数据很小并且给定库中 .net 的实现速度很快。JsonSerializer
:Newtonsoft 实现的 Json 序列化器ProtobufSerializer
:由 Protobuf-net 实现的 Google ProtoBuffers 序列化器您可以通过调用WithXXXSerializer
使用构建器选择序列化器,只需确保您的类型可以使用所选序列化器进行序列化。
var host = new ServiceHostBuilder < ChatService > ( InstanceMode . Single )
. WithCallback < ICallback > ( )
. WithProtobufSerializer ( )
. CreateHost ( ) ;
await host . Open ( ) ;
WithSerializer(ISerializer serializer)
来使用自己的序列化器Xeeny使用TLS 1.2(目前仅通过TCP),您需要将X509Certificate
添加到服务器
var host = new ServiceHostBuilder < Service > ( .. . )
. AddTcpServer ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForServer ( x509Certificate2 ) ;
} )
.. .
在客户端上,您需要传递Certificate Name
:
await new ConnectionBuilder < IService > ( )
. WithTcpTransport ( tcpAddress , options =>
{
options . SecuritySettings = SecuritySettings . CreateForClient ( certificateName ) ;
} )
.. .
如果您想验证远程证书,您可以将RemoteCertificateValidationCallback
可选委托传递给SecuritySettings.CreateForClient
Xeeny 使用与 Asp.Net Core 中相同的日志系统
要使用记录器,请添加记录器的nuget包,然后调用WithXXXLogger
您可以在其中传递LogLevel
您可能想要命名连接,以便在调试或分析日志时轻松发现它们,您可以通过在选项中设置ConnectionNameFormatter
函数委托来实现此目的,该委托将作为参数传递给IConnection.ConnectionId
,并且返回值将分配给IConnection.ConnectionName
。
var client1 = await new DuplexConnectionBuilder < IChatService , Callback > ( callback1 )
. WithTcpTransport ( address , options =>
{
options . ConnectionNameFormatter = id => $ "First-Connection ( { id } )" ;
} )
. WithConsoleLogger ( LogLevel . Trace )
. CreateConnection ( ) ;
Xeeny 旨在实现高性能和异步,拥有异步合约允许框架完全异步。尝试始终让您的操作返回Task
或Task<T>
而不是void
或T
。这将节省一个额外的线程,该线程将等待底层异步套接字完成,以防您的操作不是异步的。
Xeeny 的开销是当它需要在运行时发出“新”类型时。当您创建ServiceHost<TService>
(调用ServiceHostBuilder<TService>.CreateHost()
)时,它会执行此操作,但每个类型都会发生一次,因此一旦 xeeny 发出给定类型的第一个主机,创建更多该类型的主机就不会出现性能问题。无论如何,这通常是您的应用程序的开始。
发出类型的另一个地方是当您创建给定合约或回调类型的第一个客户端时(调用CreateConnection
)。一旦该代理的第一种类型是发射器,下一个客户端将被创建而无需任何开销。 (请注意,除非您将false
传递给CreateConnection
否则您仍在创建新套接字和新连接)。
调用OperationContext.Current.GetCallback<T>
也会发出运行时类型,就像发出类型之上的所有其他发出一样,都会被缓存,并且开销仅在第一次调用时发生。您可以根据需要多次调用此方法,但最好缓存返回结果。
您可以获得上述所有 Xeeny 框架功能,以与您的自定义传输配合使用(假设您希望它位于设备蓝牙后面)。
XeenyListener
抽象类ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<T>.WithCustomTransport()
如果您想从头开始拥有自己的协议,则需要实现自己的连接、消息帧、并发、缓冲、超时、保持活动等。
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()