[WCF]终结点与服务寻址(一)
最近在拜读蒋金楠先生的《WCF技术剖析(卷1)》,在WCF这门技术上,我觉得此书和《WCF从原理到实践》是最适合用于学习的,因为讲解都很详尽,尤其是前者更加深入讲究原理。
为了加深理解和学习研究,我决定开始写一系列的文章,这些文章描述了我在学习过程中总结的知识,然后通过实验尽可能证明这些知识。但并不是所有的知识都可以简单做实验展示的,因此可能在某些地方,我只能直接从书中引用。
本系列一共分为五篇文章,分别是以下五个实验:
1.简单通过绝对的逻辑地址部署服务终结点
2.使用基地址部署更具移植性的服务终结点
3.使用ChannelFactory<TChannel>取代服务代理对象
4.使用自定义寻址报头实现定制寻址
5.分离终结点部署的物理地址与逻辑地址
1.简单通过绝对的逻辑地址部署服务终结点
要使用WCF发布服务,终结点是一个必须理解的概念,因为服务消费者可以通过各种各样的环境访问服务,例如通过常见的HTTP协议,防火墙后的TCP协议或者同一台服务器上的其他进程通过命名管道来访问等。但无论怎样,进入服务操作都必须通过服务部署(公开)的终结点。通过部署不同的终结点,可以为服务提供多种访问途径,提供对访问消息的各种处理和过滤等功能。
接下来我就会简单实现一个服务,并通过部署一个终结点来发布该服务。
首先我定义了服务契约,还有具体的服务实现。代码如下:
{
[ServiceContract(Namespace="http://www.dreamofflea.net")]
public interface ISystemInfoServiceContract
{
[OperationContract]
string GetServerMachineName();
}
}
namespace WCF_Study1.ServiceContracts
{
public class SystemInfoService : ISystemInfoServiceContract
{
#region ISystemInfoServiceContract Members
public string GetServerMachineName()
{
return System.Environment.MachineName;
}
#endregion
}
}
可以看到,这个服务只是简单地返回服务器的机器名称,保持服务的简单,可以把注意力集中在重点上。接下来就是服务宿主程序的实现,我使用最简单的控制台程序作为服务的宿主,代码如下:
{
class Program
{
static void Main(string[] args)
{
using(ServiceHost host = new ServiceHost(typeof(WCF_Study1.ServiceContracts.SystemInfoService)))
{
host.Open();
Console.WriteLine("服务已经启动,按任意键关闭服务.....");
Console.ReadLine();
}
}
}
}
服务宿主对象host只是简单地被启动了。所有的部署细节我都放在配置文件中。在WCF(乃至各种远程调用框架)中,配置文件的处理成了关键点。虽然很多书籍都会双线介绍通过配置文件与程序代码来配置服务(包括文章开头我提到的两本书)。但我在这里只使用配置文件,除非一些很有趣的地方。因为配置文件的部署方式让服务部署更加灵活,而且代码配置方式并不复杂,基本上都可以从配置文件中各种对象的关系推导出来。因此往后的研究重点也在于这些配置文件,以下是服务宿主的配置文件代码:
<configuration>
<system.serviceModel>
<diagnostics>
<messageLogging logEntireMessage="true"
logMessagesAtServiceLevel="true"
logMessagesAtTransportLevel="true"
maxMessagesToLog="100000" />
</diagnostics>
<behaviors>
<serviceBehaviors>
<behavior name="metaBehavior">
<serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8880/meta" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="WCF_Study1.ServiceContracts.SystemInfoService"
behaviorConfiguration="metaBehavior">
<endpoint address="http://localhost:8888/SystemInfoService"
binding="basicHttpBinding"
contract="WCF_Study1.ServiceContracts.ISystemInfoServiceContract" />
</service>
</services>
</system.serviceModel>
<system.diagnostics>
<sharedListeners>
<add name="sharedListener"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData="G:\WCF Study Projects\WCF_Study1\Experiment result descriptions\终结点使用绝对逻辑地址\logs\clienttrace.svclog" />
</sharedListeners>
<sources>
<source name="System.ServiceModel"
switchValue="Verbose,ActivityTracing">
<listeners>
<add name="sharedListener" />
</listeners>
</source>
<source name="System.ServiceModel.MessageLogging"
switchValue="Verbose">
<listeners>
<add name="sharedListener" />
</listeners>
</source>
</sources>
</system.diagnostics>
</configuration>
上面的配置文件代码非常长,其实真正涉及终结点部署的代码只是很小的一段,往后我只会给出关键的代码段,在这里给出完整的配置代码,是为了说明一下,我是利用了SvcTraceViewer来追踪消息发送过程的。要使用这个软件追踪WCF消息的细节,就需要上面的很多配置代码(因为我让读者下载我的项目代码,所以不希望读者在打开项目后才发现这么复杂的配置代码,而感到沮丧)。
言归正传,在上面的配置代码中,我对路径部分做了加粗和斜体标示,因为当读者下载了项目后,该部分要改成读者自己的目录位置。在读者注意<services></services>这个节点中的配置代码。首先里面有个<service></service>节点,很明显这个节点中配置的信息就是用于刚才在服务宿主程序中被启动的host对象。那么这个对应关系是怎么建立起来的呢?请留意<service>节点的name属性,它的值为WCF_Study1.ServiceContracts.SystemInfoService,再回顾下这句代码:
注意到typeof()中的完整类名,没错,就是这样对应起来了。一旦建立了这个对应关系,在host的Open方法被调用后,WCF就会从配置文件中搜索对应的配置来初始化服务宿主对象。接下来再看下<endpoint/>节点。
binding属性:这个属性被设置为basicHttpBinding,使得这个终结点能处理基于HTTP协议传输的服务调用消息,就现在而言,我只希望让读者有这样的了解,因为关于绑定的内容足以让我写下下一系列的文章。
contract属性:每个服务实例都可以实现一个或多个服务契约的操作。因此每个终结点都被用于公开服务实例所实现的其中一个服务契约,在这个实验中,我只让服务实例实现一个服务契约,也就是WCF_Study1.ServiceContracts.ISystemInfoServiceContract。
address属性:这个属性用于设置访问这个终结点的逻辑地址,既然有逻辑地址,当然也有物理地址,关于物理地址将留在后面讲述。在现在的情况下,物理地址跟逻辑地址将是同一个地址,均为http://localhost:8888/SystemInfoService,通过这个地址,就可以访问该终结点,并由此调用服务操作。
通过上面的配置,现在已经可以启动服务宿主程序,下一步就是构建客户端程序作为服务消费者。以下为客户端程序代码:
{
class Program
{
static void Main(string[] args)
{
CallService_GetServerMachineName();
Console.ReadLine();
}
private static void CallService_GetServerMachineName()
{
WCF_Study1.Client.Services.SystemInfoServiceContractClient proxy
Console.WriteLine("服务端的终结点配置情况:");
Console.WriteLine("服务端的终结点,以绝对URL的方式公开访问地址(逻辑地址),并且不设置监听地址(物理地址)");
Console.WriteLine("调用的服务操作:GetServerMachineName");
Console.WriteLine("调用结果:Remote Server Name:{0}", proxy.GetServerMachineName());
}
}
}
在这个版本的客户端程序中,我直接构造服务代理对象(proxy),然后通过该对象进行服务调用,所有的远程消息细节都被代理封装。很明显,我没有在客户端程序中的任何地方指定我要调用的服务操作所需要的远程信息(例如:远程地址),那么这些信息从何而来?答案就是WCF应用中的另一个角色——客户端配置文件。以下为该文件的配置代码:
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ISystemInfoServiceContract" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="">
<extendedProtectionPolicy policyEnforcement="Never" />
</transport>
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8888/SystemInfoService" binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_ISystemInfoServiceContract"
contract="Services.ISystemInfoServiceContract" name="BasicHttpBinding_ISystemInfoServiceContract" />
</client>
</system.serviceModel>
</configuration>
同样,第一次我还是给出了完整的配置代码,<basicHttpBinding></basicHttpBinding>节点中,声明了标准的BasicHttpBinding,读者可以修改其中的各种配置,但不修改也完全没问题(毕竟这就是WCF的默认BasicHttpBinding设置)。重点还是<client></client>节点中的配置代码,同样有这<endpoint/>子节点,这个终结点被称为客户端终结点,它的作用就是跟服务端的某一个已经部署的终结点对应起来。当我构造服务代理对象的时候,该对象就自动使用了这个客户端终结点的信息来初始化自身(后面的内容中会看到,当出现多个客户端终结点的时候,必须明确指定使用哪个来初始化服务代理对象)。
从上面两份配置代码(分别是服务宿主程序的配置文件和客户端程序的配置文件)可以看出,当两者配置的终结点相一致(绑定、服务契约和访问地址,它们统称为终结点三要素)时,或者说相兼容时(以后会看到不必完全一致),对远程服务的调用就能顺利进行。具体的运行操作,读者可以下载实验代码完成。