真實世界:使用WCF擴展記錄服務調用時間


WCF 可擴展性

WCF 提供了許多擴展點供開發人員自定義運行時行為。 WCF 在 Channel Layer 之上還提供了一個高級運行時,主要是針對應用程序開發人員。在 WCF 文檔中,它常被稱為服務模型層(Service Model Layer)。該高級運行時主要由一個稱作 Dispatcher(在 ServiceHost 的 Context 中)的組件和一個稱作 Proxy(在客戶端的 Context 中)的組件組成。

圖片引自 MSDN Magazine : Extending WCF with Custom Behaviors

每個擴展點都使用接口定義來擴展。

Stage Interceptor Interface Description
Parameter Inspection IParameterInspector Called before and after invocation to inspect and modify parameter values.
Message Formatting IDispatchMessageFormatter IClientFormatter Called to perform serialization and deserialization.
Message Inspection IDispatchMessageInspector IClientMessageInspector Called before send or after receive to inspect and replace message contents.
Operation Selection IDispatchOperationSelector IClientOperationSelector Called to select the operation to invoke for the given message.
Operation Invoker IOperationInvoker Called to invoke the operation.

Behavior 是一種特殊類型的類,它在 ServiceHost/ChannelFactory 初始化過程中擴展運行時行為。WCF 有四種類型的行為:服務行為、終結點行為、契約行為和操作行為。

Scope Interface Potential Impact
    Service Endpoint Contract Operation
Service IServiceBehavior
Endpoint IEndpointBehavior  
Contract IContractBehavior    
Operation IOperationBehavior      

每種行為類型也是通過不同的接口定義來擴展,它們都共用一組相同的方法。一個例外是,IServiceBehavior 沒有 ApplyClientBehavior 方法,因為服務行為不能用於客戶端。

Method Description
Validate Called just before the runtime is built—allows you to perform custom validation on the service description.
AddBindingParameters Called in the first step of building the runtime, before the underlying channel is constructed, allows you to add parameters to influence the underlying channel stack.
ApplyClientBehavior Allows behavior to inject proxy (client) extensions. Note that this method is not present on IServiceBehavior.
ApplyDispatchBehavior Allows behavior to inject dispatcher extensions.

WCF擴展點

(圖片引自 lovecindywang = lovecherry 博客)

案例:使用WCF擴展記錄服務調用時間

服務定義:

1   [ServiceContract]
2   public interface ICalculatorService
3   {
4     [OperationContract]
5     int Add(int a, int b);
6   }

服務實現:

1   public class CalculatorService : ICalculatorService
2   {
3     public int Add(int a, int b)
4     {
5       return a + b;
6     }
7   }

配置文件:

 1   <system.serviceModel>
 2     <services>
 3       <service name="WcfExtensibilityTestServiceConsole.CalculatorService">
 4         <endpoint address="" binding="netTcpBinding" bindingConfiguration="" 
 5                   contract="WcfExtensibilityTestServiceConsole.ICalculatorService" 
 6                   name="myCalculatorServiceEndpoint"/>
 7         <endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange"/>
 8         <host>
 9           <baseAddresses>
10             <add baseAddress="net.tcp://localhost:12345/CalculatorService"/>
11           </baseAddresses>
12         </host>
13       </service>
14     </services>
15     <behaviors>
16       <serviceBehaviors>
17         <behavior>
18           <serviceMetadata/>
19           <serviceDebug includeExceptionDetailInFaults="true"/>
20         </behavior>
21       </serviceBehaviors>
22     </behaviors>
23   </system.serviceModel>

問題描述

現在需要記錄每次服務調用執行所需的時間,比如可以將測量的方法執行時間傳遞給 PerformanceCounter 用於性能計數,或者直接寫入到日志中等。

方式一:直接在方法內測量

 1   public class CalculatorService : ICalculatorService
 2   {
 3     public int Add(int a, int b)
 4     {
 5       Stopwatch watch = Stopwatch.StartNew();
 6 
 7       int result = a + b;
 8       Thread.Sleep(TimeSpan.FromMilliseconds(123)); // waste time here
 9 
10       Debug.WriteLine(string.Format("Method [{0}] execution cost [{1}] milliseconds.",
11         "Add", watch.ElapsedMilliseconds));
12 
13       return result;
14     }
15   }

這種方法淺顯易懂,但如果服務所提供的功能過多,會導致大量冗余代碼。

方式二:形成測量類簡化代碼

 1   public class Measure : IDisposable
 2   {
 3     private Stopwatch _watch = null;
 4 
 5     protected Measure(string methodName)
 6     {
 7       MethodName = methodName;
 8       _watch = new Stopwatch();
 9     }
10 
11     public string MethodName { get; private set; }
12 
13     public void Start()
14     {
15       _watch.Start();
16     }
17 
18     public void Stop()
19     {
20       _watch.Stop();
21 
22       Debug.WriteLine(string.Format("Measure method [{0}] execution cost [{1}] milliseconds.",
23         MethodName, _watch.ElapsedMilliseconds));
24     }
25 
26     public static Measure StartNew(string methodName)
27     {
28       Measure m = new Measure(methodName);
29       m.Start();
30       return m;
31     }
32 
33     public void Dispose()
34     {
35       Stop();
36       _watch = null;
37     }
38   }
View Code
 1   public class CalculatorService : ICalculatorService
 2   {
 3     public int Add(int a, int b)
 4     {
 5       using (var measure = Measure.StartNew("Add"))
 6       {
 7         Thread.Sleep(TimeSpan.FromMilliseconds(123)); // waste time here
 8         return a + b;
 9       }
10     }
11   }

此種方式簡化了代碼,但仍然需要在各自方法內實現,並需提供方法名作為參數。

使用 Message Inspection 來解決問題

我們定義類 IncomingMessageLoggerInspector 來實現 IDispatchMessageInspector 接口。

 1     #region IDispatchMessageInspector Members
 2 
 3     public object AfterReceiveRequest(
 4       ref Message request, 
 5       IClientChannel channel, 
 6       InstanceContext instanceContext)
 7     {
 8       var context = OperationContext.Current;
 9       if (context == null) return null;
10 
11       var operationName = ParseOperationName(context.IncomingMessageHeaders.Action);
12 
13       return MarkStartOfOperation(
14         context.EndpointDispatcher.ContractName, operationName, 
15         context.SessionId);
16     }
17 
18     public void BeforeSendReply(ref Message reply, object correlationState)
19     {
20       var context = OperationContext.Current;
21       if (context == null) return;
22 
23       var operationName = ParseOperationName(context.IncomingMessageHeaders.Action);
24 
25       MarkEndOfOperation(
26         context.EndpointDispatcher.ContractName, operationName, 
27         context.SessionId, correlationState);
28     }
29 
30     #endregion

通過服務的當前上下文實例,我們可以獲取到服務被調用的契約名稱 ContractName,並且可以在 IncomingMessageHeaders 總解析出被調用的 OperationName。

我們在方法 MarkStartOfOperation 中啟動 Stopwatch 開始測量執行時間,在方法執行完畢后服務模型會調用 BeforeSendReply 並將 Stopwatch 實例引用傳遞至 correlationState,此時我們可以在方法 MarkEndOfOperation 中解決時間測量,並打印日志。

 1     #region Private Methods
 2 
 3     private static string ParseOperationName(string action)
 4     {
 5       if (string.IsNullOrEmpty(action)) return action;
 6 
 7       string actionName = action;
 8 
 9       int index = action.LastIndexOf('/');
10       if (index >= 0)
11       {
12         actionName = action.Substring(index + 1);
13       }
14 
15       return actionName;
16     }
17 
18     private static object MarkStartOfOperation(
19       string inspectorType, string operationName, string sessionId)
20     {
21       var message = string.Format(CultureInfo.InvariantCulture,
22         "Operation [{0}] was called at [{1}] on [{2}] in thread [{3}].",
23         operationName, inspectorType,
24         DateTime.Now.ToString(@"yyyy-MM-dd HH:mm:ss.ffffff", CultureInfo.InvariantCulture),
25         Thread.CurrentThread.ManagedThreadId);
26 
27       Debug.WriteLine(message);
28 
29       return Stopwatch.StartNew();
30     }
31 
32     private static void MarkEndOfOperation(
33       string inspectorType, string operationName, 
34       string sessionId, object correlationState)
35     {
36       var watch = (Stopwatch)correlationState;
37       watch.Stop();
38 
39       var message = string.Format(CultureInfo.InvariantCulture,
40         "Operation [{0}] returned after [{1}] milliseconds at [{2}] on [{3}] in thread [{4}].",
41         operationName, watch.ElapsedMilliseconds, inspectorType,
42         DateTime.Now.ToString(@"yyyy-MM-dd HH:mm:ss.ffffff", CultureInfo.InvariantCulture),
43         Thread.CurrentThread.ManagedThreadId);
44 
45       Debug.WriteLine(message);
46     }
47 
48     #endregion

此時,我們需要定義 EndpointBehavior 來講 Inspector 設置到 DispatchRuntime 中。

 1   public class IncomingMessageLoggerEndpointBehavior : IEndpointBehavior
 2   {
 3     #region IEndpointBehavior Members
 4 
 5     public void AddBindingParameters(
 6       ServiceEndpoint endpoint, 
 7       BindingParameterCollection bindingParameters)
 8     {
 9     }
10 
11     public void ApplyClientBehavior(
12       ServiceEndpoint endpoint, 
13       ClientRuntime clientRuntime)
14     {
15     }
16 
17     public void ApplyDispatchBehavior(
18       ServiceEndpoint endpoint, 
19       EndpointDispatcher endpointDispatcher)
20     {
21       if (endpointDispatcher != null)
22       {
23         endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
24           new IncomingMessageLoggerInspector());
25       }
26     }
27 
28     public void Validate(ServiceEndpoint endpoint)
29     {
30     }
31 
32     #endregion
33   }

然后,我們在 ServiceHost 實例化后,未Open前,將 IncomingMessageLoggerEndpointBehavior 添加至 Endpoint 的 Behaviors 中。

 1   class Program
 2   {
 3     static void Main(string[] args)
 4     {
 5       ServiceHost host = new ServiceHost(typeof(CalculatorService));
 6 
 7       foreach (var endpoint in host.Description.Endpoints)
 8       {
 9         endpoint.Behaviors.Add(new IncomingMessageLoggerEndpointBehavior());
10       }
11 
12       host.Opened += new EventHandler(delegate(object obj, EventArgs e)
13       {
14         Debug.WriteLine(typeof(CalculatorService).Name + " 服務已經啟動!");
15       });
16 
17       host.Open();
18 
19       Console.ReadKey();
20     }
21   }

使用 WcfTestClient.exe 調用服務,執行 2 + 3 查看結果,

在 Debug 輸出中可以看到,

使用配置文件定制

使用配置文件定制擴展的優點就是可以按需添加和刪除擴展,而無需改動代碼。比如當發現系統有性能問題時,添加該擴展來查看具體哪個方法執行速度慢。

需要定義類來實現 BehaviorExtensionElement 抽象類。

 1   public class IncomingMessageLoggerEndpointBehaviorExtension : BehaviorExtensionElement
 2   {
 3     public override Type BehaviorType
 4     {
 5       get { return typeof(IncomingMessageLoggerEndpointBehavior); }
 6     }
 7 
 8     protected override object CreateBehavior()
 9     {
10       return new IncomingMessageLoggerEndpointBehavior();
11     }
12   }

在配置文件中添加擴展項,

1     <extensions>
2       <behaviorExtensions>
3         <add name="incomingMessageLogger" 
4              type="WcfExtensibilityTestServiceConsole.Extensions.IncomingMessageLoggerEndpointBehaviorExtension, WcfExtensibilityTestServiceConsole"/>
5       </behaviorExtensions>
6     </extensions>

在終結點行為中添加該擴展定義,

 1     <behaviors>
 2       <serviceBehaviors>
 3         <behavior>
 4           <serviceMetadata/>
 5           <serviceDebug includeExceptionDetailInFaults="true"/>
 6         </behavior>
 7       </serviceBehaviors>
 8       <endpointBehaviors>
 9         <behavior>
10           <incomingMessageLogger/>
11         </behavior>
12       </endpointBehaviors>
13     </behaviors>

使用 ServiceBehavior 擴展

同理,如果服務實現了多個 Endpoint,則想在所有 Endpoint 上添加該擴展,除了可以逐個添加或者使用 behaviorConfiguration 來配置。

另一個方法是可以借助 IServiceBehavior 的擴展實現。

 1   public class IncomingMessageLoggerServiceBehavior : IServiceBehavior
 2   {
 3     #region IServiceBehavior Members
 4 
 5     public void AddBindingParameters(
 6       ServiceDescription serviceDescription, 
 7       ServiceHostBase serviceHostBase, 
 8       Collection<ServiceEndpoint> endpoints, 
 9       BindingParameterCollection bindingParameters)
10     {
11     }
12 
13     public void ApplyDispatchBehavior(
14       ServiceDescription serviceDescription, 
15       ServiceHostBase serviceHostBase)
16     {
17       if (serviceHostBase != null)
18       {
19         foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
20         {
21           foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
22           {
23             endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
24               new IncomingMessageLoggerInspector());
25           }
26         }
27       }
28     }
29 
30     public void Validate(
31       ServiceDescription serviceDescription, 
32       ServiceHostBase serviceHostBase)
33     {
34     }
35 
36     #endregion
37   }

原理相同,僅是遍歷通道分配器中所有的終結點,逐一添加 Inspector。

同理,如果需要在配置文件中使用,也需要實現一個 BehaviorExtensionElement 類。

 1   public class IncomingMessageLoggerServiceBehaviorExtension : BehaviorExtensionElement
 2   {
 3     public override Type BehaviorType
 4     {
 5       get { return typeof(IncomingMessageLoggerServiceBehavior); }
 6     }
 7 
 8     protected override object CreateBehavior()
 9     {
10       return new IncomingMessageLoggerServiceBehavior();
11     }
12   }

此時的配置文件描述如下:

 1     <behaviors>
 2       <serviceBehaviors>
 3         <behavior>
 4           <serviceMetadata/>
 5           <serviceDebug includeExceptionDetailInFaults="true"/>
 6           <incomingMessageLogger/>
 7         </behavior>
 8       </serviceBehaviors>
 9     </behaviors>
10     <extensions>
11       <behaviorExtensions>
12         <add name="incomingMessageLogger" 
13              type="WcfExtensibilityTestServiceConsole.Extensions.IncomingMessageLoggerServiceBehaviorExtension, WcfExtensibilityTestServiceConsole"/>
14       </behaviorExtensions>
15     </extensions>

參考資料

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM