Programming WCF Services翻译笔记(九)
请求-应答(Request-Reply)操作
“除了NetPeerTcpBinding和NetMsmqBinding绑定,所有的绑定均支持请求-应答操作。”
单向操作
单向操作没有返回值。单向操作不同于异步操作,虽然单向操作只是在发出调用的瞬间阻塞客户端,但如果发出多个单向调用,WCF会将这些调用放入队列。队列存储调用的个数是有限的,一旦发出的调用个数超出了队列存储调用的设置值,则会发生阻塞现象,因为调用无法放入队列。当队列的请求出列后,产生阻塞的调用就会放入队列,并解除对客户端的阻塞。
设置单向操作的方法是利用OperationContract特性的IsOneWay属性,例如:
[ServiceContract]
interface IMyContract
{
[OperationContract(IsOneWay = true)]
void MyMethod( );
}
被设置为单向操作的方法不能包含返回值,即它的返回值只能为void,否则会抛出InvalidOperationException异常。
在会话契约中虽然允许定义单向操作,但由于单向操作无法正确地维持状态,因而,单向操作的最佳适用场景是在单调服务或单例服务中。“如果在会话契约中定义了单向操作,就必须保证单向操作是终止会话的最后一个操作(该操作必须遵循单向操作的规定,例如返回void类型值)。这可以通过分步操作来实现。”
单向操作如果抛出异常,则视其服务模型以及使用绑定的不同,会产生不同的结果。以下内容假定服务不会抛出FaultException异常或者它的子类。
如果是在会话服务中,则单向操作抛出的异常都会影响到客户端,只不过如果使用的绑定不相同,则抛出的异常会有所区别。
对于单例服务而言,无会话的单例服务与单调服务相似,具有会话的单例服务则与会话服务相似。
回调
回调机制如图所示:
回调机制尤其适用于事件。在WCF中,BasicHttpBinding或WSHttpBinding绑定并不支持回调,因为它们不支持双向通信。NetTcpBinding和NetNamedPipeBinding绑定以及WSDualHttpBinding绑定支持回调操作。
定义回调契约
一个服务契约最多只能包含一个回调契约。通过ServiceContract特性,可以指定回调契约:
interface ISomeCallbackContract
{
[OperationContract]
void OnCallback( );
}
[ServiceContract(CallbackContract = typeof(ISomeCallbackContract))]
interface IMyContract
{
[OperationContract]
intDoSomething( );
}
回调契约无须标记ServiceContract特性,但是在回调契约中必须为服务的操作标记OperationContract特性。
在导入回调契约的元数据中,回调契约以Callback结尾。为简便起见,我们在定义回调契约时,最好以Callback为后缀。
为了托管一个回调对象,客户端需要实例化回调对象,然后通过它创建一个上下文对象:
class MyCallback : IMyContractCallback
{
public void OnCallback( )
{...}
}
IMyContractCallback callback = new MyCallback( );
InstanceContext context = new InstanceContext(callback);
假定客户端的代理类为MyContractClient,则在客户端就可以通过上下文对象获得代理对象:
MyContractClient proxy = new MyContractClient(context);
注意,如果使用了回调契约,则客户端生成的代理类必须继承自DuplexClientBase<T>代理类,这是一个专门的支持双向通信的代理类。注意,该类的构造函数参数既可以接收InstanceContext类型的上下文对象,也可以接收object类型的回调契约对象。
然而,如果是通过SvcUtil或Visual Studio 2005生成的代理,却不能使用接收object类型对象的构造函数,若要创建代理对象,我们必须先创建上下文对象,如前面的代码所示。
我们可以手动修改代理类,添加对它的支持,如下所示:
partial class MyContractClient : DuplexClientBase<IMyContract>,IMyContract
{
public MyContractClient(object callbackInstance) : base(callbackInstance)
{}
//More constructors
public void DoSomething( )
{
Channel.DoSomething( );
}
}
class MyClient : IMyContractCallback,IDisposable
{
MyContractClient m_Proxy;
public void CallService( )
{
m_Proxy = new MyContractClient(this);
m_Proxy.DoSomething( );
}
public void OnCallback( )
{...}
public void Dispose( )
{
m_Proxy.Close( );
}
}
注意,上述的代码中直接由客户端实现了回调契约,这是一种比较常见的实现方式。
客户端通过回调传递给服务端的消息包含了回调契约终结点的引用。在服务端,可以通过OperationContext类的泛型方法GetCallbackChannel<T>()获得。如下所示:
ISomeCallbackContract callback = OperationContext.Current. GetCallbackChannel<ISomeCallbackContract>( );
服务对回调的调用可能会产生死锁。例如,当客户端执行服务操作时,向客户端发出的调用会阻塞服务端进程,以等待服务操作执行完毕。而在该服务操作中,又获得了回调契约对象的引用(或者获得保存的回调契约副本),并执行回调操作。由于服务类被配置为单线程访问,则服务实例是与锁相关联的。如果回调对象也需要返回同一个锁的所有权,简单的说,就是指当回调的应答消息也需要获得与服务实例关联的相同的锁时,就会导致死锁。因为此时服务线程已经被阻塞,服务操作正在等待回调操作执行完毕,而回调操作却又在等待服务释放锁,自然会产生锁的争用。
解决死锁的办法有三个,一个是将服务配置为允许多线程访问,但这会增加服务开发者管理多线程的负担。第二个方案是将回调设置为重入(Reentrancy),如下所示:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class MyService : IMyContract
{
public void DoSomething( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
callback.OnCallback( );
}
}
所谓“重入”,是指对同步域拥有独占访问权的线程A调用了同步域之外对象的方法,此时,另外的线程B若要访问该同步域,则线程A将释放对同步域的锁,允许线程B进入。直到线程B执行完毕并释放对同步域的锁后,线程A将重新进入该同步域。配置回调为重入时,因为服务对象是与线程关联的,属于同步域的对象,而回调对象则属于同步域之外的对象。由于服务被配置为重入,则服务调用回调引用时会释放锁。然后将回调返回给客户端,控制权则返回给服务,服务会重入并重新获取锁。这样就解决了死锁的问题。
第三种方案则是将回调操作设置为单向操作。此时,回调调用不会产生应答消息,服务操作一旦执行了回调操作,就会继续执行,回调对象不会争用与服务实例关联的锁,从而解决了死锁问题。
interface IMyContractCallback
{
[OperationContract(IsOneWay = true)]
void OnCallback( );
}
在使用回调对象时,需要考虑到客户端代理可能会被关闭,如果此时调用回调,就会引发一个ObjectDisposedException异常。“因此,对于客户端而言,当它不再需要接收回调或者客户端应用程序已经关闭时,最好能够通知服务。”本书给出了解决这一问题的方法,就是为服务契约增加两个操作Connect()与Disconnect()。其中,Disconnect()正是起到了通知服务的作用,它在客户端代理关闭的情况下,可以将当前的回调对象引用从列表中移除。至于Connect()方法则是出于对称的目的而引入,但引入它还有一个好处是,它可以使得客户端能够多次地连接或断开。实现Connect()与Disconnect()方法的代码如下:
static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>( );
public void Connect( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
if(m_Callbacks.Contains(callback) == false)
{
m_Callbacks.Add(callback);
}
}
public void Disconnect( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
if(m_Callbacks.Contains(callback) == true)
{
m_Callbacks.Remove(callback);
}
else
{
throw new InvalidOperationException("Cannot find callback");
}
}
单调服务与单例服务非常适合使用这种方式维持回调对象引用。会话服务则没有必要。“但是,如果会话服务为了让其它宿主端或跨会话访问,而将它的回调引用保存在某个全局变量中,就必须添加Disconnect()方法,以达到移除回调引用的目的,因为在调用Dispose()期间不能使用回调引用。”
DuplexClientBase<T>类并没有为回调契约提供类型安全性,也没有对服务契约与回调契约之间的约束关系进行验证。因此,本书还提供了DuplexClientBase<T,C>类,通过反射、泛型等技术对其提供类型安全。
与ChannelFactory<T>类相似,WCF同样提供了DuplexChannelFactory<T>类,它被用于通过编程方式设置双向代理。同样的,DuplexChannelFactory<T>类仍然缺乏对回调契约的类型安全。因此,本书提供了DuplexChannelFactory<T,C>类,可以弥补DuplexChannelFactory<T>类的不足。
回调契约仍然具有继承的层级。书中讲述了很多,其实一言以蔽之,就是要求回调契约的层级与服务契约的层级保持一致。此外,服务自身还可以实现回调契约。
由于使用WSDualHttpBinding绑定执行回调时,需要开通两个HTTP通道,一个用于服务,一个用于回调。通常,WCF为回调通道选择了默认的80端口,但如果客户端运行了IIS,则可能会占用80端口,就会导致端口的冲突。实际上,我们可以为回调通道指定任何可用的端口,例如,我们可以通过配置文件为客户端指定基地址。然而,我们指定的端口仍然可能会被占用,只是这种占用的可能性比80端口小。为了避免潜在的端口冲突,同时简化程序员的工作,最好的办法是自动分配一个可用的端口。本书定义了WsDualProxyHelper静态辅助类,能够将客户端基地址设置为任意的可用端口。定义如下:
public static class WsDualProxyHelper
{
public static void SetClientBaseAddress<T>(DuplexClientBase<T> proxy,int port)
where T : class
{
WSDualHttpBinding binding = proxy.Endpoint.Binding as WSDualHttpBinding;
Debug.Assert(binding != null);
binding.ClientBaseAddress = new Uri("http://localhost:"+ port + "/");
}
public static void SetClientBaseAddress<T>(DuplexClientBase<T> proxy)
where T : class
{
lock(typeof(WsDualProxyHelper))
{
int portNumber = FindPort( );
SetClientBaseAddress(proxy,portNumber);
proxy.Open( );
}
}
internal 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;
}
}
}
WsDualProxyHelper类的使用如下所示:
class MyClient : IMyContractCallback
{...}
IMyContractCallback callback = new MyClient( );
InstanceContext context = new InstanceContext(callback);
MyContractClient proxy = new MyContractClient(context);
WsDualProxyHelper.SetClientBaseAddress(proxy);
本文已在IT168发表:http://tech.it168.com/msoft/2007-11-19/200711190953995.shtml
“除了NetPeerTcpBinding和NetMsmqBinding绑定,所有的绑定均支持请求-应答操作。”
单向操作
单向操作没有返回值。单向操作不同于异步操作,虽然单向操作只是在发出调用的瞬间阻塞客户端,但如果发出多个单向调用,WCF会将这些调用放入队列。队列存储调用的个数是有限的,一旦发出的调用个数超出了队列存储调用的设置值,则会发生阻塞现象,因为调用无法放入队列。当队列的请求出列后,产生阻塞的调用就会放入队列,并解除对客户端的阻塞。
设置单向操作的方法是利用OperationContract特性的IsOneWay属性,例如:
[ServiceContract]
interface IMyContract
{
[OperationContract(IsOneWay = true)]
void MyMethod( );
}
被设置为单向操作的方法不能包含返回值,即它的返回值只能为void,否则会抛出InvalidOperationException异常。
在会话契约中虽然允许定义单向操作,但由于单向操作无法正确地维持状态,因而,单向操作的最佳适用场景是在单调服务或单例服务中。“如果在会话契约中定义了单向操作,就必须保证单向操作是终止会话的最后一个操作(该操作必须遵循单向操作的规定,例如返回void类型值)。这可以通过分步操作来实现。”
单向操作如果抛出异常,则视其服务模型以及使用绑定的不同,会产生不同的结果。以下内容假定服务不会抛出FaultException异常或者它的子类。
绑定 |
单调服务 |
BasicHttpBinding绑定 |
客户端不受影响 |
不包含可靠消息传输与安全的WSHttpBinding绑定 |
客户端不受影响 |
具有安全性的WSHttpBinding绑定 |
通道错误,客户端无法继续发出调用 |
不包含可靠消息传输的NetTcpBinding或NetNamedPipeBinding绑定 |
通道错误,客户端无法继续发出调用 |
具有可靠消息传输的WSHttpBinding绑定或NetTcpBinding绑定 |
客户端不受影响 |
如果是在会话服务中,则单向操作抛出的异常都会影响到客户端,只不过如果使用的绑定不相同,则抛出的异常会有所区别。
对于单例服务而言,无会话的单例服务与单调服务相似,具有会话的单例服务则与会话服务相似。
回调
回调机制如图所示:
回调机制尤其适用于事件。在WCF中,BasicHttpBinding或WSHttpBinding绑定并不支持回调,因为它们不支持双向通信。NetTcpBinding和NetNamedPipeBinding绑定以及WSDualHttpBinding绑定支持回调操作。
定义回调契约
一个服务契约最多只能包含一个回调契约。通过ServiceContract特性,可以指定回调契约:
interface ISomeCallbackContract
{
[OperationContract]
void OnCallback( );
}
[ServiceContract(CallbackContract = typeof(ISomeCallbackContract))]
interface IMyContract
{
[OperationContract]
intDoSomething( );
}
回调契约无须标记ServiceContract特性,但是在回调契约中必须为服务的操作标记OperationContract特性。
在导入回调契约的元数据中,回调契约以Callback结尾。为简便起见,我们在定义回调契约时,最好以Callback为后缀。
为了托管一个回调对象,客户端需要实例化回调对象,然后通过它创建一个上下文对象:
class MyCallback : IMyContractCallback
{
public void OnCallback( )
{...}
}
IMyContractCallback callback = new MyCallback( );
InstanceContext context = new InstanceContext(callback);
假定客户端的代理类为MyContractClient,则在客户端就可以通过上下文对象获得代理对象:
MyContractClient proxy = new MyContractClient(context);
注意,如果使用了回调契约,则客户端生成的代理类必须继承自DuplexClientBase<T>代理类,这是一个专门的支持双向通信的代理类。注意,该类的构造函数参数既可以接收InstanceContext类型的上下文对象,也可以接收object类型的回调契约对象。
然而,如果是通过SvcUtil或Visual Studio 2005生成的代理,却不能使用接收object类型对象的构造函数,若要创建代理对象,我们必须先创建上下文对象,如前面的代码所示。
我们可以手动修改代理类,添加对它的支持,如下所示:
partial class MyContractClient : DuplexClientBase<IMyContract>,IMyContract
{
public MyContractClient(object callbackInstance) : base(callbackInstance)
{}
//More constructors
public void DoSomething( )
{
Channel.DoSomething( );
}
}
class MyClient : IMyContractCallback,IDisposable
{
MyContractClient m_Proxy;
public void CallService( )
{
m_Proxy = new MyContractClient(this);
m_Proxy.DoSomething( );
}
public void OnCallback( )
{...}
public void Dispose( )
{
m_Proxy.Close( );
}
}
注意,上述的代码中直接由客户端实现了回调契约,这是一种比较常见的实现方式。
客户端通过回调传递给服务端的消息包含了回调契约终结点的引用。在服务端,可以通过OperationContext类的泛型方法GetCallbackChannel<T>()获得。如下所示:
ISomeCallbackContract callback = OperationContext.Current. GetCallbackChannel<ISomeCallbackContract>( );
服务对回调的调用可能会产生死锁。例如,当客户端执行服务操作时,向客户端发出的调用会阻塞服务端进程,以等待服务操作执行完毕。而在该服务操作中,又获得了回调契约对象的引用(或者获得保存的回调契约副本),并执行回调操作。由于服务类被配置为单线程访问,则服务实例是与锁相关联的。如果回调对象也需要返回同一个锁的所有权,简单的说,就是指当回调的应答消息也需要获得与服务实例关联的相同的锁时,就会导致死锁。因为此时服务线程已经被阻塞,服务操作正在等待回调操作执行完毕,而回调操作却又在等待服务释放锁,自然会产生锁的争用。
解决死锁的办法有三个,一个是将服务配置为允许多线程访问,但这会增加服务开发者管理多线程的负担。第二个方案是将回调设置为重入(Reentrancy),如下所示:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class MyService : IMyContract
{
public void DoSomething( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
callback.OnCallback( );
}
}
所谓“重入”,是指对同步域拥有独占访问权的线程A调用了同步域之外对象的方法,此时,另外的线程B若要访问该同步域,则线程A将释放对同步域的锁,允许线程B进入。直到线程B执行完毕并释放对同步域的锁后,线程A将重新进入该同步域。配置回调为重入时,因为服务对象是与线程关联的,属于同步域的对象,而回调对象则属于同步域之外的对象。由于服务被配置为重入,则服务调用回调引用时会释放锁。然后将回调返回给客户端,控制权则返回给服务,服务会重入并重新获取锁。这样就解决了死锁的问题。
第三种方案则是将回调操作设置为单向操作。此时,回调调用不会产生应答消息,服务操作一旦执行了回调操作,就会继续执行,回调对象不会争用与服务实例关联的锁,从而解决了死锁问题。
interface IMyContractCallback
{
[OperationContract(IsOneWay = true)]
void OnCallback( );
}
在使用回调对象时,需要考虑到客户端代理可能会被关闭,如果此时调用回调,就会引发一个ObjectDisposedException异常。“因此,对于客户端而言,当它不再需要接收回调或者客户端应用程序已经关闭时,最好能够通知服务。”本书给出了解决这一问题的方法,就是为服务契约增加两个操作Connect()与Disconnect()。其中,Disconnect()正是起到了通知服务的作用,它在客户端代理关闭的情况下,可以将当前的回调对象引用从列表中移除。至于Connect()方法则是出于对称的目的而引入,但引入它还有一个好处是,它可以使得客户端能够多次地连接或断开。实现Connect()与Disconnect()方法的代码如下:
static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>( );
public void Connect( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
if(m_Callbacks.Contains(callback) == false)
{
m_Callbacks.Add(callback);
}
}
public void Disconnect( )
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>( );
if(m_Callbacks.Contains(callback) == true)
{
m_Callbacks.Remove(callback);
}
else
{
throw new InvalidOperationException("Cannot find callback");
}
}
单调服务与单例服务非常适合使用这种方式维持回调对象引用。会话服务则没有必要。“但是,如果会话服务为了让其它宿主端或跨会话访问,而将它的回调引用保存在某个全局变量中,就必须添加Disconnect()方法,以达到移除回调引用的目的,因为在调用Dispose()期间不能使用回调引用。”
DuplexClientBase<T>类并没有为回调契约提供类型安全性,也没有对服务契约与回调契约之间的约束关系进行验证。因此,本书还提供了DuplexClientBase<T,C>类,通过反射、泛型等技术对其提供类型安全。
与ChannelFactory<T>类相似,WCF同样提供了DuplexChannelFactory<T>类,它被用于通过编程方式设置双向代理。同样的,DuplexChannelFactory<T>类仍然缺乏对回调契约的类型安全。因此,本书提供了DuplexChannelFactory<T,C>类,可以弥补DuplexChannelFactory<T>类的不足。
回调契约仍然具有继承的层级。书中讲述了很多,其实一言以蔽之,就是要求回调契约的层级与服务契约的层级保持一致。此外,服务自身还可以实现回调契约。
由于使用WSDualHttpBinding绑定执行回调时,需要开通两个HTTP通道,一个用于服务,一个用于回调。通常,WCF为回调通道选择了默认的80端口,但如果客户端运行了IIS,则可能会占用80端口,就会导致端口的冲突。实际上,我们可以为回调通道指定任何可用的端口,例如,我们可以通过配置文件为客户端指定基地址。然而,我们指定的端口仍然可能会被占用,只是这种占用的可能性比80端口小。为了避免潜在的端口冲突,同时简化程序员的工作,最好的办法是自动分配一个可用的端口。本书定义了WsDualProxyHelper静态辅助类,能够将客户端基地址设置为任意的可用端口。定义如下:
public static class WsDualProxyHelper
{
public static void SetClientBaseAddress<T>(DuplexClientBase<T> proxy,int port)
where T : class
{
WSDualHttpBinding binding = proxy.Endpoint.Binding as WSDualHttpBinding;
Debug.Assert(binding != null);
binding.ClientBaseAddress = new Uri("http://localhost:"+ port + "/");
}
public static void SetClientBaseAddress<T>(DuplexClientBase<T> proxy)
where T : class
{
lock(typeof(WsDualProxyHelper))
{
int portNumber = FindPort( );
SetClientBaseAddress(proxy,portNumber);
proxy.Open( );
}
}
internal 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;
}
}
}
WsDualProxyHelper类的使用如下所示:
class MyClient : IMyContractCallback
{...}
IMyContractCallback callback = new MyClient( );
InstanceContext context = new InstanceContext(callback);
MyContractClient proxy = new MyContractClient(context);
WsDualProxyHelper.SetClientBaseAddress(proxy);
本文已在IT168发表:http://tech.it168.com/msoft/2007-11-19/200711190953995.shtml