“一切都是訊息”--MSF(訊息服務框架)之【請求-響應】模式(點對點)
在前一篇, “一切都是訊息”--MSF(訊息服務框架)入門簡介, 我們介紹了MSF基於非同步通訊,支援請求-響應通訊模式和釋出-訂閱通訊模式,並且介紹瞭如何獲取MSF。今天,我們來看看如何使用MSF來做一個請求-響應通訊模式的例子。
MSF封裝了WCF,所以使用MSF不能像使用WCF那樣直接在客戶端新增服務引用,你需要手工編寫客戶端代理類,這樣有一個好處就是代理類寫的更簡單,使用更靈活。我們可以看看網友寫的這篇文章《不引用服務而使用WCF,手動編寫客戶端代理類 》,看看直接使用WCF是如何手動編寫客戶端代理類的。我對作者文中有一句話很認同:
--我們應當把WCF理解為一種通訊技術,而不只是服務。
這正是MSF的設計理念!
回到MSF,我們來看看實現請求-響應通訊模式的步驟。
一,編寫MSF服務類
在上一篇文中搭建好的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 中執行一些本次服務處理的收尾工作。
二,編寫MSF客戶端
我們在上一篇文中說的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方法,能夠同步阻塞呼叫結果,使得後續程式碼按照我們預期的順序執行。
三、註冊MSF服務類
執行上面編寫的服務端和客戶端,呼叫並不成功,在服務端出現了下面的異常:
![](https://img.796t.com/res/2022/04-29/02/aa72b95fa2d3d65fe4164f146d17c457.png)
上面截圖中顯示的錯誤資訊是 :“從註冊的所有容器中沒有找到符合當前型別的提供程式。”
這個錯誤資訊會返回到客戶端:
處理服務時錯誤:從註冊的所有容器中沒有找到符 合當前型別的提供程式。
這個錯誤提示我們沒有註冊我們的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,編寫MSF服務類
在前面的示中,服務類 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,編寫MSF客戶端程式碼
最後,我們在TestClient中新增呼叫此服務的客戶端程式碼:
client.RequestService<MailMessage>("Service://Service1/GetMailMessage/System.String=bluedoctor4",
PWMIS.EnterpriseFramework.Common.DataType.Json,
mail =>
{
Console.WriteLine("4,Server Response:【{0}】,rn 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}】,rn 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}】,rn 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}】,rn 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}】,rn Revovery Time:{1}", mail.Message, mail.RevoveryTime);
});
client.RequestService<MailMessage2>(request2,
PWMIS.EnterpriseFramework.Common.DataType.Json,
mail =>
{
Console.WriteLine("6,Server Response:【{0}】,rn Revovery Time:{1}", mail.Message, mail.RevoveryTime);
});
MailMessage mail2 = client.RequestServiceAsync<MailMessage>(request2).Result;
Console.WriteLine("7,Server Response:【{0}】,rn 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技術交流,否則可能被拒。