客户端编程
若要调用服务的操作,客户端首先需要导入服务契约到客户端的本地描述(Native Representation)中。如果客户端使用了WCF,调用操作的常见做法是使用代理。代理是一个CLR类,它公开了一个单独的CLR接口用以表示服务契约。注意,如果服务支持多个契约(至少是多个终结点),客户端则需要一个代理对应每个契约类型。代理不仅提供了与服务契约相同的操作,同时还包括管理代理对象生命周期的方法,以及管理服务连接的方法。代理完全封装了服务的每个方面:服务的位置、实现技术、运行时平台以及通信传输。
生成代理
我们可以使用Visual Studio 2005导入服务的元数据,然后生成代理。如果服务是自托管的,则首先需要启动服务,然后从客户端项目的快捷菜单中选择“Add Service Reference...”。如果服务托管在IIS或WAS中,则无需预先启动服务。值得注意的是,如果服务同时又作为客户端项目自托管在相同解决方案的另一个项目中,则可以在Visual Studio 2005中启动宿主,并添加引用。不同于大多数项目的设置,这一选项在调试状态下并没有被禁用(参见图1-9)。

图1-9:使用Visual Studio 2005生成代理
上述操作会弹出Add Service Reference对话框,然后开发者需要提供服务的基地址(或者基地址加上一个MEX的URI)以及包含了代理的命名空间。
如果不使用Visual Studio 2005,也可以使用命令行工具SvcUtil.exe。我们需要为SvcUtil工具提供HTTP-GET地址或者元数据交换终结点地址,而代理文件名则作为可选项。默认的代理文件名为output.cs,但是我们也可以使用/out开关指定不同的名字。
例如,如果服务MyService托管在IIS或WAS上,同时拥有可用的基于HTTP-GET的元数据共享,则只需要运行下列命令行:
SvcUtil http://localhost/MyService/MyService.svc /out:Proxy.cs
如果服务的宿主为IIS,并且选择的端口号不是80(例如端口号81),则必须将端口号提供给基地址:
SvcUtil http://localhost:81/MyService/MyService.svc /out:Proxy.cs
如果是自托管服务,假定它启用了基于HTTP-GET的元数据发布方式,则可以注册这些基地址,然后公开包含了一个MEX相对地址的与之匹配的元数据交换终结点:
net.tcp://localhost:8003
net.pipe://localhost/MyPipe
启动宿主后,可以使用如下命令生成代理:
SvcUtil http://localhost:8002/MEX /out:Proxy.cs
SvcUtil http://localhost:8002/ /out:Proxy.cs
SvcUtil net.tcp://localhost:8003/MEX /out:Proxy.cs
SvcUtil net.pipe://localhost/MyPipe/MEX /out:Proxy.cs
注意:SvcUtil工具优于Visual Studio 2005之处,在于它提供了大量的选项,通过开关控制生成的代理,正如我们在本书后面将要看到的那样。
针对服务的定义如下:
[ServiceContract(Namespace = "MyNamespace")]
interface IMyContract
{
[OperationContract]
void MyMethod();
}
class MyService : IMyContract
{
public void MyMethod()
{...}
}
SvcUtil生成的代理如例1-15所示。在大多数情况下,我们完全可以删除Action和ReplyAction的设置,因为默认使用方法名的设置已经足够。
例1-15:客户端代理文件
[ServiceContract(Namespace = "MyNamespace")]
public interface IMyContract
{
[OperationContract(Action = "MyNamespace/IMyContract/MyMethod",
ReplyAction = "MyNamespace/IMyContract/MyMethodResponse")]
void MyMethod();
}
public partial class MyContractClient : ClientBase<IMyContract>,IMyContract
{
public MyContractClient()
{}
public MyContractClient(string endpointName) : base(endpointName)
{}
public MyContractClient(Binding binding,EndpointAddress remoteAddress) :
base(binding,remoteAddress)
{}
/* 其他构造函数 */
public void MyMethod()
{
Channel.MyMethod();
}
}
代理类的闪光之处在于它可以只包含服务公开的契约,而不需要添加对服务实现类的引用。我们可以通过提供地址和绑定的客户端配置文件使用代理,也可以不通过配置文件直接使用。注意,每个代理的实例都确切地指向了一个终结点。创建代理时需要与终结点交互。正如前文提及,如果服务端契约没有提供命名空间,则默认的命名空间为http://tempuri.org。
管理方式配置客户端
客户端需要知道服务的位置,需要使用与服务相同的绑定,当然,客户端还要导入服务契约的定义。本质上讲,它的信息与从服务的终结点获取的信息完全相同。为了体现这些信息,客户端配置文件需要包含目标终结点的信息,甚至使用与宿主完全相同的终结点配置样式。
例1-16演示了与一个服务交互时必需的客户端配置文件,其中,服务宿主的配置参见例1-6。
例1-16:客户端配置文件
<system.serviceModel>
<client>
<endpoint name = "MyEndpoint"
address = "http://localhost:8000/MyService/"
binding = "wsHttpBinding"
contract = "IMyContract"
/>
</client>
</system.serviceModel>
客户端配置文件可以列出同样多的对应服务支持的终结点,客户端能够使用这些终结点中的任意一个。例1-17展示的客户端配置文件,与例1-7中的宿主配置文件相匹配。注意,客户端配置文件中的每个终结点都有一个唯一的名称。
例1-17:包含了多个目标终结点的客户端配置文件
<system.serviceModel>
<client>
<endpoint name = "FirstEndpoint"
address = http://localhost:8000/MyService/
binding = "wsHttpBinding"
contract = "IMyContract"
/>
<endpoint name = "SecondEndpoint"
address = "net.tcp://localhost:8001/MyService/"
binding = "netTcpBinding"
contract = "IMyContract"
/>
<endpoint name = "ThirdEndpoint"
address = "net.tcp://localhost:8002/MyService/"
binding = "netTcpBinding"
contract = "IMyOtherContract"
/>
</client>
</system.serviceModel>
绑定配置
我们可以使用与服务配置相同的风格定制匹配服务绑定的客户端标准绑定,如例1-18所示。
例1-18:客户端绑定配置
<system.serviceModel>
<client>
<endpoint name = "MyEndpoint"
address = "net.tcp://localhost:8000/MyService/"
bindingConfiguration = "TransactionalTCP"
binding = "netTcpBinding"
contract = "IMyContract"
/>
</client>
<bindings>
<netTcpBinding>
<binding name = "TransactionalTCP"
transactionFlow = "true"
/>
</netTcpBinding>
</bindings>
</system.serviceModel>
生成客户端配置文件
在默认情况下,SvcUtil也可以自动生成客户端配置文件output.config。同时,可以使用/config开关指定配置文件名:
SvcUtil http://localhost:8002/MyService/ /out:Proxy.cs /config:App.Config
也可以使用/noconfig开关生成精简的配置文件:
SvcUtil http://localhost:8002/MyService/ /out:Proxy.cs /noconfig
建议永远不要使用SvcUtil工具生成配置文件。原因在于它会自动地为关键的绑定节设置默认值,反而导致了整个配置文件的混乱。
进程内托管配置
对于进程内托管,客户端配置文件就是服务宿主的配置文件。同一个文件既包含了服务配置入口,也包含了客户端的配置入口,如例1-19所示。
例1-19:进程内托管的配置文件
<system.serviceModel>
<services>
<service name = "MyService">
<endpoint
address = "net.pipe://localhost/MyPipe"
binding = "netNamedPipeBinding"
contract = "IMyContract"
/>
</service>
</services>
<client>
<endpoint name = "MyEndpoint"
address = "net.pipe://localhost/MyPipe"
binding = "netNamedPipeBinding"
contract = "IMyContract"
/>
</client>
</system.serviceModel>
注意,进程内宿主使用了命名管道绑定。
SvcConfigEditor编辑器
WCF提供了配置文件的编辑器SvcConfigEditor.exe,它既能编辑宿主配置文件,又能编辑客户端配置文件(参见图1-10)。启动编辑器的方法是在Visual Studio中,右键单击配置文件(客户端和宿主文件),然后选择“Edit WCF Configuration”。

图1-10:用于编辑宿主与客户端配置文件的SvcConfigEditor
对于使用SvcConfigEditor,优劣参半。一方面,它可以帮助开发者轻松快捷地编辑配置文件,从而节约了掌握配置样式的时间。另一方面,它却不利于开发者对WCF配置的整体理解。多数情况下,采用手工方式对配置文件进行细微的修改,要比使用Visual Studio 2005更加快速。
创建和使用代理
代理类派生自ClientBase<T>类,定义如下:
public abstract class ClientBase<T> : ICommunicationObject,IDisposable
{
protected ClientBase(string endpointName);
protected ClientBase(Binding binding,EndpointAddress remoteAddress);
public void Open();
public void Close();
protected T Channel
{get;}
//其他成员
}
ClientBase<T>类通过泛型类型参数识别代理封装的服务契约。ClientBase<T>的Channel属性类型就是泛型参数的类型。ClientBase<T>的子类通过Channel调用它指向的服务契约的方法(参见例1-15)。
若要使用代理,客户端首先需要实例化代理对象,并为构造函数提供终结点信息,即配置文件中的终结点节名。如果没有使用配置文件,则为终结点地址和绑定对象。然后,客户端使用代理类的方法调用服务。一旦客户端调用完毕,就会关闭代理实例。以例1-15和例1-16的定义为例,客户端创建代理,然后通过配置文件识别使用的终结点,再调用代理的方法,最后关闭代理:
MyContractClient proxy = new MyContractClient("MyEndpoint");
proxy.MyMethod();
proxy.Close();
如果在客户端配置文件中,只为代理正在使用的契约类型定义了一个终结点,则客户端可以省略构造函数中的终结点名:
MyContractClient proxy = new MyContractClient();
proxy.MyMethod();
proxy.Close();
然而,如果相同的契约类型包含了多个可用的终结点,则代理会抛出异常。
关闭代理
最佳的做法是在客户端调用代理完毕之后要关闭代理。第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();
}
调用超时
WCF客户端的每次调用都必须在配置的超时值内完成。无论何种原因,一旦调用时间超出该时限,调用就会被取消,客户端会收到一个TimeoutException异常。绑定的一个属性用于设定超时的确切值,默认的超时值为1min。若要设置不同的超时值,可以设置Binding抽象基类的SendTimeout属性:
public abstract class Binding : ...
{
public TimeSpan SendTimeout
{get;set;}
//更多成员
}
例如,使用WSHttpBinding时:
<client>
<endpoint
...
binding = "wsHttpBinding"
bindingConfiguration = "LongTimeout"
...
/>
</client>
<bindings>
<wsHttpBinding>
<binding name = "LongTimeout" sendTimeout = "00:05:00"/>
</wsHttpBinding>
</bindings>
编程方式配置客户端
如果不借助于配置文件,客户端也可以通过编程方式创建匹配服务终结点的地址与绑定对象,并将它们传递给代理类的构造函数。既然代理的泛型类型参数就是契约,因此不必为构造函数提供契约。为了表示地址,客户端需要实例化EndpointAddress类,定义如下:
public class EndpointAddress
{
public EndpointAddress(string uri);
//更多成员
}
例1-20演示了编程方式配置客户端的技术,所示代码的功能与例1-16等价,它们使用的目标服务则为例1-9的定义。
例1-20:编程方式配置客户端
Binding wsBinding = new WSHttpBinding();
EndpointAddress endpointAddress = new
EndpointAddress("http://localhost:8000/MyService/");
MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress);
proxy.MyMethod();
proxy.Close();
与在配置文件中使用绑定节的方法相似,客户端可以通过编程方式配置绑定属性:
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类的具体子类,是为了访问与绑定相关的属性,例如事务流。





