第1章 WCF基礎
本章主要介紹WCF的基本概念、構建模塊以及WCF體系架構,以指導讀者構建一個簡單的WCF服務。從本章的內容中,我們可以了解到WCF的基本術語,包括地址(Address)、綁定(Binding)、契約(Contract)和終結點(Endpoint);了解如何托管服務,如何編寫客戶端代碼;了解WCF的相關主題,諸如進程內托管(In-Proc Hosting)以及可靠性的實現。即使你已經熟知WCF的基本概念,仍然建議你快速瀏覽本章的內容,它不僅能夠鞏固你的已有知識,而且本章介紹的一些輔助類與技術術語也將有助於你閱讀全書。
什么是WCF
Windows通信基礎(Windows Communication Foundation,WCF)是基於Windows平台下開發和部署服務的軟件開發包(Software Development Kit,SDK)。WCF為服務提供了運行時環境(Runtime Environment),使得開發者能夠將CLR類型公開為服務,又能夠以CLR類型的方式使用服務。理論上講,創建服務並不一定需要WCF,但實際上,使用WCF卻可以使得創建服務的任務事半功倍。WCF是微軟對一系列產業標准定義的實現,包括服務交互、類型轉換、封送(Marshaling)以及各種協議的管理。正因為如此,WCF才能夠提供服務之間的互操作性。WCF還為開發者提供了大多數應用程序都需要的基礎功能模塊,提高了開發者的效率。WCF的第一個版本(屬於.NET3.0的一部分)為服務開發提供了許多有用的功能,包括托管(Hosting)、服務實例管理(Service InstanceManagement)、異步調用、可靠性、事務管理、離線隊列調用(Disconnected Queued Call)以及安全性。WCF的第二個版本(屬於.NET3.5的一部分)提供了附加工具,並在原有的基礎上進行了擴展,增加了額外的通信選項。WCF的第三個版本(屬於.NET4.0的一部分)包含了配置變化、一些擴展和新的特性,如服務發現及路由器。雖然與.NET4.0沒有直接關系,但是WCF也擴展了對於Windows Azure Platform App Fabric Service Bus的支持。
同時,WCF還提供了設計優雅的可擴展模型,使開發人員能夠豐富它的基礎功能。事實上,WCF自身的實現正是利用了這樣一種可擴展模型。本書的其余章節會專注於介紹這諸多方面的內容與特征。WCF的大部分功能都包含在一個單獨的程序集System.ServiceModel.dll中,命名空間為System.ServiceModel。WCF是.NET4.0的一部分,因此它只能運行在支持它的操作系統上。Windows XP SP2、Windows Server 2003 SP1、Windows Vista(客戶端和服務器)Windows Server 2008和Windows 7等系統以及更新的版本。
服務
服務(Services)是公開的一組功能的集合。從軟件設計的角度考慮,軟件設計思想經歷了從函數發展到對象,從對象發展到組件,再從組件發展到服務的幾次變遷。在這樣一個漫長的發展旅程中,最后發展到服務的一步可以說是最具革新意義的一次飛躍。面向服務(Service-Orientation,SO)是一組原則的抽象,是創建面向服務應用程序的最佳實踐。如果你不熟悉面向服務的原則,可以參見附錄A,它介紹了使用面向服務的概況與目的。本書假定你對這些原則已經了然於胸。一個面向服務應用程序(SOA)將眾多服務聚集到單個邏輯的應用程序中,這就類似於面向組件的應用程序聚合組件,或者面向對象的應用程序聚合對象,如圖1-1所示。
服務可以是本地的,也可以是遠程的,可以由多個參與方使用任意技術進行開發。服務與版本無關,甚至可以在不同的時區同時執行。服務內部包含了諸如語言、技術、平台、版本與框架等諸多概念,而服務之間的交互,則只允許指定的通信模式。服務的客戶端只是使用服務功能的一方。 理論上講, 客戶端可以是任意的Windows窗體類、 ASP.NET頁面或其他服務。客戶端與服務通過消息的發送與接收進行交互。消息可以直接在客戶端與服務之間進行傳遞,也可以通過中間方進行傳遞。 WCF 中的消息通常為 SOAP 消息。注意 WCF 的消息與傳輸協議無關,這與 Web 服務不同。因此, WCF 服務可以在不同的協議之間傳輸,而不僅限於 HTTP。 WCF 客戶端可以與非 WCF 服務完成互操作,而 WCF 服務也可以與非 WCF 客戶端交互。不過,如果需要同時開發客戶端與服務,則創建的應用程序兩端都要求支持 WCF,這樣才能利用 WCF 的特定優勢。因為服務的創建對於外界而言是不透明的,所以 WCF 服務通常通過公開元數據( Metadata)的方式描述可用的功能以及服務可能采用的通信方式。元數據的發布可以預先定義,它與具體的技術無關( Technology-Neutral),例如采用基於HTTP-GET方式的WSDL, 或者符合元數據交換的行業標准。一個非WCF客戶端可以將元數據作為本地類型導入到本地環境中。相似的, WCF 客戶端也可以導入非 WCF 服務的元數據,然后以本地 CLR 類與接口的方式進行調用。
服務的執行邊界
WCF不允許客戶端直接與服務交互,即使它調用的是本地機器內存中的服務。相反,客戶端總是使用代理(Proxy)將調用轉發給服務。代理公開的操作與服務相同,同時還增加了一些管理代理的方法。
WCF 允許客戶端跨越執行邊界與服務通信。在同一台機器中(參見圖 1-2),客戶端可以調用同一個應用程序域中的服務,也可以在同一進程中跨應用程序域調用,甚至跨進程調用。
圖 1-3則展示了跨機器邊界的通信方式,客戶端可以跨越Intranet或 Internet的邊界與服務交互。
WCF 與位置透明度
過去,諸如 DCOM或 .NET Remoting等分布式計算技術,不管對象是本地還是遠程,都期望為客戶端提供相同的編程模型。本地調用時,客戶端使用直接引用;處理遠程對象時,則使用代理。因為位置的不同而采用兩種不同的編程模型會導致一個問題,就是遠程調用遠比本地調用復雜。復雜度體現在生命周期管理、可靠性、狀態管理、可伸縮性( scalability)以及安全性等諸多方面。由於遠程對象並不具備本地對象的特征,而編程模型卻力圖讓它成為本地對象,反而使得遠程編程模型過於復雜。 WCF同樣要求客戶端保持一致的編程模型,而不用考慮服務的位置。但它的實現途徑卻大相徑庭:即使對象是本地的, WCF仍然使用遠程編程模型的實例化方式,並使用代理。 由於所有的交互操作都經由代理完成,要求相同的配置與托管方式,因而對於本地和遠程方式而言, WCF都只需要維持相同的編程模型。這就使得開發者不會因為服務位置的改變影響客戶端,同時還大大地簡化了應用程序的編程模型。
地址
WCF的每一個服務都具有一個唯一的地址( Addresses)。地址包含兩個重要元素:服務位置與傳輸協議( Transport Protocol),或者是用於服務通信的傳輸樣式( Transport Schema)。服務位置包括目標機器名、站點或網絡、通信端口、管道或隊列,以及一個可選的特定路徑或者 URI。 URI 即統一資源標識( Universal Resource Identifier),它
可以是任意的唯一標識的字符串,例如服務名稱或 GUID。
WCF支持下列傳輸樣式:
- HTTP/HTTPS
- TCP
- Peer network(對等網)
- IPC(基於命名管道的內部進程通信)
- MSMQ
- Service bus
地址通常采用如下格式:
[基地址]/[可選的 URI]
基地址( Base Address)通常的格式如下:
[傳輸協議]://[機器名或域名][:可選端口]
下面是一些地址的示例:
http://localhost:8001
http://localhost:8001/MyService
net.tcp://localhost:8002/MyService
net.pipe://localhost/MyPipe
net.msmq://localhost/private/MyService
net.msmq://localhost/MyService
可以將地址 http://localhost:8001讀作:“采用 HTTP 協議訪問 localhost 機器,並在 8001 端口等待用戶的調用。”
如果 URI 為 http://localhost:8001/MyService,則讀作:“采用 HTTP 協議訪問localhost 機器, MyService 服務在 8001 端口處等待用戶的調用。”
TCP 地址
TCP 地址采用 net.tcp 協議進行傳輸,通常它還包括端口號,例如:
net.tcp://localhost:8002/MyService
如果沒有指定端口號,則 TCP 地址的默認端口號為 808:
net.tcp://localhost/MyService
兩個 TCP 地址(來自於相同的宿主,具體內容將在本章后面介紹)可以共享一個端口:
net.tcp://localhost:8002/MyService
net.tcp://localhost:8002/MyOtherService
本書廣泛地使用了基於 TCP 協議的地址。注意: 我們可以將不同宿主的 TCP 地址配置為共享一個端口。
HTTP 地址
HTTP 地址使用 http 協議進行傳輸,也可以利用 https 進行安全傳輸。 HTTP 地址通常會被用作對外的基於 Internet 的服務,並為其指定端口號,例如:
http://localhost:8001
如果沒有指定端口號,則默認為 80。與 TCP 地址相似,兩個相同宿主的 HTTP地址可以共享一個端口,甚至相同的機器。
本書廣泛地使用了基於 HTTP 協議的地址。
IPC 地址
IPC地址使用net.pipe進行傳輸,這意味着它將使用Windows的命名管道機制。在WCF中,使用命名管道的服務只能接收來自同一台機器的調用。因此,在使用時必須指定明確的本地機器名或者直接命名為 localhost,為管道名提供一個唯一的標識字符串:
net.pipe://localhost/MyPipe
每台機器只能打開一個命名管道,因此,兩個命名管道地址在同一台機器上不能共享一個管道名。
本書廣泛地使用了基於 IPC 的地址。
MSMQ 地址
MSMQ 地址使用 net.msmq 進行傳輸,即使用了微軟消息隊列( Microsoft Message Queue, MSMQ)機制。使用時必須為 MSMQ 地址指定隊列名。如果是處理私有隊列,
則必須指定隊列類型,但對於公有隊列而言,隊列類型可以省略:
net.msmq://localhost/private/MyService
net.msmq://localhost/MyService
對等網地址
對等網地址( Peer Network Address) 使用 net.p2p進行傳輸,它使用了 Windows 的對等網傳輸機制。如果沒有使用解析器( Resolver),我們就必須為對等網地址指定對等網名、唯一的路徑以及端口。對等網的使用與配置超出了本書范圍,但在本書的后續章節中會簡略地介紹對等網。
契約
WCF 的所有服務都會公開為契約( Contract)。契約與平台無關,是描述服務功能的標准方式。 WCF 定義了四種類型的契約。
服務契約( Service Contract)
服務契約描述了客戶端能夠執行的服務操作。
數據契約( Data Contract)
數據契約定義了與服務交互的數據類型。 WCF 為內建類型如 int 和 string 隱式地定義了契約;我們也可以非常便捷地將定制類型定義為數據契約。
錯誤契約( Fault Contract)
錯誤契約定義了服務拋出的錯誤,以及服務處理錯誤和傳遞錯誤到客戶端的方式。
消息契約( Message Contract)
消息契約允許服務直接與消息交互。消息契約可以是類型化的,也可以是非類型化的。如果系統要求互操作性,或者遵循已有消息格式,那么消息契約會非常有用。除非要利用消息契約的靈活性、強大的功能及可擴展性,否則應該避免使用它,因為這往往適得其反,增加開發的復雜程度。在大多數情況下,使用消息契約意味着要自定義應用程序的上下文,這樣就可以使用自定義消息來實現。
服務契約
ServiceContractAttribute 的定義如下:
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class,Inherited = false)] public sealed class ServiceContractAttribute : Attribute { public string Name { get; set; } public string Namespace { get; set; } // 更多成員 }
這個特性允許開發者定義一個服務契約。我們可以將該特性應用到接口或者類類型上,如例 1-1 所示。
例 1-1:定義和實現服務契約
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public string Name { get; set; } // 更多成員 } [ServiceContract] interface IMyContract { [OperationContract] string MyMethod(string text); //不會成為契約的一部分 string MyOtherMethod(string text); } class MyService : IMyContract { public string MyMethod(string text) { return "Hello " + text; } public string MyOtherMethod(string text) { return "Cannot call this method over WCF"; } }
ServiceContract特性可以將一個 CLR 接口(或者通過推斷獲得的接口,后面將詳細介紹)映射為與技術無關的服務契約。 ServiceContract特性公開了 CLR 接口(或者類)作為 WCF 契約。 WCF 契約與類型的訪問限定無關,因為類型的訪問限定屬於 CLR 的概念。即使將 ServiceContract特性應用在內部( Internal)接口上,該接口同樣會公開為公有服務契約,以便於跨越服務邊界實現服務的調用。如果接口沒有標記 ServiceContract 特性, WCF 客戶端則無法訪問它(即使接口是公有的)。這一特點遵循了面向服務的一個原則,即明確的服務邊界。為滿足這一原則,所有契約必須明確要求:只有接口(或者類)可以被標記為 ServiceContract特性,從而被定義為WCF 服務,其他類型都不允許。即使應用了ServiceContract特性,類型的所有成員也不一定就是契約中的一部分。我們必須使用OperationContractAttribute特性顯式地標明哪些方法需要暴露為WCF契約中的一部分。
WCF只允許將OperationContract特性應用到方法上,而不允許應用到同樣屬於CLR概念的屬性、索引器和事件上。 WCF 只能識別作為邏輯功能的操作( Operation)。通過應用 OperationContract特性,可以將契約方法暴露為邏輯操作,使其成為服務契約的一部分。接口(或類)中的其他方法如果沒有應用 OperationContract 特性,則與契約無關。這有利於確保明確的服務邊界,為操作自身維護一個明確參與(Opt-In)的模型。此外,契約操作不能使用引用對象作為參數,只允許使用基本類型或數據契約。
應用 ServiceContract 特性
WCF 允許將 ServiceContract 特性應用到接口或類上。當接口應用了 ServiceContract特性后,需要定義類實現該接口。總的來講,我們可以使用 C# 或 VB 去實現
接口,服務類的代碼無需修改,自然而然成為一個 WCF 服務:
[ServiceContract] interface IMyContract { [OperationContract] string MyMethod(); } class MyService : IMyContract { public string MyMethod() { return "Hello WCF"; } }
我們可以隱式或顯式實現接口:
class MyService : IMyContract { string IMyContract.MyMethod() { return "Hello WCF"; } }
一個單獨的類通過繼承和實現多個標記了 ServiceContract特性的接口,可以支持多個契約。
[ServiceContract] interface IMyContract { [OperationContract] string MyMethod(); } [ServiceContract] interface IMyOtherContract { [OperationContract] void MyOtherMethod(); } class MyService : IMyContract, IMyOtherContract { public string MyMethod() {...} public void MyOtherMethod() {...} }
然而,服務類還有一些實現上的約束。我們要避免使用帶參構造函數,因為 WCF 只能使用默認構造函數。同樣,雖然類可以使用內部( internal)的屬性、索引器以及靜態成員,但 WCF 客戶端卻無法訪問它們。
WCF允許我們直接將ServiceContract特性應用到服務類上,而不需要首先定義一個單獨的契約:
//避 免 [ServiceContract] class MyService { [OperationContract] string MyMethod() { return "Hello WCF"; } }
通過服務類的定義, WCF 能夠推斷出契約的定義。至於 OperationContract特性,則可以應用到類的任何一個方法上,不管它是私有方法,還是公有方法。
警告: 應盡量避免將ServiceContract特性直接應用到服務類上,而應該定義一個單獨的契約,這有利於在不同場景下使用契約。
名稱與命名空間
可以為契約定義命名空間。契約的命名空間具有與 .NET 編程相同的目的:確定契約的類型范圍,以降低類型的沖突幾率。 可以使用ServiceContract類型的 Namespace屬性設置命名空間:
[ServiceContract(Namespace = "MyNamespace")] interface IMyContract {...}
若非特別指定,契約的默認命名空間為 http://tempuri.org。對外服務的命名空間通常使用公司的 URL;至於企業網( Intranet)內部服務的命名空間,則可以定義有意義的唯一名稱,例如 MyApplication。
在默認情況下,契約公開的名稱就是接口名。但是也可以使用 ServiceContract特性的 Name屬性為契約定義別名,從而在客戶端的元數據( Metadata)中公開不同的名稱:
[ServiceContract(Name = "IMyContract")] interface IMyOtherContract {...}
相似的,操作公開的名稱默認為方法名,但我們同樣可以使用 OperationContract特
性的 Name 屬性設置別名,從而公開不同的操作名:
[ServiceContract] interface IMyContract { [OperationContract(Name = "SomeOperation")] void MyMethod(string text); }
托管
WCF服務類不能憑空存在。每個 WCF服務都必須托管( Hosting)在 Windows 進程中,該進程被稱為宿主進程( Host Process)。單個宿主進程可以托管多個服務,而相同的服務類型也能夠托管在多個宿主進程中。 WCF 沒有要求宿主進程是否同時又是客戶端進程。顯然,一個獨立的進程有利於錯誤與安全的隔離。誰提供進程或是提供何種類型的進程並不重要。宿主可以由 IIS 提供,也可以由 Windows Vista 的 Windows 激活服務( Windows Activation Service, WAS)提供,或者開發者直接將它作為應用程序的一部分。
注意: 一種特殊的托管方式稱為進程內托管( In-Process Hosting),簡稱 in-proc。服務與客戶端駐留在相同的進程中。通過定義,開發者能夠提供進程內托管。
IIS 托管
在微軟的 Internet 信息服務器( Internet Information Server, IIS)中托管服務,主要的優勢是宿主進程可以在客戶端提交第一次請求的時候自動啟動,還可以借助 IIS 管理宿主進程的生命周期。 IIS 托管的主要缺點在於只能使用 HTTP 協議。如果是 IIS 5,還要受端口限制,要求所有服務必須使用相同的端口號。
在 IIS 中托管服務與經典的 ASMX Web 服務托管相似,需要在 IIS 下創建虛擬目錄,並提供一個 .svc 文件。 .svc 文件的功能與 .asmx 文件相似,主要用於識別隱藏在文件和類后面的服務代碼。例 1-2 展示了 .svc 文件的語法結構。
例 1-2: .svc 文件
<%@ ServiceHost
Language = "C#"
Debug = "true"
CodeBehind = "~/App_Code/MyService.cs"
Service = "MyService"
%>
注意: 我們甚至可以將服務代碼注入到.svc文件中,但這樣的做法並不明智。這與ASMX Web服務的要求相同。
使用 IIS 托管,服務的基地址必需與 .svc 文件的地址保持一致。
使用 Visual Studio 2010
使用 Visual Studio 2010,可以生成 IIS 托管服務的模版文件。選擇 File 菜單的 New Web Site 菜單項,然后從 New Web Site 對話框中選擇 WCF Service。通過這種方式可以讓 Visual Studio 2010 創建一個新的 Web 站點,以及服務代碼和對應的 .svc 文件。之后,我們還可以通過 Add New Item 對話框添加另外的服務。
Web.Config 文件
Web 站點的配置文件( Web.Config)必須列出需要公開為服務的類型。類型使用類型全名,如果服務類型來自於一個沒有被引用的程序集,則還要包括程序集名:
<system.serviceModel> <services> <service name = "MyNamespace.MyService"> ... </service> </services> </system.serviceModel>
我們可以將提供服務類型和地址信息直接應用於serviceHostingEnvironment部分中的web.config程序而不使用.svc文件。事實上,你可以根據自己的喜好定義許多這樣的服務:
<system.serviceModel> <serviceHostingEnvironment> <serviceActivations> <add relativeAddress="MyService.svc" service="MyNamespace.MyService"/> <add relativeAddress="MyOtherService.svc" service="MyOtherService.MyOtherService"/> </serviceActivations> </serviceHostingEnvironment> <services> <service name="MyNamespace.MyService"> ... </service> <service name="MyOtherService"> ... </service> </services> </system.serviceModel>
自托管
所謂自托管( Self-Hosting), 就是由開發者提供和管理宿主進程的生命周期。 自托管方式適用於如下場景:需要確定客戶端與服務之間的進程(或機器)邊界時;使用進程內托管,即服務與客戶端處於相同的進程中時。進程可以是任意的 Windows 進程,例如Windows 窗體應用程序、控制台應用程序或 Windows NT 服務。注意,進程必須在客戶端調用服務之前運行,這意味着通常必須預先啟動進程。但 NT 服務或進程內托管不受此限制。宿主程序的實現只需要簡單的幾行代碼,就能夠實現 IIS 托管的一部分特性。與 IIS 托管相似,托管應用程序的配置文件( App.Config)必須列出所有希望托管和公開的服務類型:
<system.serviceModel> <services> <service name = "MyNamespace.MyService"> ... </service> </services> </system.serviceModel>
此外,宿主進程必須在運行時顯式地注冊服務類型,同時為客戶端的調用打開宿主,因此,我們才要求宿主進程必須在客戶端調用到達之前運行。創建宿主的方法通常是在Main()方法中調用 ServiceHost 類。 ServiceHost 類的定義如例 1-3 所示。
例 1-3: ServiceHost 類
public interface ICommunicationObject { void Open(); void Close(); //更多成員 } public abstract class CommunicationObject : ICommunicationObject { ... } public abstract class ServiceHostBase : CommunicationObject,IDisposable,... { ... } public class ServiceHost : ServiceHostBase
{ public ServiceHost(Type serviceType, params Uri[] baseAddresses) { } //更多成員 }
創建 ServiceHost 對象時,需要為 ServiceHost的構造函數提供服務類型,至於默認的基地址則是可選的。可以將基地址集合設置為空。如果提供了多個基地址,也可以將服務配置為使用不同的基地址。ServiceHost擁有基地址集合可以使得服務能夠接收來自於多個地址和協議的調用,同時只需要使用相對的 URI。注意,每個 SeriviceHost實例都與特定的服務類型相關,如果宿主進程需要運行多個服務類型,則必須創建與之匹配的多個 ServiceHost 實例。在宿主程序中,通過調用 Open()方法,可以允許調用傳入;通過調用 Close()方法終結宿主實例,完成進程中的調用。此時,即使宿主進程還在運行,仍然會拒絕客戶端的調用。而在通常情況下,執行關閉操作會停止宿主進程。例如,在 Windows 窗體應用程序中托管服務:
[ServiceContract]
interface IMyContract
{...}
class MyService : IMyContract
{...}
我們可以編寫如下的托管代碼:
public static void Main() { Uri baseAddress = new Uri("http://localhost:8000/"); ServiceHost host = new ServiceHost(typeof(MyService), baseAddress); host.Open(); // 可以執行用於阻塞的調用: Application.Run(new MyForm()); host.Close(); }
打開宿主時,將裝載 WCF 運行時( WCF runtime),啟動工作線程監控傳入的請求消息。監聽線程將傳入調用消息從I/O完成端口(I/O completion thread pool,默認具有1000個線程)分發到工作線程中。由於引入了工作線程,因此可以在打開宿主之后執行阻塞( blocking)操作。
因為宿主被正常關閉,因而所消耗的時間值是未定的。一般情況下,宿主會阻塞10s以等待Close()方法返回,以及處理在設置的超時值過期后的關閉事宜。在打開宿主之前,你可以通過ServiceHostBase的屬性CloseTimeout設置不同的關閉時間值:
public abstract class ServiceHostBase:... { public TimeSpan Colsetimeout { get; set; } //更多成員 }
例如,可以使用編程方式設置關閉超時時間值為20s:
ServiceHost host = new ServiceHost(...); host.CloseTimeout = TimeSpan.FromSeconds(20); host.Open();
在配置文件中也可以設置關閉時間值:
<system.serviceModel> <services> <service name="MyNamespace.MyService"> <host> <timeouts closeTimeout="00:00:20"/> </host> ... </service> </services> </system.serviceModel>
通過顯式控制宿主的打開與關閉,提供了 IIS 托管難以實現的特征,即能夠創建定制的應用程序控制模塊,管理者可以隨意地打開和關閉宿主,而不用每次停止宿主的運行。
使用 Visual Studio 2010
Visual Studio 2010 允許開發者為任意的應用程序項目添加 WCF 服務,方法是在 Add New Item 對話框中選擇 WCF Service 選項。當然,這種方式添加的服務,對於宿主進程而言屬於進程內托管方式,但進程外的客戶端仍然可以訪問它。
自托管與基地址
啟動服務宿主時,無需提供任何基地址:
public static void Main() { ServiceHost host = new ServiceHost(typeof(MyService)); host.Open(); Application.Run(new MyForm()); host.Close(); }
警告: 但是我們不能向空列表傳遞 null 值,這會導致拋出異常:
serviceHost host; host = new ServiceHost(typeof(MyService),null);
要這些地址沒有使用相同的傳輸樣式 ( Transport Schema),我們也可以注冊多個基地址,並以逗號作為地址之間的分隔符。代碼實現如下所示(注意例 1-3 中 params 限定符的使用):
Uri tcpBaseAddress = new Uri("net.tcp://localhost:8001/"); Uri httpBaseAddress = new Uri("http://localhost:8002/"); ServiceHost host = new ServiceHost(typeof(MyService), tcpBaseAddress,httpBaseAddress);
WCF 也允許開發者在宿主配置文件中列出基地址內容:
<system.serviceModel> <services> <service name = "MyNamespace.MyService"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "http://localhost:8002/"/> </baseAddresses> </host> ... </service> </services> </system.serviceModel>
創建宿主時,無論在配置文件中找到哪一個基地址,宿主都會使用它,同時還要加上以編程方式提供的基地址。需要特別注意,我們必須確保配置的基地址的樣式不能與代碼中的基地址的樣式重疊。
我們甚至可以針對相同的類型注冊多個宿主,只要這些宿主使用了不同的基地址:
Uri baseAddress1 = new Uri("net.tcp://localhost:8001/"); ServiceHost host1 = new ServiceHost(typeof(MyService),baseAddress1); host1.Open(); Uri baseAddress2 = new Uri("net.tcp://localhost:8002/"); ServiceHost host2 = new ServiceHost(typeof(MyService),baseAddress2); host2.Open();
然而,這並不包括第 8 章介紹的使用線程的情況,以這種方式打開多個宿主並無優勢可言。此外, 如果基地址是配置文件提供的,那么就需要使用ServiceHost的構造函數為相同的類型打開多個宿主。
托管的高級特性
ServiceHost實現的ICommunicationObject接口定義了一些高級特性,如例1-4所示。
例 1-4: ICommunicationObject接口
public interface ICommunicationObject { void Open(); void Close(); void Abort(); event EventHandler Closed; event EventHandler Closing; event EventHandler Faulted; event EventHandler Opened; event EventHandler Opening; IAsyncResult BeginClose(AsyncCallback callback, object state); IAsyncResult BeginOpen(AsyncCallback callback, object state); void EndClose(IAsyncResult result); void EndOpen(IAsyncResult result); CommunicationState State { get; } // 更多成員 } public enum CommunicationState { Created, Opening, Opened, Closing, Closed, Faulted }
如果打開或關閉宿主的操作耗時較長,可以采用異步方式調用 BeginOpen()和BeginClose()方法。我們可以訂閱諸如狀態改變或錯誤發生等宿主事件,通過調用State屬性查詢當前的宿主狀態。 ServiceHost類同樣實現了 Abort()方法。該方法提供強行退出功能,能夠及時中斷進程中的所有服務調用,然后關閉宿主。此時,活動的客戶端會獲得一個異常。
ServiceHost<T> 類
ServiceHost<T> 類能夠改進 WCF 提供的 ServiceHost 類,它的定義如例 1-5 所示。
例 1-5: ServiceHost<T> 類
public class ServiceHost<T> : ServiceHost { public ServiceHost() : base(typeof(T)) { } public ServiceHost(params string[] baseAddresses) : base(typeof(T), Convert(baseAddresses)) { } public ServiceHost(params Uri[] baseAddresses) : base(typeof(T), baseAddresses) { } static Uri[] Convert(string[] baseAddresses) { Converter<string, Uri> convert = delegate(string address) { return new Uri(address); }; return Array.ConvertAll(baseAddresses, convert); } }
ServiceHost<T>簡化了構造函數,它不需要傳遞服務類型作為構造函數的參數,還能夠直接處理字符串而不是處理令人生厭的 Uri 值。在本書余下的內容中,對 ServiceHost<T> 進行了擴展,增加了一些特性,提高了它的性能。
WAS 托管
Windows 激活服務( WAS)是一個系統服務,適用於 Windows Vista及更新版本。 WAS 是 IIS 7的一部分,但也可以獨立地安裝與配置。若要使用 WAS托管 WCF服務,必須提供一個.svc文件,這與 IIS 托管一樣。 IIS與 WAS 的主要區別在於 WAS並不局限於使用 HTTP,它支持所有可用的 WCF 傳輸協議、端口與隊列。
WAS 提供了大量基於自托管的強大功能,包括應用程序池、回收機制、空閑時間管理( Idle Time Management)、身份管理( Identity Management)以及隔離( Isolation);宿主進程可以根據情況選擇使用這些功能。若需考慮可擴展性,就應該使用 Windows Server 2008(或更新版本)服務器作為目標機器;如果只有少數客戶端,則可以將Windows Vista或者Windows 7(或更新版本) 客戶機作為服務器。
當然,自托管進程還提供了許多卓越特性,例如進程內宿主、匿名用戶環境的處理,同時還為之前介紹的高級宿主特性提供了便捷地編程訪問方式。
選擇宿主
對於Internet應用程序(調用客戶端來自Internet中的任何地方)可以依據下圖來做決定:
對於Intranet應用(發送調用的客戶端與服務同處於一個Intranet內)可以依據下圖來做決定:
綁定
服務之間的通信方式是多種多樣的,有多種可能的通信模式。包括:同步的請求 / 應答( Request/Reply)消息,或者異步的“即發即棄( Fire-and-Forget)”消息;雙向( Bidirectional)消息;即時消息或隊列消息;以及持久( Durable)隊列或者可變( Volatile)隊列。傳遞消息的傳輸協議包括: HTTP(或 HTTPS)、 TCP、 P2P(對等網)、IPC(命名管道)以及 MSMQ。消息編碼格式包括:保證互操作性的純文本編碼格式;優化性能的二進制編碼格式;提供有效負載的 MTOM(消息傳輸優化機制, Message Transport Optimization Mechanism)編碼格式。消息的安全保障也有多種策略,包括:不實施任何安全策略;只提供傳輸層的安全策略;消息層的隱私保護與安全策略。 當然,WCF 還包括多種對客戶端認證與授權的安全策略。消息傳遞( Message Delivery)可能是不可靠的,也可能是可靠的端對端跨越中間方,然后斷開連接的方式。消息傳遞可能按照發送消息的順序處理,也可能按照接收消息的順序處理。服務可能需要與其他服務或客戶端交互, 這些服務或客戶端或者只支持基本的 Web服務協議, 或者使用了流行的WS-* 協議,例如 WS-Security 或者 WS-Atomic Transaction。服務可能會基於原來的MSMQ 消息與舊的客戶端( Legacy Client)交互,或者限制服務只能與其他的 WCF 服務或客戶端交互。
若要計算所有可能的通信模式與交互方式之間的組合,數量可能達到上千萬。在這些組合選項中,有的可能是互斥的,有的則彼此約束。顯然,客戶端與服務必須合理地組合這些選項,才能保證通信的順暢。對於大多數應用程序而言,管理如此程度的復雜度並無業務價值。然而,一旦因此作出錯誤決定,就會影響系統的效率與質量,造成嚴重的后果。
為了簡化這些選項,使它們易於管理, WCF 引入了綁定( Binding)技術將這些通信特征組合在一起。一個綁定封裝了諸如傳輸協議、消息編碼、通信模式、可靠性、安全性、事務傳播以及互操作性等相關選項的集合,使得它們保持一致。理想狀態下,我們希望將所有繁雜的基礎功能模塊從服務代碼中解放出來,允許服務只需要關注業務邏輯的實現。綁定使得開發者能夠基於不同的基礎功能模塊使用相同的服務邏輯。
在使用WCF提供的綁定時, 可以調整綁定的屬性,也可以從零開始定制自己的綁定。服務在元數據中發布綁定的選項,由於客戶端使用的綁定必須與服務的綁定完全相同,因此客戶端能夠查詢綁定的類型與特定屬性。單個服務能夠支持各個地址上的多個綁定。
標准綁定
WCF 定義了 (不只)9 種標准綁定:
基本綁定( Basic Binding)
由BasicHttpBinding類提供。 基本綁定能夠將WCF服務公開為舊的ASMX Web服務,使得舊的客戶端能夠與新的服務協作。如果客戶端使用了基本綁定,那么新的 WCF 客戶端就能夠與舊的 ASMX 服務協作。
TCP 綁定
由 NetTcpBinding類提供。 TCP 綁定使用 TCP 協議實現在 Intranet中跨機器的通信。 TCP 綁定支持多種特性,包括可靠性、事務性、安全性以及 WCF 之間通信的優化。前提是,它要求客戶端與服務都必須使用 WCF。
對等網綁定
由 NetPeerTcpBinding類提供。它使用對等網進行傳輸。對等網允許客戶端與服務訂閱相同的網格( Grid),實現廣播消息。因為對等網需要網格拓撲( Grid Topology)與網狀計算策略( Mesh Computing Strategies)方面的知識,故而不在本書討論范圍之內。
IPC 綁定
由 NetNamedPipeBinding類提供。它使用命名管道為同一機器的通信進行傳輸。這種綁定方式最安全,因為它不能接收來自機器外部的調用。 IPC綁定支持的特性與 TCP 綁定相似。
Web 服務( WS)綁定
由WSHttpBinding類提供。 WS綁定使用HTTP或HTTPS進行傳輸,為基於Internet的通信提供了諸如可靠性、事務性與安全性等特性。
WS 聯邦綁定( Federated WS Binding)
由 WSFederationHttpBinding類提供。 WS 聯邦綁定是一種特殊的 WS 綁定,提供對聯邦安全( Federated Security)的支持。聯邦安全不在本書討論范圍之內。
WS 雙向綁定( Duplex WS Binding)
由 WSDualHttpBinding 類提供。 WS 雙向綁定與 WS 綁定相似,但它還支持從服務到客戶端的雙向通信,相關內容在第 5 章介紹。
MSMQ 綁定
由 NetMsmqBinding類提供。它使用 MSMQ 進行傳輸,用以提供對斷開的隊列調用的支持。相關內容在第 9 章介紹。
MSMQ 集成綁定( MSMQ Integration Binding)
由 MsmqIntegrationBinding類提供。它實現了 WCF 消息與 MSMQ消息之間的轉換,用以支持與舊的 MSMQ 客戶端之間的互操作。 MSMQ集成綁定不在本書討論范圍之內。
格式與編碼
每種標准綁定使用的傳輸協議與編碼格式都不相同,如表 1-1 所示。
表 1-1:標准綁定的傳輸協議與編碼格式(默認的編碼格式為黑體)
文本編碼格式允許 WCF服務(或客戶端)能夠通過 HTTP 協議與其他服務(或客戶端)通信,而不用考慮它使用的技術。二進制編碼格式通過 TCP 或 IPC 協議通信,它所獲得的最佳性能是以犧牲互操作性為代價的,它只支持 WCF 到 WCF 的通信。
選擇綁定
為服務選擇綁定應該遵循圖 1-4 所示的決策活動圖表。
首先需要確認服務是否需要與非 WCF 的客戶端交互。如果是,同時客戶端又是舊的MSMQ客戶端, 選擇MsmqIntegrationBinding綁定就能夠使得服務通過MSMQ與該客戶端實現互操作。如果服務需要與非 WCF 客戶端交互,並且該客戶端期望調用基本的 Web 服務協議( ASMX Web 服務),那么選擇 BasicHttpBinding 綁定就能夠模擬ASMX Web 服務(即 WSI-Basic Profile)公開 WCF 服務。缺點是我們無法使用大多數最新的 WS-*協議的優勢。但是,如果非 WCF客戶端能夠識別這些標准,就應該選擇其中一種 W S 綁定,例如 WSHttpBinding 、WSFederationBinding 或者WSDualHttpBinding。如果假定客戶端為 WCF客戶端,同時需要支持脫機或斷開狀態下的交互,則可以選擇 NetMsmqBinding使用 MSMQ 傳輸消息。如果客戶端需要聯機通信,但是需要跨機器邊界調用,則應該選擇 NetTcpBinding通過 TCP 協議進行通信。如果相同機器上的客戶端同時又是服務,選擇NetNamePipeBinding使用命名管道可以使性能達到最優化。如果基於額外的標准,例如回調(選擇 WSDualHttpBinding)或者聯邦安全(選擇 WSFederationBinding),則應對選擇的綁定進行微調。
注意: 即使超出了使用的目標場景,大多數綁定工作仍然良好。例如,我們可以使用 TCP 綁定實現相同機器甚至進程內的通信;我們也可以使用基本綁定實現 Intranet 中 WCF 對 WCF 的通信。然而,我們還是應盡量按照圖 1-4 選擇綁定。
使用綁定
每種綁定都提供了多種可配置的屬性。綁定有三種工作模式。如果內建綁定符合開發者的需求,就可以直接使用它們。我們也可以對綁定的某些屬性如事務傳播、可靠性和安全性進行調整與配置,還可以定制自己的綁定。最常見的情況是使用已有的綁定,然后只對綁定的幾個方面進行配置。應用程序開發者幾乎不需要編寫定制綁定,但這卻是框架開發者可能需要做的工作。
終結點
服務與地址、綁定以及契約有關。其中,地址定義了服務的位置,綁定定義了服務通信的方式,契約則定義了服務的內容。為便於記憶,我們可以將這種類似於“三權分立”一般管理服務的方式簡稱為服務的 ABC。 WCF 用終結點表示這樣一種組成關系。終結點就是地址、契約與綁定的混成品(參見圖 1-5)。
每一個終結點都包含了三個元素,而宿主則負責公開終結點。從邏輯上講,終結點相當於服務的接口,就像 CLR 或者 COM 接口一樣。注意,圖 1-5 使用了傳統的“棒棒糖”形式展示了一個終結點的構成。
注意: 從概念上講,不管是 C# 還是 VB,一個接口就相當於一個終結點:地址就是類型虛擬表的內存地址,綁定則是 CLR 的 JIT( Just-In-Time)編譯,而契約則代表接口本身。由於經典的 .NET 編程模式不需要處理地址或綁定,你可能認為它們是理所當然存在的。而 WCF 並未規定地址與綁定,因而必須對它們進行配置。
每個服務至少必須公開一個業務終結點,每個終結點有且只能擁有一個契約。服務上的所有終結點都包含了唯一的地址,而一個單獨的服務則可以公開多個終結點。這些終結點可以使用相同或不同的綁定,公開相同或不同的契約。每個服務提供的不同終結點之間絕對沒有任何關聯。
重要的一點是,服務代碼並沒有包含它的終結點,它們通常放在服務代碼之外。我們可以通過管理方式 ( Administratively)使用配置文件或者通過編程方式 ( Programmatically)配置終結點。
管理方式配置終結點
以管理方式配置一個終結點需要將終結點放到托管進程的配置文件中,如下的服務定義:
namespace MyNamespace { [ServiceContract] interface IMyContract {...} class MyService : IMyContract {...} }
例 1-6 演示了配置文件要求的配置入口。在每個服務類型下列出它的終結點。
例 1-6:管理方式配置終結點
<system.serviceModel> <services> <service name = "MyNamespace.MyService"> <endpoint address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "MyNamespace.IMyContract" /> </service> </services> </system.serviceModel>
當我們指定服務和契約類型時,必須使用類型全名。 在本書的其余例子中,為簡略起見,我省略了類型的命名空間,但在實際應用中,命名空間是必備的。注意,如果終結點已經提供了基地址,則地址的樣式必須與綁定一致,例如 HTTP對應 WSHttpBinding。如果兩者不匹配,就會在裝載服務時導致異常。
例 1-7 的配置文件為一個單獨的服務公開了多個終結點。多個終結點可以配置相同的基地址,前提是 URI 互不不同。
例 1-7:相同服務的多個終結點
<service name = "MyService"> <endpoint address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service>
大多數情況下,我們的首選是管理的配置方式,因為它非常靈活,即使修改了服務的地址、綁定和契約,也不需要重新編譯服務和重新部署服務。
使用基地址
例 1-7 中的每個終結點都提供了自己獨有的基地址。如果我們提供了顯式的基地址,它會重寫宿主提供的所有基地址。我們也可以讓多個終結點使用相同的基地址,只要終結點地址中的 URI 不同:
<service name = "MyService"> <endpoint address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyOtherService/" binding = "netTcpBinding" contract = "IMyContract" /> </service>
反之,如果宿主提供了與傳輸樣式匹配的基地址,則可以省略地址項。此時,終結點地址與該基地址完全相同:
<endpoint binding = "wsHttpBinding" contract = "IMyContract" />
如果宿主沒有提供匹配的基地址,則在裝載服務宿主時會拋出異常。
配置終結點地址時,可以為基地址添加相對 URI:
<endpoint address = "SubAddress" binding = "wsHttpBinding" contract = "IMyContract" />
此時,終結點地址等於它所匹配的基地址加上 URI。當然,前提是宿主必須提供匹配的基地址。
綁定配置
使用配置文件可以為終結點使用的綁定進行定制。為此,需要在 <endpoint>節中添加bindingConfiguration標志,它的值應該與 <bindings> 配置節中定制的綁定名一致。例 1-8 介紹了使用這種技術啟用事務傳播的方法。其中的 transactionFlow 標志會在第 7 章詳細介紹。
例 1-8:服務端綁定的配置
<system.serviceModel> <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService/" bindingConfiguration = "TransactionalTCP" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService/" bindingConfiguration = "TransactionalTCP" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel>
如例 1-8 所示,我們可以在多個終結點中通過指向定制綁定的方式,重用已命名的綁定配置。
編程方式配置終結點
編程方式配置終結點與管理方式配置終結點等效。但它不需要配置文件,而是通過編程調用將終結點添加到 ServiceHost 實例中。這些調用不屬於服務代碼的范圍。
ServiceHost定義了重載版本的 AddServiceEndpoint()方法:
public class ServiceHost : ServiceHostBase { public ServiceEndpoint AddServiceEndpoint(Type implementedContract,Binding binding,string address); // 其他成員 }
傳入 AddServiceEndpoint()方法的地址可以是相對地址,也可以是絕對地址,這與使用配置文件的方式相似。例 1-9 演示了編程配置的方法,它配置的終結點與例 1-7 的終結點相同。
例 1-9:服務端編程配置終結點
ServiceHost host = new ServiceHost(typeof(MyService)); Binding wsBinding = new WSHttpBinding(); Binding tcpBinding = new NetTcpBinding(); host.AddServiceEndpoint(typeof(IMyContract),wsBinding,"http://localhost:8000/MyService"); host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,"net.tcp://localhost:8001/MyService"); host.AddServiceEndpoint(typeof(IMyOtherContract),tcpBinding,"net.tcp://localhost:8002/MyService"); host.Open();
以編程方式添加終結點時, address 參數為 string 類型, contract 參數為 Type 類型,而binding 參數的類型則是 Binding 抽象類的其中一個子類,例如:
public class NetTcpBinding : Binding,... {...}
由於宿主提供了基地址,因此若要使用基地址,可以將空字符串賦給 address 參數,或者只設置 URI 值,此時使用的地址就應該是基地址加上 URI:
Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/"); ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress); Binding tcpBinding = new NetTcpBinding(); // 使用基地址作為地址 host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,""); // 添加相對地址 host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,"MyService"); // 忽略基地址 host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8001/MyService"); host.Open();
使用配置文件進行管理方式的配置,宿主必須提供一個匹配的基地址,否則會引發異常。事實上,編程方式配置與管理方式配置並沒有任何區別。使用配置文件時, WCF會解析文件,然后執行對應的編程調用。
綁定配置
我們可以通過編程方式設置綁定的屬性。例如, 以下代碼就實現了與例1-8相似的功能,啟用事務傳播:
ServiceHost host = new ServiceHost(typeof(MyService)); NetTcpBinding tcpBinding = new NetTcpBinding(); tcpBinding.TransactionFlow = true; host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8000/MyService"); host.Open();
注意,在處理特定的綁定屬性時,通常應該與具體的綁定子類如 NetTcpBinding交互,而不是使用抽象類 Binding。
第1章 WCF基礎(2)
摘自:《WCF服務編程》Juval Louml著 張逸 徐寧 譯
轉載請注明出處: