跟我一起學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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM