772
技術社區[雲棲]
通過一個模擬程序讓你明白WCF大致的執行流程
在《通過一個模擬程序讓你明白ASP.NET MVC是如何運行的》一文中我通過一個普通的ASP.NET Web程序模擬了ASP.NET MVC的執行流程,現在我們通過類似的原理創建一個用於模擬WCF服務端和客戶端工作原理的模擬程序。[源代碼從這裏下載]
目錄
一、基本的組件和執行流程
二、創建自定義HttpHandler實現對服務調用請求的處理
三、定義創建WCF組件的工廠
四、定義HttpModule映射WcfHandler
五、創建自定義的真實代理實現服務的調用
六、定義服務代理工廠
七、服務“寄宿”和調用
我們隻模擬WCF完成一個簡單的服務調用所必需的組件和流程,右圖反映了進行服務調用的必要步驟和使用的相關WCF組件。下麵列出了服務端涉及的組件和流程:
- 請求消息的接收和回複消息的發送:服務端在傳輸層監聽與接收來自客戶的請求,並將經過編碼後的回複消息通過傳輸層發送到客戶端;
- 請求消息的解碼和回複消息的編碼:將接收到的字節數組通過解碼生成請求消息對象,並將回複消息通過編碼轉化成字節數組。消息的編碼和解碼通過消息編碼器(MessageEncoder)完成,而消息編碼器工廠(MessageEncoderFactory)負責創建該對象;
- 請求消息的反序列化和回複消息的序列化:對請求消息進行反序列化,為服務操作的執行生成相應的輸入參數,以及將服務操作執行的結果(返回值或輸出/引用參數)序列化,並生成回複消息。序列化和反序列化通過分發消息格式化器(DispatchMessageFormatter)完成;
- 服務對象的創建:創建或激活服務對象實例,實例提供者(InstanceProvider)用於服務對象的創建或獲取,本例直接通過反射創建服務實例;
- 服務操作的執行:調用創建的服務對象的操作方法,並傳入經過反序列化生成的輸入參數。操作調用器(OperationInvoker)完成對服務操作的最終執行。
相較於服務端的請求監聽、消息接收、服務實例激活和操作調用流程,客戶端的處理流程顯得相對簡單,僅僅包含以下3個必需的步驟:
-
請求消息的序列化和回複消息的反序列化:生成請求消息並將輸入參數序列化到請求消息中,以及對回複消息進行反序列化,轉化成方法調用的返回值或輸出/引用參數。序列化和反序列化通過ClientMessageFormatter完成;
- 請求消息的編碼和回複消息的解碼:對請求消息進行編碼生成字節數組供傳輸層發送,以及將傳輸層接收到的字節數組解碼生成回複消息。消息的編碼和解碼通過消息編碼器完成,而消息編碼器工廠負責創建該對象;
- 請求消息的發送和回複消息的接收:在傳輸層將經過編碼的請求消息發送到服務端,以及接收來自服務端的回複消息。
本實例的解決方法依然采用包含Service.Interface、Service和Client三個項目的結構,不過Service項目現在是一個Web應用。也就是說我們通過一個Web應用的方式實現WCF端對服務調用請求的整個處理流程。
對於一個ASP.NET Web應用來說,對請求的處理最終都落實到一個具體的HttpHandler對象上,所以我們通過實現接口System.Web.IHttpHandler自定義了如下一個WcfHandler用於處理針對WCF服務請求的處理。
1: public class WcfHandler: IHttpHandler
2: {
3: //其他成員
4: public Type ServiceType { get; private set; }
5: public MessageEncoderFactory MessageEncoderFactory { get; private set; }
6: public IDictionary<string, MethodInfo> Methods { get; private set; }
7: public IDictionary<string, IDispatchMessageFormatter> MessageFormatters { get; private set; }
8: public IDictionary<string, IOperationInvoker> OperationInvokers { get; private set; }
9:
10: public bool IsReusable
11: {
12: get { return false; }
13: }
14:
15: public WcfHandler(Type serviceType, MessageEncoderFactory messageEncoderFactory)
16: {
17: this.ServiceType = serviceType;
18: this.MessageEncoderFactory = messageEncoderFactory;
19: this.Methods = new Dictionary<string, MethodInfo>();
20: this.MessageFormatters = new Dictionary<string, IDispatchMessageFormatter>();
21: this.OperationInvokers = new Dictionary<string, IOperationInvoker>();
22: }
23: }
如上麵代碼所示,上述的關於WCF服務端框架所需的組件以隻讀屬性的方式體現在WcfHandler上。ServiceType屬性表示服務的類型,基於這個類型通過反射創建服務實例。消息編碼器工廠通過MessageEncoderFactory屬性表示,兩個字典類型的屬性MessageFormatters和OperationInvokers代表基於操作的分發消息格式化器和操作調用器列表,字典的Key為操作請求消息的<Action>報頭的值。而Methods表示契約接口所有操作方法的MethodInfo集合。
針對WCF服務的請求處理實現在如下的ProcessRequest方法中,執行的邏輯也不算複雜。我們直接通過消息編碼器工廠創建的消息編碼從當前HTTP請求的輸入流中讀取出消息。然後根據當前消息的<Action>報頭的值從MessageFormatters屬性中找到與當前請求操作相匹配的分發消息格式化器對消息進行反序列化。
接著直接通過反射的方式根據服務類型創建服務實例對象。同樣根據當前消息的<Action>報頭從OperationInvokers屬性獲取出基於當前請求操作的操作調用器,並將創建的服務實例和反序列化後生成的參數作為輸入執行操作方法。
操作的執行結果通過分發消息格式化器進行序列化生成的消息最終通過消息編碼器寫入當前HTTP回複的輸出流中返回給客戶端。
1: public class WcfHandler: IHttpHandler
2: {
3: //其他成員
4: public void ProcessRequest(HttpContext context)
5: {
6: //對HttpPRequest進行解碼生成請求消息對象
7: Message request = this.MessageEncoderFactory.Encoder.ReadMessage(context.Request.InputStream, int.MaxValue, "application/soap+xml; charset=utf-8");
8:
9: //通過請求消息得到代表服務操作的Action
10: string action = request.Headers.Action;
11:
12: //通過Action從MethodInfo字典中獲取服務操作對應的MethodInfo對象
13: MethodInfo method = this.Methods[action];
14:
15: //得到輸出參數的數量
16: int outArgsCount = 0;
17: foreach (var parameter in method.GetParameters())
18: {
19: if (parameter.IsOut)
20: {
21: outArgsCount++;
22: }
23: }
24:
25: //創建數組容器,用於保存請求消息反序列後生成的輸入參數對象
26: int inputArgsCount = method.GetParameters().Length - outArgsCount;
27: object[] parameters = new object[inputArgsCount];
28: try
29: {
30: this.MessageFormatters[action].DeserializeRequest(request, parameters);
31: }
32: catch
33: {}
34:
35: List<object> inputArgs = new List<object>();
36: object[] outArgs = new object[outArgsCount];
37: //創建服務對象,在WCF中服務對象通過InstanceProvider創建
38:
39: object serviceInstance = Activator.CreateInstance(this.ServiceType);
40:
41: //執行服務操作
42: object result = this.OperationInvokers[action].Invoke(serviceInstance,parameters, out outArgs);
43:
44: //將操作執行的結果(返回值或者輸出參數)序列化生成回複消息
45: Message reply = this.MessageFormatters[action].SerializeReply(request.Version, outArgs, result);
46: context.Response.ClearContent();
47: context.Response.ContentEncoding = Encoding.UTF8;
48: context.Response.ContentType = "application/soap+xml; charset=utf-8";
49:
50: //對回複消息進行編碼,並將編碼後的消息通過HttpResponse返回
51: this.MessageEncoderFactory.Encoder.WriteMessage(reply, context.Response.OutputStream);
52: context.Response.Flush();
53: }
54: }
對於本例來說,客戶端和服務端需要的組件主要有四類,即消息編碼器工廠、分發消息格式化器、客戶端消息格式化器和操作調用器。我們通過具有如下定義的靜態的工廠類ComponentBuilder來創建它們。我們調用操作行為DataContractSerializerOperationBehavior的GetFormatter方法來創建基於指定操作的消息格式化器。不過該方法是一個內部方法,所以我們是通過反射的方式來調用的。isProxy參數表示創建的是客戶端消息格式化器(True)還是分發消息格式化器(False)。
消息編碼器工廠通過基於文本編碼方式綁定元素TextMessageEncodingBindingElement的CreateMessageEncoderFactory創建,傳入的參數分別表示消息的版本和文本編碼類型。我們采用SyncMethodInvoker以同步的方式進行操作的執行。由於SyncMethodInvoker是一個內部類型,所以我們不得不采用反射的方式來創建它。
1: public static class ComponentBuilder
2: {
3: public static object GetFormatter(OperationDescription operation, bool isProxy)
4: {
5: bool formatRequest = false;
6: bool formatReply = false;
7: DataContractSerializerOperationBehavior behavior = new DataContractSerializerOperationBehavior(operation);
8: MethodInfo method = typeof(DataContractSerializerOperationBehavior).GetMethod("GetFormatter", BindingFlags.Instance | BindingFlags.NonPublic);
9: return method.Invoke(behavior, new object[] { operation, formatRequest, formatReply, isProxy });
10: }
11:
12: public static MessageEncoderFactory GetMessageEncoderFactory(MessageVersion messageVersion, Encoding writeEncoding)
13: {
14: TextMessageEncodingBindingElement bindingElement = new TextMessageEncodingBindingElement(messageVersion, writeEncoding);
15: return bindingElement.CreateMessageEncoderFactory();
16: }
17: public static IOperationInvoker GetOperationInvoker(MethodInfo method)
18: {
19: string syncMethodInvokerType = "System.ServiceModel.Dispatcher.SyncMethodInvoker, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
20: Type type = Type.GetType(syncMethodInvokerType);
21: return (IOperationInvoker)Activator.CreateInstance(type, new object[]{method});
22: }
23: }
我們通過HttpModule的方式將用於處理WCF服務請求的映射到相應的WCF服務調用請求,為此我們定義了如下一個實現了System.Web.IHttpModule接口的WcfHttpModule類型。WcfHttpModule通過注冊HttpApplication的BeginRequest事件的方式將創建的WcfHandler映射為處理當前HTTP請求的HttpHandler。
1: public class WcfHttpModule: IHttpModule
2: {
3: public void Dispose() {}
4:
5: public void Init(HttpApplication context)
6: {
7: context.BeginRequest += (sender, args) =>
8: {
9: string relativeAddress = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.Remove(0,2);
10: Type serviceType = RouteTable.Routes.Find(relativeAddress);
11: if (null == serviceType)
12: {
13: return;
14: }
15: IHttpHandler handler = this.CreateHttpHandler(serviceType);
16: context.Context.RemapHandler(handler);
17: };
18: }
19: protected IHttpHandler CreateHttpHandler(Type serviceType)
20: {
21: MessageEncoderFactory encoderFactory = ComponentBuilder.GetMessageEncoderFactory(MessageVersion.Default, Encoding.UTF8);
22: WcfHandler handler = new WcfHandler(serviceType, encoderFactory);
23: Type interfaceType = serviceType.GetInterfaces()[0];
24: ContractDescription contract = ContractDescription.GetContract(interfaceType);
25: foreach (OperationDescription operation in contract.Operations)
26: {
27: IDispatchMessageFormatter messageFormatter = (IDispatchMessageFormatter)ComponentBuilder.GetFormatter(operation, false);
28: handler.MessageFormatters.Add(operation.Messages[0].Action, messageFormatter);
29:
30: IOperationInvoker operationInvoker = ComponentBuilder.GetOperationInvoker(operation.SyncMethod);
31: handler.OperationInvokers.Add(operation.Messages[0].Action, operationInvoker);
32:
33: handler.Methods.Add(operation.Messages[0].Action, operation.SyncMethod);
34: }
35: return handler;
36: }
37: }
至於WcfHandler的創建,需要確定服務的類型。而服務的類型隻能根據請求的地址來確定,這個IIS寄宿根據.svc文件來創建ServiceHost的原理是一樣的。對於本例來說,我們需要對請求的地址和服務類型作一個映射,為此我們定義了如下一個RouteMapping的類型表示這個映射。
1: public class RouteMapping
2: {
3: public string Address { get; private set; }
4: public Type ServiceType { get; private set; }
5: public RouteMapping(string address, Type serviceType)
6: {
7: this.Address = address;
8: this.ServiceType = serviceType;
9: }
10: }
而映射表則通過如下一個繼承自Collection<RouteMapping>的RouteTable來定義。泛型的Register<T>方法用於注冊地址與服務類型的映射關係,而Find方法則根據地址獲取相應的服務類型。靜態屬性Routes表示當前被使用的映射表,而在WcfHttpModule中正是通過這個靜態屬性根據解析出來的地址得到用於創建WcfHandler的服務類型的。
1: public class RouteTable: Collection<RouteMapping>
2: {
3: public static RouteTable Routes{get; private set;}
4: static RouteTable()
5: {
6: Routes = new RouteTable();
7: }
8: public Type Find(string address)
9: {
10: RouteMapping routeMapping = (from route in this
11: where string.Compare(route.Address, address,true) == 0
12: select route).FirstOrDefault();
13: return null == routeMapping? null: routeMapping.ServiceType;
14: }
15:
16: public void Register<T>(string address)
17: {
18: this.Add(new RouteMapping(address, typeof(T)));
19: }
20: }
ChannelFactory<TChannel>創建的服務代理僅僅是一個透明代理,而真實實現服務調用的是它的真實代理。為此我們創建了如下一個繼承自RealProxy的泛型的ServiceChannelProxy<TChannel>,其中泛型參數為契約接口類型。
1: public class ServiceChannelProxy<TChannel>: RealProxy
2: {
3: //其他成員
4: public Uri Address { get; private set; }
5: public MessageVersion MessageVersion { get; private set; }
6: public IDictionary<string, IClientMessageFormatter> MessageFormatters { get; private set; }
7: public MessageEncoderFactory MessageEncoderFactory { get; private set; }
8:
9: public ServiceChannelProxy(Uri address, MessageVersion messageVersion, MessageEncoderFactory encoderFactory): base(typeof(TChannel))
10: {
11: this.Address = address;
12: this.MessageVersion = messageVersion;
13: this.MessageEncoderFactory = encoderFactory;
14: this.MessageFormatters = new Dictionary<string, IClientMessageFormatter>();
15: }
16: }
和WcfHttpHandler類似,進行服務調用所需的組件通過相應的隻讀屬性表示。屬性MessageEncoderFactory表示消息編碼器工廠,而字典類型的MessageFormatters表示基於每個操作的客戶端消息格式化器列表,其中的Key為操作的名稱。屬性Address表示被調用服務的地址。
針對透明代理的方法調用最終都會轉移到針對真實真實代理的Invoke方法,所以我們將所有的服務調用操作實現在如下的Invoke方法中。我們首先獲取代表當前調用方法的MethodBase上應用的OperationContractAttribute特性,並借此獲得操作名稱。
根據獲取的操作名稱從屬性MessageFormatters屬性中獲得基於當前操作的客戶端消息格式化器,並將方法調用轉化消息。接著根據Address屬性表示的服務調用地址創建EndpointAddress對象並將其附加到請求消息中。除此之外,還需要為請求消息添加一些必要的報頭(比如<MessageId>和<ReplyTo>)。
接下來通過消息編碼器工廠創建的消息編碼器對消息進行編碼,並將得到的字節數據通過創建的HttpWebRequest對象發送出去。對於得到的HttpWebResponse,則通過消息編碼器進行解碼以生成回複消息。回複消息最終通過客戶端消息格式化器進行反序列化,得到的對象映射為方法返回值和輸出/引用參數返回。
1: public class ServiceChannelProxy<TChannel>: RealProxy
2: {
3: //其他成員
4: public override IMessage Invoke(IMessage msg)
5: {
6: IMethodCallMessage methodCall = (IMethodCallMessage)msg;
7:
8: //得到操作名稱
9: object[] attributes = methodCall.MethodBase.GetCustomAttributes(typeof(OperationContractAttribute), true);
10: OperationContractAttribute attribute = (OperationContractAttribute)attributes[0];
11: string operationName = string.IsNullOrEmpty(attribute.Name) ? methodCall.MethodName : attribute.Name;
12:
13: //序列化請求消息
14: Message requestMessage = this.MessageFormatters[operationName].SerializeRequest(this.MessageVersion, methodCall.InArgs);
15:
16: //添加必要的WS-Address報頭
17: EndpointAddress address = new EndpointAddress(this.Address);
18: requestMessage.Headers.MessageId = new UniqueId(Guid.NewGuid());
19: requestMessage.Headers.ReplyTo = new EndpointAddress("https://www.w3.org/2005/08/addressing/anonymous");
20: address.ApplyTo(requestMessage);
21:
22: //對請求消息進行編碼,並將編碼生成的字節發送通過HttpWebRequest向服務端發送
23: HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(this.Address);
24: webRequest.Method = "Post";
25: webRequest.KeepAlive = true;
26: webRequest.ContentType = "application/soap+xml; charset=utf-8";
27: ArraySegment<byte> bytes = this.MessageEncoderFactory.Encoder.WriteMessage(requestMessage, int.MaxValue, BufferManager.CreateBufferManager(long.MaxValue, int.MaxValue));
28: webRequest.ContentLength = bytes.Array.Length;
29: webRequest.GetRequestStream().Write(bytes.Array, 0, bytes.Array.Length);
30: webRequest.GetRequestStream().Close();
31: WebResponse webResponse = webRequest.GetResponse();
32:
33: //對HttpResponse進行解碼生成回複消息.
34: Message responseMessage = this.MessageEncoderFactory.Encoder.ReadMessage(webResponse.GetResponseStream(), int.MaxValue);
35:
36: //回複消息進行反列化生成相應的對象,並映射為方法調用的返回值或者ref/out參數
37: object[] allArgs = (object[])Array.CreateInstance(typeof(object),methodCall.ArgCount);
38: Array.Copy(methodCall.Args, allArgs, methodCall.ArgCount);
39: object[] refOutParameters = new object[GetRefOutParameterCount(methodCall.MethodBase)];
40: object returnValue = this.MessageFormatters[operationName].DeserializeReply(responseMessage, refOutParameters);
41: MapRefOutParameter(methodCall.MethodBase, allArgs, refOutParameters);
42:
43: //通過ReturnMessage的形式將返回值和ref/out參數返回
44: return new ReturnMessage(returnValue, allArgs, allArgs.Length, methodCall.LogicalCallContext, methodCall);
45: }
46:
47: private int GetRefOutParameterCount(MethodBase method)
48: {
49: int count = 0;
50: foreach (ParameterInfo parameter in method.GetParameters())
51: {
52: if (parameter.IsOut || parameter.ParameterType.IsByRef)
53: {
54: count++;
55: }
56: }
57: return count;
58: }
59:
60: private void MapRefOutParameter(MethodBase method, object[] allArgs,object[] refOutArgs)
61: {
62: List<int> refOutParamPositionsList = new List<int>();
63: foreach (ParameterInfo parameter in method.GetParameters())
64: {
65: if (parameter.IsOut || parameter.ParameterType.IsByRef)
66: {
67: refOutParamPositionsList.Add(parameter.Position);
68: }
69: }
70: int[] refOutParamPositionArray = refOutParamPositionsList.ToArray();
71: for (int i = 0; i < refOutArgs.Length; i++)
72: {
73: allArgs[refOutParamPositionArray[i]] = refOutArgs[i];
74: }
75: }
76: }
WCF的服務代理對象是通過ChannelFactory<TChannel>創建的,我們來創建如下一個與之對應的ServiceProxyFactory<TChannel>類,泛型參數依然表示契約接口類型。CreateChannel方法中通過表示服務地址的Uri,契約接口類型和默認消息版本創建上述的真實代理ServiceChannelProxy<TChannel>對象,並返回其透明代理作為進行服務調用的代理對象。
1: public class ServiceProxyFactory<TChannel>
2: {
3: public Uri Address { get; private set; }
4:
5: public ServiceProxyFactory(Uri address)
6: {
7: this.Address = address;
8: }
9: public TChannel CreateChannel()
10: {
11: MessageEncoderFactory encoderFactory = ComponentBuilder.GetMessageEncoderFactory(MessageVersion.Default, Encoding.UTF8);
12: ServiceChannelProxy<TChannel> proxy = new ServiceChannelProxy<TChannel>(this.Address, MessageVersion.Default, encoderFactory);
13: ContractDescription contract = ContractDescription.GetContract(typeof(TChannel));
14: foreach (OperationDescription operation in contract.Operations)
15: {
16: IClientMessageFormatter messageFormatter = (IClientMessageFormatter)ComponentBuilder.GetFormatter(operation, true);
17: proxy.MessageFormatters.Add(operation.Name, messageFormatter);
18: }
19: return (TChannel)proxy.GetTransparentProxy();
20: }
21: }
現在我們創建一個服務寄宿在我們自定義的迷你版本的WCF中。依然采用我們熟悉的計算服務,下麵是分別定義的Service.Interface和Service項目中的契約接口定義和服務類型定義。
1: //契約接口
2: [ServiceContract(Namespace = "https://www.artech.com/")]
3: public interface ICalculator
4: {
5: [OperationContract]
6: double Add(double x, double y);
7: [OperationContract]
8: double Subtract(double x, double y);
9: [OperationContract]
10: double Multiply(double x, double y);
11: [OperationContract]
12: double Divide(double x, double y);
13: }
14:
15: //服務類型
16: public class CalculatorService: ICalculator
17: {
18: public double Add(double x, double y)
19: {
20: return x + y;
21: }
22: public double Subtract(double x, double y)
23: {
24: return x - y;
25: }
26: public double Multiply(double x, double y)
27: {
28: return x * y;
29: }
30: public double Divide(double x, double y)
31: {
32: return x / y;
33: }
34: }
然後我們為Web項目Service中添加一個Global.asax文件,並通過如下的定義讓Web應用啟動的時候注冊寄宿的服務類型CalculatorService和地址(calculatorservice)之間的映射關係。然後在IIS中創建一個Web應用(比如起名為WcfServices)並將物理路徑映射為Service項目的根目錄。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: RouteTable.Routes.Register<CalculatorService>("calculatorservice");
6: }
7: }
由於最終處理服務調用請求的WcfHandler是通過WcfHttpModule進行映射的,所以我們需要將WcfHttpModule類型配置在Service項目的Web.config中。
1: <configuration>
2: <system.webServer>
3: <modules>
4: <add name="WcfHttpModule" type="Artech.WcfServices.Service.WcfHttpModule, Artech.WcfServices.Service"/>
5: </modules>
6: </system.webServer>
7: </configuration>
在客戶端我們隻需要按照如下的方式通過指定正確的調用地址(Web應用地址+在Global.asax文件中添加的路由映射的地址)創建ServiceProxyFactory<TChannel>對象,並用它來創建用於盡心服務調用的代理對象即可。
1: Uri address = new Uri("https://localhost/WcfServices/CalculatorService");
2: ServiceProxyFactory<ICalculator> factory = new ServiceProxyFactory<ICalculator>(address);
3: ICalculator proxy = factory.CreateChannel();
4: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, proxy.Add(1, 2));
5: Console.WriteLine("x - y = {2} when x = {0} and y = {1}", 1, 2, proxy.Subtract(1, 2));
6: Console.WriteLine("x * y = {2} when x = {0} and y = {1}", 1, 2, proxy.Multiply(1, 2));
7: Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 1, 2, proxy.Divide(1, 2));
上麵的代碼執行之後,就像你真正調用WCF服務一樣,同樣可以得到如下的運算結果。
1: x + y = 3 when x = 1 and y = 2
2: x - y = -1 when x = 1 and y = 2
3: x * y = 2 when x = 1 and y = 2
4: x / y = 0.5 when x = 1 and y = 2
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 14:34:16