经典的面向对象或组件编程模型只为客户端提供了一种调用方法的模型:客户端发出请求然后阻塞客户端,服务器端执行方法然后返回客户端,客户端接收请求后解除阻塞。如果想使用其他调用模式就必须进行手动调整,这样一来便可能影响项目的开发周期与质量。WCF除了支持这种经典的调用模式外还提供了对另外几种调用模型的内建支持。
一 即发即弃的单向操作
存在一种方式是不需要返回值的,客户端只关系请求是否到达的服务器端,不关心服务器端处理的结果如何。那么我们可以将调用配置为单向操作。但这样一来该调用方法得不到任何返回值(方法声明返回值为void),如果服务器端抛出异常客户端也一无所知。
在理想情况下是客户端发出请求然后阻塞客户端,请求进入服务器队列,然后客户端解除阻塞。所以客户端并不是只管发,它还需要确认请求是否到达了服务器端。如果服务器端队列满载则客户端一直被阻塞直到请求进入队列为止。
WCF所有绑定均支持单向操作。
1.1 配置单向操作
public interface IMember { [OperationContract(IsOneWay=true)] Void LogOut(); ...... }
IsOneWay的默认值为false,如果手动配置为true则该方法为单向操作。需要注意的是单向操作没有返回值。
如果服务器方法处理抛出异常在存在与不存在传输会话的情况下不相同的。查看一下代码:
[ServiceContract] public interface IMember { [OperationContract] bool Login(string username, string password, bool CreateCookie); [OperationContract(IsOneWay=true)] void LogOut(); [OperationContract] int Add(params int[] num); }
该契约指定不启用会话且配置LogOut为单向操作。
public class MemberService : IMember { public bool Login(string username, string password, bool CreateCookie) { return true; } public void LogOut() { throw new Exception(); } public int Add(params int[] nums) { int result = 0; foreach (var item in nums) { result += item; } return result; } }
客户端执行如下代码:
Console.WriteLine(client.Add(new int[] { 1, 2, 3, 4 })); client.LogOut(); Console.WriteLine(client.Login("", "", true));
如果绑定选用无传输会话的BasicHttpBinding。那么执行结果为:
10
True
由于无传输会话,而且LogOut为单向操作,服务器端抛出异常对客户端来说一无所知。所以LogOut后的Login方法任然得到正确的执行。
如果绑定为启用传输会话饿wsHttpBinding。那么指定结果为:
10
//这里会抛出异常:CommunicationExcaption。
由于存在传输会话,服务器端抛出异常,通道将不再可用,所以客户端将获得一个通讯错误的异常。
二 回调操作
WCF允许将调用返回给客户端,在这样的操作期间服务器客户端将倒转过来,服务器将成为客户端,客户端将成为服务器。这样的操作需要具有双工通信的绑定支持,BasicHttpBinding和WSHttpBinding是不支持双向调用的。我们可以选择具有双工通信功能的WSDualHttpBinding(实际上是两个Http通道)。NetTcpBinding和NetNamedPipeBinding提供了对回调的支持。
在回调操作中服务可能需要调用引入的回调引用。然后这样的行为被默认禁止(因为它受到默认的服务并发管理的限制)。在默认情况下服务类对象(实例)被配置为单线程访问的;服务实例上下文与锁关联。在操作调用期间,向客户端发出的调用会阻塞服务线程,并调用回调。但一旦回调返回需要重入相同的上下文,以及获得相同一个锁的所有权,这样一来处理从相同通道的客户端返回的应答详细会导致死锁。为了处理这样的问题存在以下解决方案。
2.1 配置服务行为为允许重入
一旦配置为允许重入,则服务实例任然与锁关联,同时只允许单线程访问。但是,如果服务正在回调客户端,WCF会首先释放锁。
以下代码演示使用该方案实现回调。
/*******************************契约********************************/
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(ICallBackContract))] public interface IMember { [OperationContract] string GetCallBackMessage(); } public interface ICallBackContract { [OperationContract] int SomeMethod(); }
一个服务契约只允许存在一个回调契约。回调契约不需要指明ServiceContract。因为如果是回调契约自然就是服务契约。但回调契约方法任然需要指明OperationContract。
/******************************服务实现******************************/
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode. Reentrant)] public class MemberService : IMember { public string GetCallBackMessage() { var callback = OperationContext.Current .GetCallbackChannel<ICallBackContract>(); return String.Format("Client Speak:{0}", callback.SomeMethod()); } }
服务实现需要配置服务行为为允许重入。通过OperationContext.Current.GetCallbackChannel<T>方法可以获得对回调的应用。
/******************************客户端********************************/
public class MyCallBack : IMemberCallback { public int SomeMethod() { return 1; } } IMemberCallback callback = new MyCallBack(); InstanceContext ctx = new InstanceContext(callback); using (MemberClient client = new MemberClient(ctx)) { Console.WriteLine(client.GetCallBackMessage()); //...... }
注:在客户端的代理中回调契约被重命名为原契约名+Callback的形式。所以这里原回调契约名ICallBackContract被重命名为IMemberCallback。
由于是服务器端调用客户端,者时候客户端为服务器端,服务器端为客户端。所以我们需要在客户端实现回调契约中的方法。然后我们需要在客户端托管对回调的服务。使用InstanceContext实例化一个回调。然后将这个实例作为参数传给客户端代理。之后框架会帮助我们完成其他的工作。
2.2 将回调配置为单向操作
如果回调不需要返回任何值,那么我们可以将其配置为单向操作。这样服务就能够安全地将调用返回给客户端,因为没有任何应答消息会争用锁。
[OperationContract(IsOneWay=true)] void GetCallBackMessage();
需要注意的是,在客户端关闭之前得先终止对回调的托管。否在会抛出异常。
IMemberCallback callback = new MyCallBack(); InstanceContext ctx = new InstanceContext(callback); using (MemberClient client = new MemberClient(ctx)) { try { client.GetCallBackMessage(); Console.WriteLine("回调操作已完成"); } finally { ctx.Close(); } }
2.3 回调连接管理
回调成功的前提是客户端处于开启状态。某些需要可能需要持久化链接,如使用WCF实现订阅-收发模式。服务器端将通知所有订阅客户端来取消息。一旦其中一个订阅者已经关闭将导致服务器端异常,导致其他订阅者无法接收请求。处理这样的请求这里建议在服务契约中显示引入Connect()和DisConnect()方法。并用List<T>保存连接。客户端注册订阅时添加连接,客户端离开时注销连接。
当我们使用WSDualHttpBinding的时候,WCF为回调维护了一个单独的专门的HTTP通道,因为HTTP自身属于单向协议。WCF为回调通道选择了80端口,并将使用HTTP的回调地址、客户端机器名称以及端口号80传递给服务器。通常,基于Internet的服务会使用80端口,但对于基于Internet的服务而言,端口值就太小了。此时如果客户端与运行了IE5 或6,由于80端口被预留,客户端将无法托管回调终结点。(默认情况下IE7允许端口共享)对于这样的情况我们可以为客户端代理分配回调基地址。
<configuration>
<system.serviceModel>
<client>
<endpoint
......
bindingConfiguration="Member">
</endpoint>
</client>
<bindings>
<wsDualHttpBinding>
<binding
......
clientBaseAddress="http://127.0.0.1:9899/">
......
</binding>
</wsDualHttpBinding>
</bindings>
由于服务不必预先知道回调端口,实际上所有可用的端口都能够作为基地址的端口。所以,最好以编程方式将客户端基地址设置为任意可用端口。以下是一个设置基地址的辅助类。
public static class WSDualProxyHelper
{
public static void SetClientBaseAddress<T>(this DuplexClientBase<T> proxy, int port)
where T : class
{
WSDualHttpBinding binding = proxy.Endpoint.Binding as WSDualHttpBinding;
Debug.Assert(binding != null);
binding.ClientBaseAddress = new Uri("http://127.0.0.1:" + port + "/");
}
public static void SetClientBaseAddress<T>(this DuplexClientBase<T> proxy) where T : class
{
lock (typeof(WSDualProxyHelper))
{
int portNum = FindPort();
SetClientBaseAddress(proxy, portNum);
proxy.Open();
}
}
private static int FindPort()
{
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
socket.Bind(endpoint);
IPEndPoint local = (IPEndPoint)socket.LocalEndPoint;
return local.Port;
}
}
}
IMemberCallback callback = new MyCallBack();
InstanceContext ctx = new InstanceContext(callback);
using (MemberClient client = new MemberClient(ctx))
{
client.SetClientBaseAddress();
......
}
另外还可以使用特性的方式制定基地址,使得程序变得更加优雅。
[AttributeUsage(AttributeTargets.Class)]
public class CallbackBaseAddressBehaviorAttribute : Attribute, IEndpointBehavior
{
int port = 80;
public int CallbackPort { get; set; }
void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
if (port == 80)
{
return;
}
lock (typeof(WSDualProxyHelper))
{
if (port == 0)
{
port = WSDualProxyHelper.FindPort();
}
WSDualHttpBinding binding = endpoint.Binding as WSDualHttpBinding;
if (binding != null)
{
binding.ClientBaseAddress =
new Uri("http://127.0.0.1:" + port + "/");
}
}
}
}
[CallbackBaseAddressBehavior(CallbackPort=0)]
public class MemberService : IMember,IDisposable
{
......
}
三 流操作
在默认情况下,当客户端与服务交换消息时,这些消息会放入接收端的缓存中,一旦接收到了完整的消息,就立即会被传递。在客户端调用服务的时候,只有服务接收到完整的消息后才会被调用。同样客户端只有在得到完整的放回结果时才会解除阻塞。
3.1 配置流操作
对于数据量小的消息,这种机制不会出现什么问题。但是如果数据量大到某种程度的时候这样的机制就显得不那么理想。为了处理这样的问题。WCF提供流操作,允许接收端通过通道接收信息的同时,启动对消息数据的处理。
public interface IMember { [OperationContract] Stream GetStream(); [OperationContract] void GetStream1(out Stream stream); [OperationContract] void GetStream2(Stream stream); }
这里只能使用Steam或则它的可序列化子类,如MemoryStream作为操作的参数。然而FileStream这样不支持序列化的子类就只能使用Stream。
默认情况下流操作不是默认的。我们需要使用TransferMode进行配置。默认值为Buffered,枚举Streamed值是支持所有流操作的配置选择。如果该契约只对于请求进行流操作或只对应答进行流操作则可以配置为StreamRequest或StreamRespones
配置方法通过WCF配置工具添加一个绑定配置给服务即可。
3.2 流操作的管理
当客户端将请求消息流传递给服务时,服务会在客户端停止调用之后读取流的长度。客户端无法得知服务器端何时使用流进行操作,这边会导致客户端无法关闭流,一旦服务执行完流操作,WCF负责关闭客户端的流
然后对于客户端通过应答流进行交互时,服务器端不知道客户端何时操作流,对于这样的情况WCF就无能为力了。所以总是应该在客户端负责关闭应答消息流。
此外,契约被配置为SessionMode.Required时无法使用流操作。