Платформа для создания и использования кроссплатформенных сервисов в .Net Standard.
Кроссплатформенный, дуплексный, масштабируемый, настраиваемый и расширяемый
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 ) ;
}
}
ServiceHost
, используя ServiceHostBuilder<TService>
, где находится реализация службы.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 определяет три режима создания экземпляров сервиса.
Вы определяете режим экземпляра службы, используя перечисление InstanceMode
при создании ServiceHost.
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 ) ;
}
Если у вас есть перегрузка методов в одном интерфейсе (или аналогичная сигнатура метода в родительском интерфейсе), вам необходимо отличить их друг от друга с помощью атрибута Operation
, установив свойство Name
. Это применимо как к контрактам на обслуживание, так и к контрактам обратного вызова.
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 идентифицирует каждое соединение (на данный момент идентификаторы на сервере и клиенте не совпадают).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 минут, для клиента по умолчанию: бесконечность ).KeepAliveInterval
: интервал проверки связи ( по умолчанию 30 секунд ).KeepAliveRetries
: количество повторных попыток, прежде чем будет принято решение об отключении соединения ( по умолчанию 10 попыток ).SendBufferSize
: размер буфера отправки ( по умолчанию 4096 байт = 4 КБ ).ReceiveBufferSize
: размер приемного буфера ( по умолчанию 4096 байт = 4 КБ ).MaxMessageSize
: Максимальный размер сообщений ( по умолчанию 1000000 байт = 1 МБ ).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
— размер приемного буфера. Установка малых значений не повлияет на возможность получения больших сообщений, но если этот размер значительно мал по сравнению с получаемыми сообщениями, введите больше операций ввода-вывода. Лучше оставить значение по умолчанию в начале, а затем, при необходимости, провести нагрузочное тестирование и анализ, чтобы найти размер, который работает хорошо и занимает
SendBufferSize
— размер буфера отправки. Установка малых значений не повлияет на возможность отправки больших сообщений, но если этот размер значительно мал по сравнению с отправляемыми сообщениями, введите больше операций ввода-вывода. Лучше оставить значение по умолчанию в начале, а затем, при необходимости, провести нагрузочное тестирование и анализ, чтобы найти размер, который работает хорошо и занимает меньше памяти.
ReceiveBufferSize
получателя должен равняться SendBufferSize
отправителя, поскольку некоторые транспорты, такие как UDP, не будут работать должным образом, если эти два размера не равны. На данный момент Xeeny не проверяет размеры буфера, но в будущем я модифицирую протокол, чтобы включить эту проверку во время обработки Connect.
MaxMessageSize
— максимально допустимое количество байтов для получения. Это значение не имеет ничего общего с буферами, поэтому не влияет на память или производительность. Однако это значение важно для проверки ваших клиентов и предотвращения огромных сообщений от клиентов. Xeeny использует протокол префикса размера, поэтому при поступлении сообщения оно будет буферизовано в буфере с размером ReceiveBufferSize
который должен быть намного меньше, чем MaxMessageSize
. Читается заголовок размера. Если размер больше MaxMessageSize
сообщение отклоняется и соединение закрывается.
Xeeny использует свои собственные сообщения поддержания активности, поскольку не все виды транспорта имеют встроенный механизм поддержания активности. Эти сообщения представляют собой поток размером 5 байт только от клиента к серверу. Интервал KeepAliveInterval
по умолчанию составляет 30 секунд. Если вы установите его на клиенте, клиент отправит сообщение ping, если он не смог ничего успешно отправить во время последнего KeepAliveInterval
.
Вам необходимо установить KeepAliveInterval
меньше, чем ReceiveTimeout
сервера, по крайней мере 1/2 или 1/3 от ReceiveTimeout
сервера, потому что сервер истечет по тайм-ауту и закроет соединение, если он ничего не получил во время его ReceiveTimeout
KeepAliveRetries
— это количество неудачных сообщений проверки активности, после достижения которых клиент решает, что соединение разорвано, и закрывается.
Установка KeepAliveInterval
или KeepAliveRetries
на сервере не имеет никакого эффекта.
Чтобы Xeeny мог маршалировать параметры метода и возвращаемые типы в сети, необходимо их сериализовать. В рамках уже поддерживаются три сериализатора.
MessagePackSerializer
: реализована ли сериализация MessagePack с помощью MsgPack.Cli. Это сериализатор по умолчанию, поскольку сериализованные данные невелики, а реализация .net в данной библиотеке выполняется быстро.JsonSerializer
: сериализатор Json, реализованный Newtonsoft.ProtobufSerializer
: сериализатор Google ProtoBuffers, реализованный Protobuf-net. Вы можете выбрать сериализатор с помощью сборщиков, вызвав 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 для работы с вашим пользовательским транспортом (скажем, вы хотите, чтобы он работал за устройством Blueetooth).
XeenyListener
ServiceHostBuilder<T>.AddCustomServer()
IXeenyTransportFactory
ConnectionBuilder<T>.WithCustomTransport()
Если вы хотите создать свой собственный протокол с нуля, вам необходимо реализовать свои собственные возможности подключения, кадрирования сообщений, параллелизма, буферизации, тайм-аута, поддержания активности и т. д.
IListener
ServiceHostBuilder<T>.AddCustomServer()
ITransportFactory
ConnectionBuilder<T>.WithCustomTransport()