WCF服务编程 读书笔记——第1章 WCF基础(1)

时间:2021-09-18 12:51:46

第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所示。

WCF服务编程 读书笔记——第1章 WCF基础(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),客户端可以调用同一个应用程序域中的服务,也可以在同一进程中跨应用程序域调用,甚至跨进程调用。

WCF服务编程 读书笔记——第1章 WCF基础(1)

图 1-3则展示了跨机器边界的通信方式,客户端可以跨越Intranet或 Internet的边界与服务交互。

WCF服务编程 读书笔记——第1章 WCF基础(1)

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();
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中的任何地方)可以依据下图来做决定:

WCF服务编程 读书笔记——第1章 WCF基础(1)

对于Intranet应用(发送调用的客户端与服务同处于一个Intranet内)可以依据下图来做决定:

WCF服务编程 读书笔记——第1章 WCF基础(1)

绑定
服务之间的通信方式是多种多样的,有多种可能的通信模式。包括:同步的请求 / 应答( 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服务编程 读书笔记——第1章 WCF基础(1)

文本编码格式允许 WCF服务(或客户端)能够通过 HTTP 协议与其他服务(或客户端)通信,而不用考虑它使用的技术。二进制编码格式通过 TCP 或 IPC 协议通信,它所获得的最佳性能是以牺牲互操作性为代价的,它只支持 WCF 到 WCF 的通信。

选择绑定
为服务选择绑定应该遵循图 1-4 所示的决策活动图表。

WCF服务编程 读书笔记——第1章 WCF基础(1)
首先需要确认服务是否需要与非 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)。
WCF服务编程 读书笔记——第1章 WCF基础(1)
每一个终结点都包含了三个元素,而宿主则负责公开终结点。从逻辑上讲,终结点相当于服务的接口,就像 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著    张逸 徐宁 译

转载请注明出处:

作者:JesseLZJ
出处:http://jesselzj.cnblogs.com