MSF的名字是 Message Service Framework 的簡稱,由於目前框架主要功能在於處理即時(immediately)消息,所以iMSF就是 immediately Message Service Framework,中文名稱:即時消息服務框架,它是PDF.NET框架的一部分。
在后續的文章中,iMSF跟MSF是一個意思,或者你也可以給它取一個好聽的中文名稱:愛美XX :)
在前一篇, “一切都是消息”--MSF(消息服務框架)入門簡介, 我們介紹了MSF基於異步通信,支持請求-響應通信模式和發布-訂閱通信模式,並且介紹了如何獲取MSF。今天,我們來看看如何使用MSF來做一個請求-響應通信模式的例子。
MSF封裝了WCF,所以使用MSF不能像使用WCF那樣直接在客戶端添加服務引用,你需要手工編寫客戶端代理類,這樣有一個好處就是代理類寫的更簡單,使用更靈活。我們可以看看網友寫的這篇文章《不引用服務而使用WCF,手動編寫客戶端代理類 》,看看直接使用WCF是如何手動編寫客戶端代理類的。我對作者文中有一句話很認同:
--我們應當把WCF理解為一種通信技術,而不只是服務。
這正是MSF的設計理念!
回到MSF,我們來看看實現請求-響應通信模式的步驟。
一,編寫iMSF服務類
在上一篇文中搭建好的MSF解決方案中,我們創建了一個名字為 TestService的項目,首先,添加Nuget 的MSF服務端引用,
Install-Package PDF.Net.MSF.Service
現在添加一個類 Service1,讓它繼承MSF的IService 接口。具體代碼如下:
using PWMIS.EnterpriseFramework.Service.Runtime; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace TestService { public class Service1:IService { public void CompleteRequest(IServiceContext context) { throw new NotImplementedException(); } public bool IsUnSubscribe { get { throw new NotImplementedException(); } } public bool ProcessRequest(IServiceContext context) { throw new NotImplementedException(); } } }
然后,將上面方法中的異常信息注釋掉,並且添加一個 SayHello 方法,具體修改如下:
public class Service1:IService { public string SayHello(string who) { return string.Format("Hello {0} ,I am MSF Server.", who); } public void CompleteRequest(IServiceContext context) { //throw new NotImplementedException(); } public bool IsUnSubscribe { //get { throw new NotImplementedException(); } get { //返回True ,表示當前服務不執行系統后續的服務方法的訂閱處理過程,而是由用戶自己輸出結果數據 return false; } } public bool ProcessRequest(IServiceContext context) { //throw new NotImplementedException(); return true; } }
可以看到,實現MSF服務類,不需要先定義一個WCF服務契約接口,也沒用其它WCF的代碼影子,不過有點類似 ASP.NET 的HTTP處理過程:
- 首先,每個MSF服務類都會執行 ProcessRequest 方法,它有一個IServiceContext 對象,通過它可以知道請求相關的上下文信息;
- 然后,會有一個 IsUnSubscribe 屬性,表示本次請求是否是一個自定義的服務訂閱而不再繼續執行系統的服務訂閱,雖然本次的示例是演示“請求-響應”的通信模式的,但是MSF本質上對此種通信模式還是通過“發布-訂閱”通信模式實現的,也就是說,MSF的消息通信,始終是面向長連接的;
- 如果IsUnSubscribe 屬性返回為False,緊接着,MSF會調用您真正的服務方法,比如這里的 SayHello 方法;
- 最后,你可以在 CompleteRequest 中執行一些本次服務處理的收尾工作。
二,編寫iMSF客戶端
我們在上一篇文中說的TestClient 項目中,來編寫今天的MSF客戶端代碼,在原有代碼基礎上,做適當的修改。
MSF客戶端調用可以分為多種方式:
2.1,使用服務請求的URI模式:
本質上,MSF的客戶端請求服務端的時候,是將請求的服務信息轉換成MSF固有的URI地址參數信息的,類似RESTfull WebAPI 的URI一樣,本次請求的URI地址如下:
Service://Service1/SayHello/System.String=bluedoctor1
它表示我們請求的服務類名稱是 Service1,請求的服務方法名稱是 SayHello,有一個String類型的參數並且給它賦值為 bluedoctor 。
MSF請求服務的時候,服務方法的參數是不用區分參數名的,只跟參數的順序,參數的類型和參數的數量有關系,這根我們使用委托方法調用一個實際的方法一樣的方式。
相關調用代碼如下:
client.RequestService<string>("Service://Service1/SayHello/System.String=bluedoctor1", PWMIS.EnterpriseFramework.Common.DataType.Text, s => { Console.WriteLine("1,Server Response:【{0}】", s); });
如果調用服務成功,將輸出結果:
1,Server Response:【Hello bluedoctor1 ,I am is MSF Server.】
2.2,使用ServiceRequest 對象來封裝服務請求信息
前面通過服務請求的URI模式雖然比較直觀簡潔,但使用對象來封裝請求信息可能更可靠,對前面例子改寫成 ServiceRequest 對象的代碼如下:
ServiceRequest request = new ServiceRequest(); request.ServiceName = "Service1"; request.MethodName = "SayHello"; request.Parameters = new object[] { "bluedoctor23" }; client.RequestService<string>(request, PWMIS.EnterpriseFramework.Common.DataType.Text, s => { Console.WriteLine("2,Server Response:【{0}】", s); });
2.3,使用異步調用方法
前面兩個調用示例,其實都是傳入一個委托方法給RequestService 方法,然后服務端回調此委托方法的,而此委托方法的回調時機是不確定的,相對於調用線程它是異步執行的,所以我們稱呼前面2種調用方式為“異步委托方法”。這種方式的一大缺點就是我們的代碼中會有大量的難以閱讀和調試的異步回調代碼。.NET 4.0之后提供了Task對象它可以簡化這種調用過程,使得代碼寫起來就跟同步調用代碼一樣。這種異步調用方式,MSF也提供了支持,使用服務請求的Async后綴的方法即可:
string serverMsg= client.RequestServiceAsync<string>(request).Result; Console.WriteLine("3,Server Response:【{0}】", serverMsg);
調用 RequestServiceAsync 方法返回結果的 Result方法,能夠同步阻塞調用結果,使得后續代碼按照我們預期的順序執行。
三、注冊iMSF服務類
運行上面編寫的服務端和客戶端,調用並不成功,在服務端出現了下面的異常:
上面截圖中顯示的錯誤信息是 :“從注冊的所有容器中沒有找到符合當前類型的提供程序。”
這個錯誤信息會返回到客戶端:
處理服務時錯誤:從注冊的所有容器中沒有找到符 合當前類型的提供程序。
這個錯誤提示我們沒有注冊我們的MSF服務類,因為MSF會通過IOC容器去尋找我們調用的服務類,所以需要注冊下。
MSF采用了一個簡單的IOC工具,它支持通過XML配置文件類注冊我們自定義的MSF服務類。
在解決方案中,看到引用了MSF Host的主項目 MSFTest,nuget添加MSF Host的時候,已經添加了一個IOC配置文件:IOCConfig.xml
這個文件的使用,在MSF Host的配置文件 PdfNetEF.MessageServiceHost.exe.config 中做了配置:
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="IOCConfigFile" value=".\IOCConfig.xml" /> <add key="ServerIP" value="127.0.0.1" /> <add key="ServerPort" value="8888" /> </appSettings> ... 其它配置內容略
IOCConfig.xml 文件已經配置了MSF必要的內容和一些示例的測試配置,具體內容為:
<?xml version="1.0" encoding="utf-8" ?> <IOCConfig> <!-- IOC 依賴注入容器配置 ver 1.0 注:PDF.NET MSF Servic Host 使用 PWMIS 消息服務框架,2011.12.7 創建 --> <GroupConfig> <Group ID="1" ParentID="0" Name="ServiceRuntime" >分布式服務運行時</Group> <Group ID="2" ParentID="0" Name="ServiceModel" >服務模型</Group> <Group ID="3" ParentID="0" Name="TestService" >示例服務</Group> </GroupConfig> <SystemInterface> <Add Name="IService" Interface="PWMIS.EnterpriseFramework.Service.IService" Assembly="PWMIS.EnterpriseFramework.Service.Runtime"/> </SystemInterface> <GroupSet> <IOC Name="ServiceRuntime"> <Add Key="CacheServer" InterfaceName="IService" FullClassName="PWMIS.EnterpriseFramework.Service.Runtime.CacheService" Assembly="PWMIS.EnterpriseFramework.Service.Runtime" /> <Add Key="RegService" InterfaceName="IService" FullClassName="PWMIS.EnterpriseFramework.Service.Group.RegService" Assembly="PWMIS.EnterpriseFramework.Service.Group" /> <Add Key="ManageService" InterfaceName="IService" FullClassName="TranstarAuction.Service.Runtime.ManageService" Assembly="TranstarAuctionServiceRuntime" /> </IOC> <IOC Name="TestService"> <Add Key="Calculator" InterfaceName="IService" FullClassName="ServiceSample.TestCalculatorService" Assembly="ServiceSample" /> <Add Key="TestTimeService" InterfaceName="IService" FullClassName="ServiceSample.TestTimeService" Assembly="ServiceSample" /> <Add Key="AlarmClockService" InterfaceName="IService" FullClassName="ServiceSample.AlarmClockService" Assembly="ServiceSample" /> <Add Key="Service1" InterfaceName="IService" FullClassName="TestService.Service1" Assembly="TestService" /> </IOC> <IOC Name="ServiceModel"> <Add Key="TimeCount" InterfaceName="" FullClassName="Model.TimeCount" Assembly="Model" /> <!-- 下面4個是消息服務框架必須的ServiceModel --> <Add Key="ServiceIdentity" InterfaceName="" FullClassName="PWMIS.EnterpriseFramework.Service.Runtime.Principal.ServiceIdentity" Assembly="PWMIS.EnterpriseFramework.Service.Runtime" /> <Add Key="CacheItemPolicy" InterfaceName="" FullClassName="System.Runtime.Caching.CacheItemPolicy" Assembly="System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <Add Key="ServiceRegModel" InterfaceName="" FullClassName="PWMIS.EnterpriseFramework.Service.Client.Model.ServiceRegModel" Assembly="PWMIS.EnterpriseFramework.Service.Client" /> <Add Key="ServiceHostInfo" InterfaceName="" FullClassName="PWMIS.EnterpriseFramework.Service.Client.Model.ServiceHostInfo" Assembly="PWMIS.EnterpriseFramework.Service.Client" /> </IOC> </GroupSet> </IOCConfig>
其中,Key=“Service1” 配置節,便是我們今天需要配置的Service1 服務類的內容。
四、返回復雜類型的服務方法
4.1,編寫iMSF服務類
在前面的示中,服務類 Service1 的服務方法 SayHello 返回的是String 類型這樣的簡單類型,很多時候,我們需要服務方法返回結構復雜的自定義業務類型。在本次示例中,我們定義一個郵件消息類,我們新建一個C# 類庫項目 TestDto,然后如下定義它:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TestDto { public class MailMessage { public string Sender { get; set; } public string Reply { get; set; } public string Message { get; set; } public DateTime RevoveryTime { get; set; } } }
然后,回到TestService項目的Service1服務類,增加一個GetMailMessage 方法:
public MailMessage GetMailMessage(string who) { MailMessage mail = new MailMessage(); mail.Reply = who; mail.Sender = "MSF Server"; mail.Message = string.Format("Hello {0} ,I am MSF Server.", who); mail.RevoveryTime = DateTime.Now; return mail; }
4.2,編寫iMSF客戶端代碼
最后,我們在TestClient中添加調用此服務的客戶端代碼:
client.RequestService<MailMessage>("Service://Service1/GetMailMessage/System.String=bluedoctor4", PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("4,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message,mail.RevoveryTime); });
調用結果將輸出:
4,Server Response:【Hello bluedoctor4 ,I am is MSF Server.】
同樣,我們也可以使用ServiceRequest 對象來調用這個服務:
ServiceRequest request2 = new ServiceRequest(); request2.ServiceName = "Service1"; request2.MethodName = "GetMailMessage"; request2.Parameters = new object[] { "bluedoctor567" }; client.RequestService<MailMessage>(request2, PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("5,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message, mail.RevoveryTime); });
4.3,使用客戶端自定義結果類型
我們調用服務方法的時候,客戶端調用服務方法返回值的對象類型,可以跟服務方法定義的返回類型名字不一樣,比如GetMailMessage 方法調用,我們可以在本地定義一個結構相同的不同的類:
namespace TestClient { public class MailMessage2 { public string Sender { get; set; } public string Reply { get; set; } public string Message { get; set; } public DateTime RevoveryTime { get; set; } } }
然后在客戶端調用,使用這個新的在客戶端定義的類型作為服務方法調用的返回值:
client.RequestService<MailMessage2>(request2, PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("6,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message, mail.RevoveryTime); });
輸出結果跟上面是一樣的。這是因為服務端和客戶端使用的都是JSON序列化,它是不關心類型的名字的只關心內部數據結構是否一致。
五、小結
上面的過程演示了MSF編寫服務端和客戶端代碼的簡單過程,對MSF而言,服務是本質上都是異步調用和返回的,服務方法返回結果不僅支持簡單類型,還支持復雜類型;客戶端支持多種調用代碼書寫方式。
雖然MSF是基於WCF構建的,但是從本文的示例過程看,僅使用MSF,無需掌握任何WCF的知識。
下面是完整的MSF服務端代碼:

namespace TestService { public class Service1 : IService { public string SayHello(string who) { return string.Format("Hello {0} ,I am MSF Server.", who); } public MailMessage GetMailMessage(string who) { MailMessage mail = new MailMessage(); mail.Reply = who; mail.Sender = "MSF Server"; mail.Message = string.Format("Hello {0} ,I am MSF Server.", who); mail.RevoveryTime = DateTime.Now; return mail; } public void CompleteRequest(IServiceContext context) { //throw new NotImplementedException(); } public bool IsUnSubscribe { //get { throw new NotImplementedException(); } get { //返回True ,表示當前服務不執行系統后續的服務方法的訂閱處理過程,而是由用戶自己輸出結果數據 return false; } } public bool ProcessRequest(IServiceContext context) { //throw new NotImplementedException(); return true; } } }
下面是完整的客戶端代碼:

namespace MSFTest { class Program { static void Main(string[] args) { Console.WriteLine("******** PDF.NET MSF 客戶端測試程序 *********"); Console.WriteLine(); Proxy client = new Proxy(); client.ErrorMessage += client_ErrorMessage; Console.Write("請輸入服務器的主機名或者IP地址(默認 127.0.0.1):"); string host = Console.ReadLine(); if (string.IsNullOrEmpty(host)) host = "127.0.0.1"; Console.WriteLine("服務地址:{0}",host); Console.Write("請輸入服務的端口號(默認 8888):"); string port = Console.ReadLine(); if (string.IsNullOrEmpty(port)) port = "8888"; Console.WriteLine("服務端口號:{0}", port); client.ServiceBaseUri = string.Format("net.tcp://{0}:{1}", host, port); Console.WriteLine("當前客戶端代理的服務基礎地址是:{0}",client.ServiceBaseUri); Console.WriteLine(); Console.WriteLine("MSF 請求-響應 模式調用示例:"); client.RequestService<string>("Service://Service1/SayHello/System.String=bluedoctor1", PWMIS.EnterpriseFramework.Common.DataType.Text, s => { Console.WriteLine("1,Server Response:【{0}】", s); }); ServiceRequest request = new ServiceRequest(); request.ServiceName = "Service1"; request.MethodName = "SayHello"; request.Parameters = new object[] { "bluedoctor23" }; client.RequestService<string>(request, PWMIS.EnterpriseFramework.Common.DataType.Text, s => { Console.WriteLine("2,Server Response:【{0}】", s); }); string serverMsg= client.RequestServiceAsync<string>(request).Result; Console.WriteLine("3,Server Response:【{0}】", serverMsg); client.RequestService<MailMessage>("Service://Service1/GetMailMessage/System.String=bluedoctor4", PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("4,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message,mail.RevoveryTime); }); ServiceRequest request2 = new ServiceRequest(); request2.ServiceName = "Service1"; request2.MethodName = "GetMailMessage"; request2.Parameters = new object[] { "bluedoctor567" }; client.RequestService<MailMessage>(request2, PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("5,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message, mail.RevoveryTime); }); client.RequestService<MailMessage2>(request2, PWMIS.EnterpriseFramework.Common.DataType.Json, mail => { Console.WriteLine("6,Server Response:【{0}】,\r\n Revovery Time:{1}", mail.Message, mail.RevoveryTime); }); MailMessage mail2 = client.RequestServiceAsync<MailMessage>(request2).Result; Console.WriteLine("7,Server Response:【{0}】,\r\n Revovery Time:{1}", mail2.Message, mail2.RevoveryTime); Console.WriteLine("按回車鍵繼續"); Console.ReadLine(); Console.WriteLine(); Console.WriteLine("MSF 發布-訂閱 模式調用示例:"); string repMsg = "你好!"; client.SubscribeTextMessage("我是客戶端", serverMessage => { Console.WriteLine(); Console.WriteLine("[來自服務器的消息]::{0}", serverMessage); }); while (repMsg != "") { Console.Write("回復服務器(輸入為空,則退出):>>"); repMsg = Console.ReadLine(); client.SendTextMessage(repMsg); } Console.WriteLine("測試完成,退出"); } static void client_ErrorMessage(object sender, MessageSubscriber.MessageEventArgs e) { Console.WriteLine("---處理服務時錯誤:{0}",e.MessageText); } } }
下面是運行客戶端輸出的結果示例:
******** PDF.NET MSF 客戶端測試程序 ********* 請輸入服務器的主機名或者IP地址(默認 127.0.0.1): 服務地址:127.0.0.1 請輸入服務的端口號(默認 8888): 服務端口號:8888 當前客戶端代理的服務基礎地址是:net.tcp://127.0.0.1:8888 MSF 請求-響應 模式調用示例: 1,Server Response:【Hello bluedoctor1 ,I am MSF Server.】 3,Server Response:【Hello bluedoctor23 ,I am MSF Server.】 2,Server Response:【Hello bluedoctor23 ,I am MSF Server.】 7,Server Response:【Hello bluedoctor567 ,I am MSF Server.】, Revovery Time:2017-10-9 15:50:33 按回車鍵繼續 4,Server Response:【Hello bluedoctor4 ,I am MSF Server.】, Revovery Time:2017-10-9 15:50:33 5,Server Response:【Hello bluedoctor567 ,I am MSF Server.】, Revovery Time:2017-10-9 15:50:33 6,Server Response:【Hello bluedoctor567 ,I am MSF Server.】, Revovery Time:2017-10-9 15:50:33
在下一篇,我們將演示MSF的“發布-訂閱”通信模式。
本篇測試程序全部源碼,已經上傳到GitHub:
https://github.com/bluedoctor/MSFTest
歡迎加入我們的QQ群討論MSF框架的使用,群號:敏思(PWMIS) .NET 18215717,加群請注明:PDF.NET技術交流,否則可能被拒。