zoukankan      html  css  js  c++  java
  • 跟我一起学WCF(8)——WCF中Session、实例管理详解

    一、引言

       由前面几篇博文我们知道,WCF是微软基于SOA建立的一套在分布式环境中各个相对独立的应用进行交流(Communication)的框架,它实现了最新的基于WS-*规范。按照SOA的原则,相对独自的业务逻辑以Service的形式进行封装,调用者通过消息(Messaging)的方式来调用服务。对于承载某个业务功能实现的服务应该具有上下文(Context)无关性,意思就是说构造服务的操作(Operation)不应该绑定到具体的调用上下文,对于任何的调用,具有什么的样输入就会对应怎样的输出。因为SOA一个最大的目标是尽可能地实现重用,只有具有Context无关性,服务才能最大限度的重用。即从软件架构角度理解为,一个模块只有尽可能的独立,即具有上下文无关性,才能被最大限度的重用。软件体系一直在强调低耦合也是这个道理。

      但是在某些场景下,我们却希望系统为我们创建一个Session来保留Client和Service的交互的状态,如Asp.net中Session的机制一样,WCF也提供了对Session的支持。下面就具体看看WCF中对Session的实现。

    二、WCF中Session详细介绍

     2.1 Asp.net的Session与WCF中的Session

      在WCF中,Session属于Service Contract的范畴,并在Service Contract定义中通过SessionModel参数来实现。WCF中会话具有以下几个重要的特征:

    • Session都是由Client端显示启动和终止的。

      在WCF中Client通过创建的代理对象来和服务进行交互,在支持Session的默认情况下,Session是和具体的代理对象绑定在一起,当Client通过调用代理对象的某个方法来访问服务时,Session就被初始化,直到代理的关闭,Session则被终止。我们可以通过两种方式来关闭Proxy:一是调用ICommunicationObject.Close 方法,二是调用ClientBase<TChannel>.Close 方法 。我们也可以通过服务中的某个操作方法来初始化、或者终止Session,可以通过OperationContractAttribute的IsInitiating和IsTerminating参数来指定初始化和终止Session的Operation。

    • 在WCF会话期间,传递的消息按照它发送的顺序被接收。
    • WCF并没有为Session的支持保存相关的状态数据。

      讲到Session,做过Asp.net开发的人,自然想到的就是Asp.net中的Session。它们只是名字一样,在实现机制上有很大的不同。Asp.net中的Session具有以下特性:

    • Asp.net的Session总是由服务端启动的,即在服务端进行初始化的。
    • Asp.net中的Session是无需,不能保证请求处理是有序的。
    • Asp.net是通过在服务端以某种方式保存State数据来实现对Session的支持,例如保存在Web Server端的内存中。

    2.2 WCF中服务实例管理

      对于Client来说,它实际上不能和Service进行直接交互,它只能通过客户端创建的Proxy来间接地和Service进行交互,然而真正的调用而是通过服务实例来进行的。我们把通过Client的调用来创建最终的服务实例过程称作激活,在.NET Remoting中包括Singleton模式、SingleCall模式和客户端激活方式,WCF中也有类似的服务激活方式:单调服务(PerCall)、会话服务(PerSession)和单例服务(Singleton)。

    • 单调服务(Percall):为每个客户端请求分配一个新的服务实例。类似.NET Remoting中的SingleCall模式
    • 会话服务(Persession):在会话期间,为每次客户端请求共享一个服务实例,类似.NET Remoting中的客户端激活模式。
    • 单例服务(Singleton):所有客户端请求都共享一个相同的服务实例,类似于.NET Remoting的Singleton模式。但它的激活方式需要注意一点:当为对于的服务类型进行Host的时候,与之对应的服务实例就被创建出来,之后所有的服务调用都由这个服务实例进行处理。

      WCF中服务激活的默认方式是PerSession,但不是所有的Bingding都支持Session,比如BasicHttpBinding就不支持Session。你也可以通过下面的方式使ServiceContract不支持Session.

    [ServiceContract(SessionMode = SessionMode.NotAllowed)]
    

      下面分别介绍下这三种激活方式的实现。

    三、WCF中实例管理的实现

       WCF中服务激活的默认是PerSession的方式,下面就看看PerSession的实现方式。我们还是按照前面几篇博文的方式来实现使用PerSession方式的WCF服务程序。

      第一步:自然是实现我们的WCF契约和契约的服务实现。具体的实现代码如下所示:

     1 // 服务契约的定义
     2     [ServiceContract]
     3     public interface ICalculator
     4     {
     5         [OperationContract(IsOneWay = true)]
     6         void Increase();
     7 
     8         [OperationContract]
     9         int GetResult();
    10     }
    11 
    12   // 契约的实现
    13     public class CalculatorService : ICalculator, IDisposable
    14     {
    15         private int _nCount = 0;
    16 
    17         public CalculatorService()
    18         {
    19             Console.WriteLine("CalulatorService object has been created");
    20         }
    21 
    22         // 为了看出服务实例的释放情况
    23         public void Dispose()
    24         {
    25             Console.WriteLine("CalulatorService object has been Disposed");
    26         }
    27 
    28         #region ICalulator Members
    29         public void Increase()
    30         {
    31             // 输出Session ID 
    32             Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
    33             this._nCount++;
    34         }
    35 
    36         public int GetResult()
    37         {
    38             Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
    39             return this._nCount;
    40         }
    41         #endregion 
    42     }

      为了让大家对服务对象的创建和释放有一个直观的认识,我特意对服务类实现了构造函数和IDisposable接口,同时在每个操作中输出当前的Session ID。

      第二步:实现服务宿主程序。这里还是采用控制台程序作为服务宿主程序,具体的实现代码如下所示:

     1 // 服务宿主程序
     2     class Program
     3     {
     4         static void Main(string[] args)
     5         {
     6             using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
     7             {
     8                 host.Opened += delegate
     9                 {
    10                     Console.WriteLine("The Calculator Service has been started, begun to listen request..."); 
    11                 };
    12 
    13                 host.Open();
    14                 Console.ReadLine();
    15             }
    16         }
    17     }

      对应的配置文件为:

    <!--服务宿主的配置文件-->
    <configuration>
      <system.serviceModel>
          <behaviors>
              <serviceBehaviors>
                  <behavior name ="CalculatorBehavior">
                      <serviceMetadata httpGetEnabled="true"/>
                  </behavior>
              </serviceBehaviors>
          </behaviors>
      <services>
          <service name="WCFContractAndService.CalculatorService" behaviorConfiguration="CalculatorBehavior">
              <endpoint address="" binding="basicHttpBinding" contract="WCFContractAndService.ICalculator"/>
          <host>
              <baseAddresses>
                  <add baseAddress="http://localhost:9003/CalculatorPerSession"/>
              </baseAddresses>
          </host>
          </service>
      </services>
      </system.serviceModel>
    </configuration>

      第三步:实现完了服务宿主程序,接下来自然是实现客户端程序来访问服务操作。这里的客户端也是控制台程序,具体的实现代码如下所示:

     1 // 客户端程序实现
     2     class Program
     3     {
     4         static void Main(string[] args)
     5         {
     6             // Use ChannelFactory<ICalculator> to create WCF Service proxy 
     7             ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint");
     8             Console.WriteLine("Create a calculator proxy :proxy1");
     9             ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
    10             Console.WriteLine("Invoke proxy1.Increate() method");
    11             proxy1.Increase();
    12             Console.WriteLine("Invoke proxy1.Increate() method again");
    13             proxy1.Increase();
    14             Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
    15 
    16             Console.WriteLine("Create another calculator proxy: proxy2");
    17             ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
    18             Console.WriteLine("Invoke proxy2.Increate() method");
    19             proxy2.Increase();
    20             Console.WriteLine("Invoke proxy2.Increate() method again");
    21             proxy2.Increase();
    22             Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
    23 
    24             Console.ReadLine();
    25         }
    26     }

      客户端对应的配置文件内容如下所示:

    <!--客户端配置文件-->
    <configuration>
        <system.serviceModel>
            <client>
                <endpoint address="http://localhost:9003/CalculatorPerSession"
                          binding="basicHttpBinding"
                          contract="WCFContractAndService.ICalculator" name="HttpEndPoint"/>
            </client>
        </system.serviceModel>
    </configuration>

      经过上面三步,我们就完成了PerSession方式的WCF程序了,下面看看该程序的运行结果。

      首先,以管理员权限运行服务寄宿程序,运行成功后,你将看到如下图所示的画面:

      接下来,运行客户端对服务操作进行调用,运行成功后,你将看到服务宿主的输出和客户端的输出情况如下图所示:

      从客户端的运行结果可以看出,虽然我们两次调用了Increase方法来增加_nCount的值,但是最终的运行结果仍然是0。这样的运行结果好像与我们之前所说的WCF默认Session支持矛盾,因为如果WCF默认的方式PerSession的话,则服务实例是和Proxy绑定在一起,当Proxy调用任何一个操作的时候Session开始,从此Session将会与Proxy具有一样的生命周期。按照这个描述,客户端运行的结果应该是2而不是0。这里,我只能说运行结果并没有错,因为有图有真相嘛,那到底是什么原因导致客户端获得_nCount值是0呢?其实在前面已经讲到过,并不是所有的绑定都是支持Session的,上面程序的实现我们使用的basicHttpBinding,而basicHttpBinding是不支持Session方式的,所以WCF会采用PerCall的方式创建Service Instance,所以在服务端中对于每一个Proxy都有3个对象被创建,两个是对Increase方法的调用会导致服务实例的激活,另一个是对GetResult方法的调用导致服务实例的激活。因为是PerCall方式,所以每次调用完之后,就会对服务实例进行释放,所以对应的就有3行服务对象释放输出。并且由于使用的是不支持Session的binding,所以Session ID的输出也为null。所以,上面WCF程序其实是PerCall方式的实现。

      既然,上面的运行结果是由于使用了不支持Session的basicHttpBinding导致的,下面看看使用一个支持Session的Binding:wsHttpBinding来看看运行结果是怎样的,这里的修改很简单,只需要把宿主和客户端的配置文件把绑定类型修改为wsHttpBinding就可以了。

    <!--客户端配置文件-->
    <configuration>
        <system.serviceModel>
            <client>
                <endpoint address="http://localhost:9003/CalculatorPerSession"
                          binding="wsHttpBinding"
                          contract="WCFContractAndService.ICalculator" name="HttpEndPoint"/>
            </client>
        </system.serviceModel>
    </configuration>
    <!--服务宿主的配置文件-->
    <configuration>
      <system.serviceModel>
          <behaviors>
              <serviceBehaviors>
                  <behavior name ="CalculatorBehavior">
                      <serviceMetadata httpGetEnabled="true"/>
                  </behavior>
              </serviceBehaviors>
          </behaviors>
      <services>
          <service name="WCFContractAndService.CalculatorService" behaviorConfiguration="CalculatorBehavior">
              <endpoint address="" binding="wsHttpBinding" contract="WCFContractAndService.ICalculator"/>
          <host>
              <baseAddresses>
                  <add baseAddress="http://localhost:9003/CalculatorPerSession"/>
              </baseAddresses>
          </host>
          </service>
      </services>
      </system.serviceModel>
    </configuration>

      现在我们再运行下上面的程序来看看此时的执行结果,具体的运行结果如下图所示:

      从上面的运行结果可以看出,此时两个Proxy的运行结果都是2,可以看出此时服务激活方式采用的是PerSession方式。此时对于服务端就只有两个服务实例被创建了,并且对于每个服务实例具有相同的Session ID。 另外由于Client的Proxy还依然存在,服务实例也不会被回收掉,从上面服务端运行的结果也可以证实这点,因为运行结果中没有对象呗Disposable的输出。你可以在客户端显式调用ICommunicationObject.Close方法来显式关闭掉Proxy,在客户端添加对Proxy的显示关闭代码,此时客户端的代码修改为如下所示:

     1 // 客户端程序实现
     2     class Program
     3     {
     4         static void Main(string[] args)
     5         {
     6             // Use ChannelFactory<ICalculator> to create WCF Service proxy 
     7             ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint");
     8             Console.WriteLine("Create a calculator proxy :proxy1");
     9             ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
    10             Console.WriteLine("Invoke proxy1.Increate() method");
    11             proxy1.Increase();
    12             Console.WriteLine("Invoke proxy1.Increate() method again");
    13             proxy1.Increase();
    14             Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
    15             (proxy1 as ICommunicationObject).Close(); // 显示关闭Proxy
    16 
    17             Console.WriteLine("Create another calculator proxy: proxy2");
    18             ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
    19             Console.WriteLine("Invoke proxy2.Increate() method");
    20             proxy2.Increase();
    21             Console.WriteLine("Invoke proxy2.Increate() method again");
    22             proxy2.Increase();
    23             Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
    24             (proxy2 as ICommunicationObject).Close();
    25 
    26             Console.ReadLine();
    27         }
    28     }

      此时,服务对象的Dispose()方法将会调用,此时服务端的运行结果如下图所示:

      上面演示了默认支持Session的情况,下面我们修改服务契约使之不支持Session,此时只需要知道ServiceContract的SessionMode为NotAllowed即可。

    [ServiceContract(SessionMode= SessionMode.NotAllowed)] // 是服务契约不支持Session
        public interface ICalculator
        {
            [OperationContract(IsOneWay = true)]
            void Increase();
    
            [OperationContract]
            int GetResult();
        }

      此时,由于服务契约不支持Session,此时服务激活方式采用的仍然是PerCall。运行结果与前面采用不支持Session的绑定的运行结果一样,这里就不一一贴图了。

      除了通过显式修改ServiceContract的SessionMode来使服务契约支持或不支持Session外,还可以定制操作对Session的支持。定制操作对Session的支持可以通过OperationContract的IsInitiating和InTerminating属性设置。

     1  // 服务契约的定义
     2     [ServiceContract(SessionMode= SessionMode.Required)] // 显式使服务契约支持Session
     3     public interface ICalculator
     4     {
     5         // IsInitiating:该值指示方法是否实现可在服务器上启动会话(如果存在会话)的操作,默认值是true
     6         // IsTerminating:获取或设置一个值,该值指示服务操作在发送答复消息(如果存在)后,是否会导致服务器关闭会话,默认值是false
     7         [OperationContract(IsOneWay = true, IsInitiating =true, IsTerminating=false )]
     8         void Increase();
     9 
    10         [OperationContract(IsInitiating = true, IsTerminating = true)]
    11         int GetResult();
    12     }

      在上面代码中,对两个操作都设置InInitiating的属性为true,意味着调用这两个操作都会启动会话,而把GetResult操作的IsTerminating设置为true,意味着调用完这个操作后,会导致服务关闭掉会话,因为在Session方式下,Proxy与Session有一致的生命周期,所以关闭Session也就是关闭proxy对象,所以如果后面再对proxy对象的任何一个方法进行调用将会导致异常,下面代码即演示了这种情况。

     1 // 客户端程序实现
     2     class Program
     3     {
     4         static void Main(string[] args)
     5         {
     6             // Use ChannelFactory<ICalculator> to create WCF Service proxy 
     7             ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint");
     8             Console.WriteLine("Create a calculator proxy :proxy1");
     9             ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
    10             Console.WriteLine("Invoke proxy1.Increate() method");
    11             proxy1.Increase();
    12             Console.WriteLine("Invoke proxy1.Increate() method again");
    13             proxy1.Increase();
    14             Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
    15             try
    16             {
    17                 proxy1.Increase(); // session关闭后对proxy1.Increase方法调用将会导致异常
    18             }
    19             catch (Exception ex) // 异常捕获
    20             {
    21                 Console.WriteLine("在Session关闭后调用Increase方法失败,错误信息为:{0}", ex.Message);
    22             }
    23 
    24             Console.WriteLine("Create another calculator proxy: proxy2");
    25             ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
    26             Console.WriteLine("Invoke proxy2.Increate() method");
    27             proxy2.Increase();
    28             Console.WriteLine("Invoke proxy2.Increate() method again");
    29             proxy2.Increase();
    30             Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
    31 
    32             Console.ReadLine();
    33         }
    34     }

      此时运行结果也验证我们上面的分析,客户端和服务端的运行结果如下图所示:

      上面演示了PerSession和PerCall的两种服务对象激活方式,下面看看Single的激活方式运行的结果。首先通过ServiceBehavior的InstanceContextMode属性显式指定激活方式为Single,由于ServiceBehaviorAttribute特性只能应用于类上,所以把该特性应用于CalculatorService类上,此时服务实现的代码如下所示:

    // 契约的实现
        // ServiceBehavior属性只能应用在类上
        [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] // 显示指定PerSingle方式
        public class CalculatorService : ICalculator, IDisposable
        {
            private int _nCount = 0;
    
            public CalculatorService()
            {
                Console.WriteLine("CalulatorService object has been created");
            }
    
            // 为了看出服务实例的释放情况
            public void Dispose()
            {
                Console.WriteLine("CalulatorService object has been Disposed");
            }
    
            #region ICalulator Members
            public void Increase()
            {
                // 输出Session ID 
                Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
                this._nCount++;
            }
    
            public int GetResult()
            {
                Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
                return this._nCount;
            }
            #endregion 
        }

      此时运行服务宿主的输出结果如下图所示:

      从运行结果可以看出,对于Single方式,服务实例在服务类型被寄宿的时候就已经创建了,对于PerCall和PerSession方式而是在通过Proxy调用相应的服务操作之后,服务实例才开始创建的。下面运行客户端程序,你将看到如下图所示的运行结果:

      此时,第二个Proxy返回的结果是4而不是2,这是因为采用Single方式只存在一个服务实例,所有的调用状态都将保留,所以_nCount的值在原来的基础上继续累加。

    四、总结

      到这里,本文的分享就结束了,本文主要分享了WCF中实例管理的实现。从WCF的实例实现可以看出,WCF实例实现是借鉴了.NET Remoting中实例实现,然后分别分享了服务实例三种激活方式在WCF中的实现,并通过对运行结果进行对比来让大家理解它们之间的区别。

      本文所以源码:WCFInstanceManager.zip

  • 相关阅读:
    windows环境配置多个tomcat
    navicat 12 破解
    Eclipse创建Maven报异常:Could not get the value for parameter encoding for plugin......
    tomcat设置日志打印到文件中
    修改mysql数据库的休眠时间
    spring的bean标签的常用属性
    redis 安装与下载(windows版本)
    mysql设置远程可访问
    WPF非轮询方式更新数据库变化SqlDependency(数据库修改前台自动更新)
    WPF实战案例-在线程内同步集合数据到UI线程
  • 原文地址:https://www.cnblogs.com/zhili/p/WCFInstanceManager.html
Copyright © 2011-2022 走看看