玩转C科技.NET

从学会做人开始认识这个世界!http://volnet.github.io

导航

[WCF]Instance Management

[有兴趣阅读本文的请从头至尾阅读,有兴趣帮助我解答疑问的请从尾至头读(红色部分),万分感谢!]
我们很容易理解在旧有编程模型中关于类实例的内容。设计模式中Singleton也就是在描述着档子事。但基于WCF并非适合于以上场景,Service与Client之间要保持良好的Instance模型则需要依靠很多其他机制。

Programming WCF Service Chapter4 对此进行了细致的描述。(更多细节请自行阅读~)

WCF支持三种类型的Instance管理:

InstanceManagement

1、pre-call services:每个客户端请求对应一个instance

2、Sessionful services:每个客户端连接对应一个instance

3、Singleton services:所有客户端共享一个instance

利用Behaviors可以解决这方面的问题(还有一些其他基于“服务端”的其他方面的问题可以通过使用behaviors来解决)。

注:客户端是不知道服务端设置了什么样的behaviors的。

VS2008MSDN:ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/fxref_system.servicemodel/html/88efb135-d425-e5b1-57d6-01a67158c1a5.htm

Apply the ServiceBehaviorAttribute attribute to a service implementation to specify service-wide execution behavior. (To specify execution behavior at the method level, use the OperationBehaviorAttribute attribute.) This attribute can be applied only to service implementations.

ServiceBehaviorAttribute:仅应用于服务实现。

OperationBehaviorAttribute:用于方法级别。

//瞧这里什么属性都没有 
public interface IMyContract
{} 
//而是设置在了具体服务实现上 
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class MyContract : IMyContract,IDisposable 
{}


设置instance模式类型由ServiceBehaviorAttribute的属性InstanceContextMode进行设置,默认值为PerSession.

Per-Call Services

只有当客户端调用的时候才有instance。

为了说明问题,书中用了很形象的例子。

Code:

public interface IMyContract
{
    [OperationContract]
    
string GetData(int value); 
    [OperationContract]
    CompositeType GetDataUsingDataContract(CompositeType composite); 
    
// TODO: Add your service operations here
    [OperationContract]
    
void Count();
}
[ServiceBehavior(InstanceContextMode 
= InstanceContextMode.PerCall)]
public class MyContract : IMyContract,IDisposable
{
    
//Other Members
    public MyContract()
    {
        Trace.WriteLine(
"WcfServiceLibrary1.MyContract()");
    } 
    
#region IMyContract Members
    
int count = 0;
    
public void Count()
    {
        count
++;
        Trace.WriteLine(
"Counter = " + count);
    } 
    
#endregion 
    
#region IDisposable Members 
    
public void Dispose()
    {
        Trace.WriteLine(
"WcfServiceLibrary1.Dispose()");
    } 
    
#endregion
}
//Tester

ServiceReference1.MyContractClient proxy 
= new ConsoleApplication1.ServiceReference1.MyContractClient(); 

proxy.Count();
proxy.Count();
proxy.Count();

proxy.Close();
Console.ReadKey();

结果为:

WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()
WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()
WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()

很明显,每次的值都是0+1的结果,这正说明了percall的方式是每个请求一个Instance的。

Per-Session Services

修改上面的例子:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]

为:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]

结果为:

WcfServiceLibrary1.MyContract()
Counter = 1
Counter = 2
Counter = 3
WcfServiceLibrary1.Dispose()

很明显Instance只有一个了。

我们WcfServiceLibrary默认的Bind是wsHttpBinding,但若是basicHttpBinding,由于每个http到达服务端都是一个新的连接,因此服务端无法判断是哪个连接。

增加服务端app.config中Endpoint。

<endpoint address="basic" binding="basicHttpBinding" name="basic" contract="WcfServiceLibrary1.IMyContract" />

重新导入后修改Program里的程序:

ServiceReference1.MyContractClient proxy = new ConsoleApplication1.ServiceReference1.MyContractClient("basic");

ServiceReference1.MyContractClient proxy = new ConsoleApplication1.ServiceReference1.MyContractClient("WSHttpBinding_IMyContract");

其中basic和WSHttpBinding_IMyContract为两种不同形式的服务在客户端的Endpoint.Name。

之前默认WSHttpBinding_IMyContract,现在由于存在多个Endpoint,则需要显示指定。

现指定为basic。再次运行,结果:

WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()
WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()
WcfServiceLibrary1.MyContract()
Counter = 1
WcfServiceLibrary1.Dispose()

其结果与PerCall是相同的。

通过SessionId可以获得Instance的SessionId

使用Per-Session方式可以通过设置SessionMode属性(允许、必须、不允许三种枚举)。

SessionMode:Gets or sets a value that indicates whether a session is required by the contract.

SessionMode 枚举http://msdn2.microsoft.com/zh-cn/library/system.servicemodel.sessionmode.aspx

Allowed(允许)
Specifies that the contract supports sessions if the incoming binding supports them.

如果绑定支持Session的话,则让其支持,否则按照可以支持的方式,比如PerCall的方式进行支持。

Required(必须)
Specifies that the contract requires a sessionful binding. An exception is thrown if the binding is not configured to support session.

指定契约必须使用Sessionful的方式。如果不支持,则抛出异常。

NotAllowed(不允许)
Specifies that the contract never supports bindings that initiate sessions.

指定不能使用Sessionful的方式。作者推荐是用NotAllowed的时候仅用PerCall方式。

刚才由于我添加了basic的方式,因为默认选中Allowed,因此刚才的之所以结果与PerCall相同,是因为它,下面我将其修改为Required。

[ServiceContract]

修改为

[ServiceContract(SessionMode=SessionMode.Required)]

结果为一个运行时错误:

System.InvalidOperationException: Contract requires Session, but Binding 'BasicHttpBinding' doesn't support it or isn't configured properly to support it.
   at System.ServiceModel.Description.DispatcherBuilder.BuildChannelListener(StuffPerListenUriInfo stuff, ServiceHostBase serviceHost, Uri listenUri, ListenUriMode listenUriMode, Boolean supportContextSession, IChannelListener& result)
   at System.ServiceModel.Description.DispatcherBuilder.InitializeServiceHost(ServiceDescription description, ServiceHostBase serviceHost)
   at System.ServiceModel.ServiceHostBase.InitializeRuntime()
   at System.ServiceModel.ServiceHostBase.OnOpen(TimeSpan timeout)
   at System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout)
   at System.ServiceModel.Channels.CommunicationObject.Open()
   at Microsoft.Tools.SvcHost.ServiceHostHelper.OpenService(ServiceInfo info)

但是若使用wsHttpBinding,但却without security and without reliable messaging也将无法维持transport-level session。

添加如下代码到app.config(Service)(指定其为无安全并且不可靠消息)

<system.serviceModel><!-- Other service Model -->

<bindings>
    
<wsHttpBinding>
      
<binding name="wsBinding">
        
<reliableSession enabled="false" />
        
<security mode="None" />
      
</binding>
    
</wsHttpBinding>
  
</bindings>
</system.serviceModel>

 

 

<endpoint address="" binding="wsHttpBinding" contract="WcfServiceLibrary1.IMyContract" bindingConfiguration="wsBinding" >


增加绑定配置为wsBinding

确保服务端程序为:
[ServiceContract(SessionMode = SessionMode.Allowed)]
(或者没设)
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]

运行结果与PerCall的结果相同。其原因也就是因为wsHttpBinding未设置安全可靠的Session。

超时

inactivityTimeout:超时时间

在连接空闲的情况下,以客户端和服务端的超时时间中最短的那个来决定是否移除Instance,若之后再调用则会抛出异常。

作者额外注明可以采用:AutomaticSessionShutdown属性。其设置为true则当proxy.Close()的时候自动关闭Session,设为false的时候则只有在服务端将服务关闭才会关闭Session。

但是,若将其修改为NotAllowed

则结果与PerCall相同(手动写为PerSession)。(不管服务配置如何,它总会是PerCall。因为TCP和IPC协议总是维持transport level,你不能将它们配置SessionMode.NotAllowed,它们会在服务载入时进行验证。作者建议是“在选择使用SessionMode.NotAllowed的同时,将服务配置为PerCall”。)

Singleton Service

Singleton,顾名思义就是仅有一个Instance,供所有客户端调用。

在说明问题之前先修改上面的例子:

//[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
//[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
为:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]

Tester中:

static void Main(string[] args)
{
    ServiceReference1.MyContractClient proxy 
= new ConsoleApplication1.ServiceReference1.MyContractClient("WSHttpBinding_IMyContract");
    proxy.Count();
    proxy.Count();
    proxy.Count();
    Console.WriteLine(proxy.Endpoint.Name);
    Console.WriteLine(proxy.InnerChannel.SessionId);
    proxy.Close();
    Console.WriteLine(proxy.Endpoint.Name);
    Console.WriteLine(proxy.InnerChannel.SessionId);
    Console.WriteLine(
"_________________________________________________________");
    Console.ReadKey(); 
    ServiceReference1.MyContractClient proxy1 
= new ConsoleApplication1.ServiceReference1.MyContractClient("WSHttpBinding_IMyContract"); 
    proxy1.Count();
    proxy1.Count();
    proxy1.Count();
    Console.WriteLine(proxy1.Endpoint.Name);
    Console.WriteLine(proxy1.InnerChannel.SessionId);
    proxy1.Close();
    Console.WriteLine(proxy1.Endpoint.Name);
    Console.WriteLine(proxy1.InnerChannel.SessionId);
    Console.WriteLine(
"_________________________________________________________");
    Console.ReadKey(); 
    ServiceReference1.MyContractClient proxy2 
= new ConsoleApplication1.ServiceReference1.MyContractClient("basic"); 
    proxy2.Count();
    proxy2.Count();
    proxy2.Count();
    Console.WriteLine(proxy2.Endpoint.Name);
    Console.WriteLine(proxy2.InnerChannel.SessionId);
    proxy2.Close();
    Console.WriteLine(proxy2.Endpoint.Name);
    Console.WriteLine(proxy2.InnerChannel.SessionId);
    Console.WriteLine(
"_________________________________________________________");
    Console.ReadKey();
}


运行的结果:(Output<Debug>)

//客户端调用前

//...
//其他代码
//...
WcfServiceLibrary1.MyContract()
//...
//其他代码
//...

//客户端调用后

//...
//其他代码
//...
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9
//...
//其他代码
//...

从Counter的值看来,多个proxy调用的是同一个Instance。

值得一提的是WcfServiceLibrary1.MyContract(),也就是构造函数的调用时间是在Service启动的时候,而PerCall与Sessionful构造函数调用时间都是在proxy调用之时。而且只有当Host关闭的时候才会Dispose()。

MSDN:

If the InstanceContextMode value is set to Single the result is that your service can only process one message at a time unless you also set the ConcurrencyMode value to Multiple.

也就是说除非将服务设置成多线程的,否则在一个时间只能处理一个消息请求。

从HOST端控制SingletonInstance

方式一:

修改代码:

static void Main(string[] args)
{
    Uri baseAddress 
= new Uri(http://localhost:8731/Design_Time_Addresses/WcfServiceLibrary1/MyContract/);
    WcfServiceLibrary1.MyContract singleInstance = new WcfServiceLibrary1.MyContract();
    singleInstance.CountProperty 
= 200;//设置初始值200
    System.ServiceModel.ServiceHost host = new System.ServiceModel.ServiceHost(singleInstance);
    host.AddServiceEndpoint(
typeof(WcfServiceLibrary1.IMyContract), new WSHttpBinding(), baseAddress);
    host.Open();
    Console.WriteLine(
"host state = {0}", host.State.ToString()); 
    ServiceReference1.MyContractClient proxy 
= new ConsoleApplication1.ServiceReference1.MyContractClient("WSHttpBinding_IMyContract");
    proxy.Count();
    proxy.Count();
    singleInstance.CountProperty 
= 100;//修改singletonInstance.CountProperty为100
    proxy.Count();
    Console.WriteLine(proxy.Endpoint.Name);
    Console.WriteLine(proxy.InnerChannel.SessionId);
    proxy.Close();
    Console.WriteLine(proxy.Endpoint.Name);
    Console.WriteLine(proxy.InnerChannel.SessionId);
    Console.WriteLine(
"_________________________________________________________");
    Console.ReadKey(); 
    host.Close();
}

public class MyContract
添加属性:
public int CountProperty
{
    
set
    {
        count 
= value;
    }
}

无须启动WcfServiceLibrary1(将用外部Host进行启动)直接运行客户端程序:

结果:

//客户端调用前

//...
//其他代码
//...
WcfServiceLibrary1.MyContract()
//...
//其他代码
//...

//客户端调用后

//...
//其他代码
//...
Counter = 201
Counter = 202
Counter = 101
//...
//其他代码
//...

方式二:

通过OperationContext.Current.Host 来获取当前进程的host

此方法我暂时未调出来,大家有想到或做到的麻烦告诉我!

 

ServiceHost host = OperationContext.Current.Host as ServiceHost;
MyContract singletonInstance 
= host.SingletonInstance as MyContract;
if (singletonInstance != null)
    singletonInstance.CountProperty 
= DateTime.Now.Second; 

 

先假设以上方法可行吧。

现在我遇到的问题:

1、OperationContext.Current这里的(MSDN:Gets or sets the execution context for the current thread.
)current thread是指我ConsoleApplication也就是Client的线程呢,还是指Service端的线程?(据我常理分析应该是服务端的线程)。理论上我应该在Count()方法内写这段代码,但是问题又涉及到通过Client端进行调用时是使用proxy,这样真正的情况会是怎样呢?

2、因此我又写了一个在一个ConsoleApplication里完成服务的代码,此时在host.SingletonInstance的确是被赋值了(不再是null了),但是紧接着我调用OperationContext.Current却发现其为null,也就是说这里的OperationContext并没有被赋值。继而将其转移到同在一个程序内的MyContract.Count()方法中,但是其值仍然为空,因此此法再度失效。

希望作为高手的您能够提供一个使用OperationContext.Current的场景。什么样算是OperationContext的当前线程?

 

推荐阅读:

http://www.microsoft.com/china/MSDN/library/Windev/WindowsVista/WCFEssentials.mspx?mfr=true

posted on 2008-07-18 20:53  volnet(可以叫我大V)  阅读(1811)  评论(1编辑  收藏  举报

使用Live Messenger联系我
关闭