إطار عمل لبناء واستهلاك الخدمات عبر الأنظمة الأساسية في .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
events و 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
: يحدد المرشد كل اتصال (في الوقت الحالي لا يتطابق المعرف الموجود على الخادم والعميل)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
: حافظ على الفاصل الزمني بينج ( افتراضي 30 ثانية )KeepAliveRetries
: عدد مرات إعادة المحاولة قبل أن تقرر أن الاتصال متوقف ( الافتراضي 10 مرات إعادة المحاولة )SendBufferSize
: حجم المخزن المؤقت للإرسال ( الافتراضي 4096 بايت = 4 كيلو بايت )ReceiveBufferSize
: حجم المخزن المؤقت للاستلام ( افتراضي 4096 بايت = 4 كيلو بايت )MaxMessageSize
: الحد الأقصى لحجم الرسائل ( الافتراضي 1000000 بايت = 1 ميجابايت )ConnectionNameFormatter
: تفويض لتعيين أو تنسيق ConnectionName
( الافتراضي هو فارغ ). (انظر التسجيل)SecuritySettings
: إعدادات SSL ( الافتراضي فارغ ) (راجع الأمان)يمكنك الحصول على إجراء تكوين الخيارات هذه على الخادم عند الاتصال بـ 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
" Idle Remote Timeout " إذا قمت بتعيينه على الخادم، فسيحدد المهلة للخادم لإغلاق العملاء غير النشطين الذين هم العملاء الذين لا يرسلون أي طلب أو رسالة KeepAlive خلال ذلك الوقت.
يتم تعيين ReceiveTimeout
على العميل على Infinity افتراضيًا، إذا قمت بتعيينه على عميل الطباعة على الوجهين، فإنك تطلب من العميل تجاهل عمليات الاسترجاعات التي لا تأتي خلال ذلك الوقت وهو سيناريو غريب ولكنه لا يزال ممكنًا إذا اخترت القيام بذلك .
ReceiveBufferSize
هو حجم المخزن المؤقت المتلقي. لن يؤثر تعيينه على قيم صغيرة على القدرة على استقبال الرسائل الكبيرة، إذا كان هذا الحجم صغيرًا بشكل ملحوظ مقارنة بالرسائل المطلوب تلقيها، فقم بإدخال المزيد من عمليات الإدخال والإخراج. من الأفضل أن تترك القيمة الافتراضية في البداية، ثم إذا لزم الأمر، قم بإجراء اختبار التحميل والتحليل للعثور على الحجم الذي يؤدي أداءً جيدًا ويشغل
SendBufferSize
هو حجم المخزن المؤقت للإرسال. لن يؤثر تعيينه على قيم صغيرة على القدرة على إرسال رسائل كبيرة، إذا كان هذا الحجم صغيرًا بشكل ملحوظ مقارنة بالرسائل المراد إرسالها، فقم بإدخال المزيد من عمليات الإدخال والإخراج. من الأفضل أن تترك القيمة الافتراضية في البداية، ثم إذا لزم الأمر، قم بإجراء اختبار التحميل والتحليل للعثور على الحجم الذي يؤدي أداءً جيدًا ويشغل ذاكرة أقل.
يجب أن يكون ReceiveBufferSize
الخاص بالمستقبل مساوياً لـ SendBufferSize
الخاص بالمرسل لأن بعض وسائل النقل مثل UDP لن تعمل بشكل جيد إذا لم يكن هذين الحجمين متساويين. في الوقت الحالي، لا يتحقق Xeeny من أحجام المخزن المؤقت ولكن في المستقبل سأقوم بتعديل البروتوكول ليشمل هذا الفحص أثناء معالجة الاتصال.
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 تم تنفيذه بواسطة NewtonsoftProtobufSerializer
: برنامج تسلسل ProtoBuffers من Google والذي تم تنفيذه بواسطة 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()