(承接上文,只是不好意思,在文中用到的图片自己没有插入。)
12.终结点
服务与地址、绑定以及契约有关。其中,地址定义了服务的位置,绑定定义了服务通信的方式,契约则定义了服务的内容。为便于记忆,我们可以将这种类似于“三权分立”一般管理服务的方式简称为服务的 ABC。WCF 用终结点表示这样一种组成关系。终结点就是地址、契约与绑定的混成品(参见图 1-5)。
每一个终结点都包含了三个元素,而宿主则负责公开终结点。从逻辑上讲,终结点相当于服务的接口,就像 CLR 或者 COM 接口一样。注意,图 1-5 使用了传统的“棒棒糖”形式展示了一个终结点的构成。
注意:从概念上讲,不管是 C#还是 VB,一个接口就相当于一个终结点:地址就是类型虚拟表的内存地址,绑定则是 CLR 的 JIT(Just-In-Time)编译,而契约则代表接口本身。 由于经典的.NET 编程模式不需要处理地址或绑定,你可能认为它们是理所当然存在的。而WCF 并未规定地址与绑定,因而必须对它们进行配置。
每个服务至少必须公开一个业务终结点,每个终结点有且只能拥有一个契约。服务上的所有终结点都包含了唯一的地址,而一个单独的服务则可以公开多个终结点。这些终结点可以使用相同或不同的绑定,公开相同或不同的契约。每个服务提供的不同终结点之间绝对没有任何关联。
重要的一点是,服务代码并没有包含它的终结点,它们通常放在服务代码之外。我们可以通过管理方式(Administratively)使用配置文件或者通过编程方式(Programmatically)配置终结点。
(1).管理方式配置终结点
以管理方式配置一个终结点需要将终结点放到托管进程的配置文件中。
例 1-6 演示了配置文件要求的配置入口。在每个服务类型下列出它的终结点。此为以管理方式配置终结点。
当我们指定服务和契约类型时,必须使用类型全名。在本书的其余例子中,为简略起见,我省略了类型的命名空间,但在实际应用中,命名空间是必备的。注意,如果终结点已经提供了基地址,则地址的样式必须与绑定一致,例如 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>
大多数情况下,我们的首选是管理的配置方式,因为它非常灵活,即使修改了服务的地址、绑定和契约,也不需要重新编译服务和重新部署服务。
(2).使用基地址
例 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。当然,前提是宿主必须提供匹配的基地址。
(3).绑定配置
使用配置文件可以为终结点使用的绑定进行定制。为此,需要在<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 所示,我们可以在多个终结点中通过指向定制绑定的方式,重用已命名的绑定配置。
(4).编程方式配置终结点
编程方式配置终结点与管理方式配置终结点等效。但它不需要配置文件,而是通过编程调用将终结点添加到 ServiceHost 实例中。这些调用不属于服务代码的范围。ServiceHost 定义了重载版本的 AddServiceEndpoint()方法,传入 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 抽象类的其中一个子类。
由于宿主提供了基地址,因此若要使用基地址,可以将空字符串赋给 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,"MyServi ce");
//忽略基地址
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8001/MyService");
host.Open();
使用配置文件进行管理方式的配置,宿主必须提供一个匹配的基地址,否则会引发异常。事实上,编程方式配置与管理方式配置并没有任何区别。使用配置文件时,WCF 会解析文件,然后执行对应的编程调用。
(5).绑定配置
我们可以通过编程方式设置绑定的属性。例如,以下代码就实现了与例 1-8 相似的功能,启用事务传播:
ServiceHost host = new Servi ceHost(typeof(MyService));
NetTcpBinding tcpBinding = new NetTcpBinding();
tcpBinding.TransactionFlow = true;
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8000/MyService");
host.Open();
注意,在处理特定的绑定属性时,通常应该与具体的绑定子类如 NetTcpBinding 交互,而不是使用抽象类 Binding,正如例 1-9 所示。
13.元数据交换
服务有两种方案可以发布自己的元数据。一种是基于 HTTP-GET 协议提供元数据,另一种则是后面将要讨论的使用专门的终结点的方式。WCF 能够为服务自动提供基于 HTTP-GET的元数据,但需要显式地添加服务行为(Behavior)以支持这一功能。本书后面的章节会介绍行为的相关知识。现在,我们只需要知道行为属于服务的本地特性,例如是否需要基于HTTP-GET 交换元数据,就是一种服务行为。我们可以通过编程方式或管理方式添加行为。
在例 1-10 演示的宿主应用程序的配置文件中,所有引用了定制<behavior>配置节的托管服务都支持基于 HTTP-GET 协议实现元数据交换。为了使用 HTTP-GET,客户端使用的地址需要注册服务的 HTTP 基地址。我们也可以在行为中指定一个外部 URL 以达到同样的目的。
一旦启用了基于 HTTP-GET 的元数据交换,在浏览器中就可以通过 HTTP 基地址(如果存在)进行访问。如果一切正确,就会获得一个确认页面,如图 1-6 所示,告知开发者已经成功托管了服务。确认页面与 IIS 托管无关,即使使用自托管,我们也可以使用浏览器定位服务地址。
(1).编程方式启用元数据交换
若要以编程方式启用基于 HTTP-GET 的元数据交换,首先需要将行为添加到行为集合中,该行为集合是宿主针对服务类型而维持的。ServiceHostBase 类定义了 Description 属性,类型为 ServiceDescription 。
顾名思义,ServiceDescription 就是对服务各个方面与行为的描述。Service-Description类定义了类型为 KeyedByTypeCollection<I>的属性 Behaviors。所有行为的类与特性均实现了 IServiceBehavior 接口。
例 1-11:编程方式启用元数据交换行为
ServiceHost host = new ServiceHost(typeof(MyService));
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{ metadataBehavior = new ServiceMetadataBehavior();
metadataBehavior.HttpGetEnabled = true;
host.Description.Behaviors .Add(metadataBehavior); }
host.Open();
ServiceMetadataBehavior 类定义在 System.ServiceModel. Description 命名空间 下,如果返回的行为为 null,托管代码就会创建一个新的 ServiceMetadataBehavior 对象,并将 HttpGetEnabled 属性值设为 true,然后将它添加到服务描述的 behaviors 属性中。
(2).元数据交换终结点
元数据交换终结点是一种特殊的终结点,有时候又被称为 MEX 终结点。服务能够根据元数据交换终结点发布自己的元数据。
图 1-7 展示了一个具有业务终结点和元数据交换终结点的服务。不过,在通常情况下并不需要在设计图中显示元数据交换终结点。
元数据交换终结点支持元数据交换的行业标准,在 WCF 中表现为IMetadataExchange 接口,IMetadataExchange 接口定义的细节并不合理。它与多数行业标准相似,都存在难以实现的问题。所幸,WCF 自动地为服务宿主提供了 IMetadataExchange 接口的实现,公开元数据交换终结点。我们只需要指定使用的地址和绑定,以及添加服务元数据行为。对于绑定,WCF 提供了专门的基于 HTTP、HTTPS、TCP 和 IPC 协议的绑定传输元素。对于地址,我们可以提供完整的地址,或者使用任意一个注册了的基地址。没有必要启用 HTTP-GET 选项,但是即使启用了也不会造成影响。
例 1-12 演示的服务公开了三个 MEX 终结点,分别基于 HTTP、TCP 和 IPC。出于演示的目的,TCP 和 IPC 的 MEX 终结点使用了相对地址,HTTP 则使用了绝对地址。
(3).编程方式添加MEX终结点
与其他终结点相似,我们只能在打开宿主之前通过编码方式添加元数据交换终结点。WCF并没有为元数据交换终结点提供专门的绑定类型。为此,我们需要创建定制绑定。定制绑定使用了与之匹配的传输绑定元素,然后将绑定元素对象作为构造函数的参数,传递给定制绑定实例。最后,调用宿主的 AddServiceEndpoint()方法,参数值分别为地址、定制绑定与IMetadataExchange 契约类型。例 1-13 的代码添加了基于 TCP 的 MEX 终结点。注意,在添加终结点之前,必须校验元数据行为是否存在。 例 1-13:编程方式添加 TCP MEX 终结点
BindingElement bindingElement = new TcpTransportBindingElement();
CustomBinding binding = new CustomBinding(bindingElement);
Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/");
ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress);
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); host.Description.Behaviors .Add(metadataBehavior); } host.AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX" );
host.Open();
(4).简化ServiceHost<T>类
我们可以扩展 ServiceHost<T>类,从而自动实现例 1-11 和例 1-13 中的代码。 ServiceHost<T>定义了 Boolean 型属性 EnableMetadataExchange,通过调用该属性添加 HTTP-GET 元数据行为和 MEX 终结点。 如果 EnableMetadataExchange 属性设置为 true,就会添加元数据交换行为。如果没有可用的 MEX 终结点,它就会为每个已注册的基地址样式添加一个 MEX 终结点。使用ServiceHost<T>,例 1-11 和例 1-13 就可以简化为。 ServiceHost<T>还定义了 Boolean 属性 HasMexEndpoint。如果服务包含了任意一个 MEX 终结点(与传输协议无关),则返回 true。ServiceHost<T>定义的AddAllMexEndPoints()方法可以为每个已注册的基地址添加一个 MEX 终结点,这些基地址的样式类型包括 HTTP、TCP 或 IPC。例 1-14 介绍了这些方法的实现。 对于ServiceHost<T>的实现请查看对应的ServiceHost<T>的文件。
(5).元数据浏览器
元数据交换终结点提供的元数据不仅描述了契约与操作,还包括关于数据契约、安全性、事务性、可靠性以及错误的信息。为了可视化表示正在运行的服务的元数据,我们开发了元数据浏览器工具,它的实现包含在本书附带的源代码中。
14.客户端编程
若要调用服务的操作,客户端首先需要导入服务契约到客户端的本地描述(Native Representation)中。如果客户端使用了 WCF,调用操作的常见做法是使用代理。代理是一个 CLR 类,它公开了一个单独的 CLR 接口用以表示服务契约。注意,如果服务支持多个契约(至少是多个终结点),客户端则需要一个代理对应每个契约类型。代理不仅提供了与服务契约相同的操作,同时还包括管理代理对象生命周期的方法,以及管理服务连接的方法。代理完全封装了服务的每个方面:服务的位置、实现技术、运行时平台以及通信传输。
(1).生成代理
如果服务是自托管的,则首先需要启动服务,然后从客户端项目的快捷菜单中选择“Add Service Reference...”。 如果服务托管在 IIS 或 WAS 中,则无需预先启动服务。值得注意的是,如果服务同时又作为客户端项目自托管在相同解决方案的另一个项目中,则可以在 Visual Studio 2005 中启动宿主,并添加引用。
也可以使用命令行工具 SvcUtil.exe工具来生成代理。
代理类的闪光之处在于它可以只包含服务公开的契约,而不需要添加对服务实现类的引用。我们可以通过提供地址和绑定的客户端配置文件使用代理,也可以不通过配置文件直接使用。注意,每个代理的实例都确切地指向了一个终结点。创建代理时需要与终结点交互。正如前文提及,如果服务端契约没有提供命名空间,则默认的命名空间为http://tempuri.org 。
(2).管理方式配置客户端
客户端需要知道服务的位置,需要使用与服务相同的绑定,当然,客户端还要导入服务契约的定义。本质上讲,它的信息与从服务的终结点获取的信息完全相同。为了体现这些信息,客户端配置文件需要包含目标终结点的信息,甚至使用与宿主完全相同的终结点配置样式。 客户端配置文件可以列出同样多的对应服务支持的终结点,客户端能够使用这些终结点中的任意一个。注意,客户端配置文件中的每个终结点都有一个唯一的名称。
(3).绑定配置 我们可以使用与服务配置相同的风格定制匹配服务绑定的客户端标准绑定。
(4).生成客户端配置文件
在默认情况下,SvcUtil也可以自动生成客户端配置文件output.config。建议永远不要使用SvcUtil工具生成配置文件。原因在于它会自动地为关键的绑定节设置默认值,反而导致了整个配置文件的混乱。
(5).进程内托管配置
对于进程内托管,客户端配置文件就是服务宿主的配置文件。同一个文件既包含了服务配置入口,也包含了客户端的配置入口。 注意,进程内宿主使用了命名管道绑定。
(6).SvcConfigEditor编辑器
对于使用 SvcConfigEditor,优劣参半。一方面,它可以帮助开发者轻松快捷地编辑配置文件,从而节约了掌握配置样式的时间。另一方面,它却不利于开发者对 WCF 配置的整体理解。
(7).创建和使用代理
代理类派生自ClientBase类,ClientBase 类通过泛型类型参数识别代理封装的服务契约。ClientBase 的 Channel 属性类型就是泛型参数的类型。ClientBase 的子类通过 Channel 调用它指向的服务契约的方法。
若要使用代理,客户端首先需要实例化代理对象,并为构造函数提供终结点信息,即配置文件中的终结点节名。如果没有使用配置文件,则为终结点地址和绑定对象。然后,客户端使用代理类的方法调用服务。一旦客户端调用完毕,就会关闭代理实例。
如果在客户端配置文件中,只为代理正在使用的契约类型定义了一个终结点,则客户端可以省略构造函数中的终结点名。然而,如果相同的契约类型包含了多个可用的终结点,则代理会抛出异常。
(8).关闭代理
最佳的做法是在客户端调用代理完毕之后要关闭代理。第 4 章会详细解释为何在正确情况下客户端需要关闭代理,因为关闭代理会终止与服务的会话,关闭连接。 使用代理的 Dispose()方法同样可以关闭代理。这种方式的优势在于它支持 using 语句的使用,即使出现异常,仍然能够调用:
using(MyContractClient proxy = new MyContractClient())
{ proxy.MyMethod(); }
如果客户端直接声明了契约,而不是具体的代理类,则客户端可以首先判断代理对象是否实现了 IDisposable 接口:
IMyContract proxy = new MyContractClient());
proxy.MyMethod();
IDisposable disposable = proxy as IDisposable;
if(disposable != null) { disposable.Dispose(); }
或者使用 using 语句,省略对类型的判断:
IMyContract proxy = new MyContractClient();
using(proxy as IDisposable) { proxy.MyMethod(); }
(9).调用超时
WCF 客户端的每次调用都必须在配置的超时值内完成。无论何种原因,一旦调用时间超出该时限,调用就会被取消,客户端会收到一个 TimeoutException 异常。绑定的一个属性用于设定超时的确切值,默认的超时值为 1min。若要设置不同的超时值,可以设置 Binding抽象基类的 SendTimeout 属性。
(10).编程方式配置客户端
如果不借助于配置文件,客户端也可以通过编程方式创建匹配服务终结点的地址与绑定对象,并将它们传递给代理类的构造函数。既然代理的泛型类型参数就是契约,因此不必为构造函数提供契约。为了表示地址,客户端需要实例化 EndpointAddress 类。 与在配置文件中使用绑定节的方法相似,客户端可以通过编程方式配置绑定属性:
WSHttpBinding wsBinding = new WSHttpBinding();
wsBinding.SendTimeout = TimeSpan.FromMinutes(5);
wsBinding.TransactionFlow = true;
EndpointAddress endpointAddress = new EndpointAddress(" http://localhost:8000/MyService/ ");
MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress);
proxy.MyMethod();
proxy.Close();
注意,使用 Binding 类的具体子类,是为了访问与绑定相关的属性,例如事务流。
(11).编程方式配置与管理方式配置
目前介绍的配置客户端与服务的两种技术各有所长,相辅相成。管理配置方式允许开发者在部署服务之后,修改服务与客户端的主要特性,而不需要重新编译或重新部署。主要缺陷则是不具备类型安全,只有在运行时才能发现配置的错误。 如果配置的决策完全是动态的,那么编程配置方式就体现了它的价值,它可以在运行时基于当前的输入或条件对服务的配置进行处理。如果判断条件是静态的,而且是恒定不变的,就可以采取硬编码方式。例如,如果我们只关注于进程内托管的调用,就可以采取硬编码方式,使用 NetNamePipeBinding 以及它的配置。不过,大体而言,大多数客户端和服务都会使用配置文件。
15.通道Channel(自定义知识点)
(1).WCF体系框架
WCF 提供了对可靠性、事务性、并发管理、安全性以及实例激活等技术的有力支持,它们均依赖于基于拦截机制的 WCF 体系架构(WCF Architecture)。通过代理与客户端的交互意味着 WCF 总是处于服务与客户端之间,拦截所有的调用,执行调用前和调用后的处理。当代理将调用栈帧(Stack Frame)序列化到消息中,并将消息通过通道链向下传递时,WCF 就开始执行拦截。通道相当于一个拦截器,目的在于执行一个特定的任务。每个客户端通道都会执行消息的调用前处理。链的组成与结构主要依赖于绑定。
例如,一个通道对消息编码(二进制格式、文本格式或者 MTOM),另一个通道传递安全的调用上下文;还有一个通道传播客户端的事务,一个通道管理可靠会话,另一个通道对消息正文(Message Body)加密(如果进行了配置),诸如此类。客户端的最后一个通道是传输通道,根据配置的传输方式发送消息给宿主。
在宿主端,消息同样通过通道链进行传输,它会对消息执行宿主端的调用前处理。宿主端的第一个通道是传输通道,接收传输过来的消息。随后的通道执行不同的任务,例如消息正文的解密、消息的解码、参与传播事务、设置安全准则、管理会话、激活服务实例。宿主端的最后一个通道负责将消息传递给分发器(Dispatcher)。分发器将消息转换到一个栈帧,并调用服务实例。执行顺序如图 1-11 所示。
服务并不知道它是否被本地客户端调用。事实上,服务会被本地客户端 —— 分发器调用。客户端与服务端的拦截器确保了它们能够获得运行时环境,以便于它们执行正确的操作。服务实例会执行调用,然后将控制权(Control)返回给分发器。分发器负责将返回值以及错误信息(如果存在)转换为一条返回消息。分发器获得控制权,执行的过程则刚好相反:分发器通过宿主端通道传递消息,执行调用后的处理,例如管理事务、停用实例、回复消息的编码与加密等。为了执行客户端调用后的处理,包括解密、解码、提交或取消事务等任务,传输通道会将返回消息发送到客户端通道。最后一个通道将消息传递给代理。代理将返回消息转化到栈帧,然后将控制权返回给客户端。
特别值得注意的是,体系架构中的所有要点均与可扩展性息息相关。我们可以为专有交互定制通道,为实例管理定制行为,以及定制安全行为等。事实上,WCF 提供的标准功能都能够通过相同的可扩展模式实现。本书介绍了许多针对可扩展性的实例与应用。
(2).宿主体系架构
如何将与技术无关的面向服务交互转换为 CLR 接口与类,对这一技术的探索无疑充满了趣味。宿主消除了两者之间的鸿沟,搭建了相互之间转换的桥梁。每个.NET 宿主进程都包含了多个应用程序域。每个应用程序域则包含了零到多个宿主实例。每个服务宿主实例专门对应于一个特殊的服务类型。创建一个宿主实例,实际上就是为对应于基地址的宿主机器的类型,注册一个包含了所有的终结点的服务宿主实例。每个服务宿主实例拥有零到多个上下文(Context)。上下文是服务实例最核心的执行范围。一个上下文最多只能与一个服务实例关联,这意味着上下文可能为空,不包含任何服务实例。体系架构如图 1-12 所示。
注意:WCF 上下文的概念与企业服务上下文(Enterprise Services Context)或者.NET 上下文绑定对象(Context-Bound Object)的上下文概念相似。
WCF 上下文将服务宿主与公开本地 CLR 类型为服务的上下文组合在一起。当消息经由通道进行传递时,宿主会将消息 。
(3).使用通道
我们可以直接使用通道调用服务的操作,而无须借助于代理类。ChannelFactory<T>类(以及它所支持的类型)有助于我们轻松地创建代理,如例 1-21 所示。
我们需要向 ChannelFactory<T>类的构造函数传递一个终结点对象,终结点名称可以从客户端配置文件中获取;或者传递绑定与地址对象,或者传递 ServiceEndpoint 对象。接着,调用 CreateChannel()方法获得代理的引用,然后使用代理的方法。最后,通过将代理强制转换为 IDisposable 类型,调用 Dispose()方法关闭代理。当然,也可以将代理强制转换为ICommunicationObject 类型,通过调用 Close()方法关闭代理:
ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>();
IMyContract proxy1 = factory.CreateChannel();
using(proxy1 as IDisposable) { proxy1.MyMethod(); }
IMyContract proxy2 = factory.CreateChannel();
proxy2.MyMethod();
ICommunicationObject c hannel = proxy2 as ICommunicationObject;
Debug.Assert(channel != null);
channel.Close();
我们还可以直接调用 CreateChannel()静态方法,根据给定的绑定和地址值创建代理,这样就不需要创建 ChannelFactory<T>类的实例了。
Binding binding = new NetTcpBinding();
EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000");
IMyContract proxy = ChannelFactory<IMyContract>.CreateChannel(binding,address);
using(proxy as IDisposable) { proxy1.MyMethod(); }
16.可靠性
WCF 与其他面向服务技术之间最大的区别在于传输可靠性(Transport Reliability)与消息可靠性(Message Reliability)。传输可靠性(例如通过 TCP 传输)在网络数据包层提供了点对点保证传递(Point-to-Point Guaranteed Delivery),以确保数据包的顺序无误。传输可靠性不会受到网络连接的中断或其他通信问题的影响。
顾名思义,消息可靠性负责处理消息层的可靠性,它与传递消息的数据包数量无关。消息可靠性提供了端对端保证传递(End-to-End Guaranteed Delivery),确保消息的顺序无误。消息可靠性与引入的中间方的数量无关,与网络跳数(Network Hops)的数量也没有关联。消息可靠性基于一个行业标准。该行业标准为可靠的基于消息的通信维持了一个在传输层的会话。如果传输失败,例如无线连接中断,消息可靠性就会提供重试(Retries)功能。它还能够自动处理网络阻塞(Congestion)、消息缓存(Message Buffering)以及流控制(Flow Control),根据具体情况适时调整发送的消息数。消息可靠性还能够通过对连接的验证管理连接自身,并在不需要连接时清除它们。
(1).绑定与可靠性
WCF 的可靠性是在绑定中控制与配置的。一个特定的绑定可以支持可靠消息传输(Reliable Messaging),也可以不支持它。如果支持,也可以通过设置为启用或禁用。何种绑定支持何种可靠性值,要根据绑定的目标场景而定。表 1-2 总结了绑定、可靠性、有序传递(Ordered Delivery)以及它们各自的默认值之间的关系。 BasicHttpBinding、NetPeerTcpBinding 以及两种 MSMQ 绑定(NetMsmqBinding 和 MsmqIntegrationBinding)不支持可靠性。因为 BasicHttpBinding 面向旧的 ASMX Web服务,是不具有可靠性的。NetPeerTcpBinding 则为广播场景设计。MSMQ 绑定针对断开调用,在任何情况下都不存在传输会话。 WSDualHttpBinding 总是支持可靠性的,它能够保持回调通道,确保基于 HTTP 协议的客户端存在。
NetTcpBinding 绑定以及各种 WS 绑定,默认情况下并不支持可靠性,但是允许启用对它的支持。由于 NetNamedPipeBinding 绑定总是拥有一个确定的从客户端到服务的跳数,因而它的可靠性是绑定固有的。
(2).有序消息
消息可靠性确保了消息的有序传递,允许消息按照发送顺序而非接收顺序执行。此外,它保证了消息只会被传递一次。 WCF 允许开发者只启用可靠性,而不启用有序传递。此时,消息按照接收它们的顺序进行传递。如果同时启用了可靠性与有序传递,则所有绑定的默认值均支持可靠性。
(3).配置可靠性
通过编程方式或管理方式都可以配置可靠性(以及有序传递)。如果我们启用了可靠性,则客户端与服务宿主端必须保持一致,否则客户端无法与服务通信。我们可以只对支持它的绑定配置可靠性。例 1-23 所示的服务端配置文件,使用了绑定配置节,启用了 TCP 绑定的可靠性。 例 1-23:启用 TCP 绑定的可靠性 .... <bindings>
<netTcpBinding>
<binding name = "ReliableTCP">
<reliableSession enabled = "true"/>
</binding>
</netTcpBinding>
</bindings>
.....
至于编程配置方式,TCP 绑定和 WS 绑定提供了略微不同的属性来配置可靠性。例如,NetTcpBinding 绑定接受一个 Boolean 型的构造函数参数,用来启动可靠性: public class NetTcpBinding : Binding,... { public NetTcpBinding(...,bool reliableSessionEnabled); //更多成员 } 我们只能在对象的构造期间启用可靠性。 NetTcpBinding 定义了只读的 ReliableSession 类,通过它获取可靠性的状态。
(4).必备有序传递
理论上,服务代码和契约定义应该与它使用的绑定及属性无关。服务不应该考虑绑定,在服务代码中也不应该包含它所使用的绑定。不管配置的绑定是哪一种,服务都应该能够正常工作。然而实际上,服务的实现或者契约本身都会依赖于消息的有序传递(Ordered Delivery)。为了帮助契约或服务的开发者能够约束支持的绑定,WCF 定义了DeliveryRequirementsAttribute 特性DeliveryRequirements 特性可以应用到服务一级,对服务的所有终结点施加影响,或者只对公开了特定契约的终结点施加影响;如果应用到服务一级,则意味着选用有序传递是根据具体实现作出的决策。DeliveryRequirements 特性也可以应用到契约一级,它会对所有支持该契约的服务施加影响。这样一种在契约一级的应用,体现了对有序传递的要求是根据设计作出的决策。这一约束会在装载服务时得到执行与验证。如果一个终结点包含的绑定并不支持可靠性;或者支持可靠性,却被禁用了;或者虽然启用了可靠性,但却禁用了有序传递,那么装载服务就会失败,抛出 InvalidOperationException 异常。
注意:命名管道绑定符合有序传递的约束。 举例来说,如果不考虑契约,要求服务的所有终结点都启用有序传递,则可以将 DeliveryRequirements 特性直接应用到服务类上:
[DeliveryRequirements(RequireOrderedDelivery = true)]
class MyService : IMyContract,IMyOtherContract {...}
通过设置 TargetContract 属性,只有支持目标契约的服务终结点才需要遵循可靠的有序传递的约束:
[DeliveryRequirements(TargetContract=typeo(IMyContract), RequireOrderedDelivery = true)]
class MyService : IMyContract,IMyOtherContract {...}
如果将 DeliveryRequirements 特性应用到契约接口上,则支持该契约的所有服务都必须遵循这一约束:
[DeliveryRequirements(RequireOrderedDelivery = true)]
[ServiceContract]
interface IMyContract {...}
class MyService : IMyContract {...}
class MyOtherService : IMyContract {...}
RequireOrderedDelivery 的默认值为 false,如果只应用了 DeliveryRequirements 特性,没有设置 RequireOrderedDelivery 的值,则是无效的。 [ServiceContract]
interface IMyContract {...}
[DeliveryRequirements]
[ServiceContract]
interface IMyContract {...}
[DeliveryRequirements(RequireOrderedDelivery = false)]
[ServiceContract]
interface IMyContract {...}