傳說中的WCF(11):會話(Session)
在標題中我加了一個大家都很熟悉的單詞——Session,熟吧?玩過Web開發的朋友肯定在夢中都會見到她。
在Web中為什么要會話呢?畢竟每個用戶在一個Web應用中可能不止進行一次操作,比如,某二手飛機交易網站,用戶A登陸后,可能他會修改他的個人 信息,他也有可能看好了一架二手飛機,打算入手,就把商品放到他的“購物車”中,這些過程中,都會產生許多與用戶A相關的數據,這些數據只是對A有效,而 當用戶B登陸后,對于B,又會有他自己的數據,總的一句話就是,每個客戶端在服務器上都有其的獨立數據存儲區,互不相干,就好像A和服務器在單獨談話一樣,所以叫會話。
在WCF中,會話的含義與Web中的會話概念是差不多的,就是客戶端與服務器端在“私聊”,這便是存在會話的調用;那么,沒有會話的調用呢,就是“群聊”;通信過程中,數據都以明文顯示,不進行加密保護,這叫“裸*聊”。
好了,現在大家對于會話,肯定有點理解了。但是,會話是看不見摸不著的,怎么通過實例來檢驗它呢?
下面,我們寫一個例子,看看在不支持會話的綁定上連續調用兩個有關聯的代碼,會發生什么情況。
namespace Server
{
[ServiceContract]
public interface IService
{
[OperationContract(IsOneWay = true)]
void SetValue(int n);
[OperationContract]
int GetValue();
}
}
namespace Server
{
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class MyService : IService
{
public MyService()
{
Console.WriteLine("-------------------------------");
Console.WriteLine("{0} - 服務被實例化。", DateTime.Now.ToLongTimeString());
}
// 在析構函數中也輸出信息
~MyService()
{
Console.WriteLine("{0} - 服務實例被釋放。", DateTime.Now.ToLongTimeString());
Console.WriteLine("--------------------------------");
}
/// <summary>
/// 私有字段
/// </summary>
private int mValue = int.MinValue;
public void SetValue(int n)
{
this.mValue = n;
Console.WriteLine("會話ID:{0}", OperationContext.Current.SessionId);
}
public int GetValue()
{
Console.WriteLine("會話ID:{0}", OperationContext.Current.SessionId);
return this.mValue;
}
}
}
在服務類中,我們分別在構造函數和析構函數中輸出一些內容,以便于在運行時看到服務什么時候被實例化,什么時候被釋放。同時,在調用每個操作協定時,我們也輸出當前會話的ID,如果空就說明當前沒有啟用會話。
接著,實現服務主機,我們使用不支持的Bindding。
namespace Server
{
class Program
{
static void Main(string[] args)
{
Console.Title = "WCF服務器端";
using (ServiceHost host = new ServiceHost(typeof(MyService), new Uri("http://127.0.0.1:1211/sv")))
{
// 綁定
BasicHttpBinding binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.None;//不需要安全模式
host.AddServiceEndpoint(typeof(IService), binding, "/ep");
// 服務元數據
ServiceMetadataBehavior mb = new ServiceMetadataBehavior();
mb.HttpGetEnabled = true;
mb.HttpGetUrl = new Uri("http://127.0.0.1:8008/meta");
host.Description.Behaviors.Add(mb);
host.Opened += (sender, arg) =>
{
Console.WriteLine("服務已啟動。");
};
try
{
host.Open();//打開服務
Console.Read();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
}
然后,實現客戶端,為了使演示操作更方便,客戶端使用wpf項目,界面大致如下圖所示。

服務有兩個操作,從前面服務類的定義我們知道,我在服務類內部定義了一個私有字段,而公開的兩個操作協定,分別是設置這個值和獲取這個值。
namespace Client
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ServiceReference1.ServiceClient client = null;
public MainWindow()
{
InitializeComponent();
client = new ServiceReference1.ServiceClient();
// 關閉通道
this.Closing += (frmsender, frmargs) => client.Close();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
int v;
if (!int.TryParse(this.textBox1.Text, out v))
{
return;
}
client.SetValue(v);
}
private void button2_Click(object sender, RoutedEventArgs e)
{
int v = client.GetValue();
this.textBox2.Text = v.ToString();
}
}
}
我們現在要測試一下,看看先后調用這兩個方法,能不能得到我們預期的值。即如果我調用SetValue(100),那么,在調用GetValue時應該返回100,事實是不是這樣呢?
八格牙路,我沒有看到預期的值。

我輸入了100,可是取出來的是Min int,再看一看服務器端輸出了哪些信息。

很明顯,每調用一次操作,服務類就被實例化一次,意思就是:調用SetValue時是實例A,而調用GetValue時可能是實例B了,所以,私有字段的值沒有被保存。
那么,如何證明兩次調用的操作不屬于同一個服務實例呢?還記得GetHashCode嗎?對的,只要在內存中不是同一個實例的,其哈希值肯定不同。是不是這樣呢,我們把上面的服務代碼改一下。
public void SetValue(int n)
{
this.mValue = n;
Console.WriteLine("------------------------");
Console.WriteLine("當前實例的哈希值:{0}", this.GetHashCode().ToString("x"));
Console.WriteLine("會話ID:{0}", OperationContext.Current.SessionId);
}
public int GetValue()
{
Console.WriteLine("------------------------");
Console.WriteLine("當前實例的哈希值:{0}", this.GetHashCode().ToString("x"));
Console.WriteLine("會話ID:{0}", OperationContext.Current.SessionId);
return this.mValue;
}
那么,這次又會發生什么事呢,看結果。

這個結果證實了我之前的推斷,先后調用的兩個方法不是同一個實例的。
那么,如果啟用了會話,結果又會如何呢?
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IService
{
。。。。。
}
在定義服務協定的時候,設置SessionMode可以讓服務要求客戶端啟用會話。
接下來,要使用支持會話的綁定。
// 綁定
//BasicHttpBinding binding = new BasicHttpBinding();
//binding.Security.Mode = BasicHttpSecurityMode.None;//不需要安全模式
//host.AddServiceEndpoint(typeof(IService), binding, "/ep");
NetTcpBinding binding = new NetTcpBinding();
binding.Security.Mode = SecurityMode.None;
host.AddServiceEndpoint(typeof(IService), binding, "net.tcp://127.0.0.1:2377/ep");
把客戶端的服務引用更新一下。然后看看這回會不會達到我們的目的。


怎么樣,高興吧?終于看到我們要的效果了,我輸入了100,取出來的還是100,這一回從服務器端的輸出看,服務類只被實例化了一次,而且看看兩個哈希值是相同的,這證明了確實是同一個實例,同時,我們也看到了兩次調用的會話ID是一樣的,這也說明了,客戶端兩次調用都基于同一個會話進行的,這么一來,輸進去的100就能順利取出來了。
你不妨多開幾個客戶端來試試。




看到那個不同的會話ID,哈希值和實例化時間了吧?這表明了:服務器獨立維護著與每個客戶端的會話。
下面還要對我們的解決方案進行修改。
服務器端,把服務協定改為:
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IService
{
[OperationContract(IsOneWay = true, IsInitiating = true, IsTerminating = false)]
void SetValue(int n);
[OperationContract]
int GetValue();
[OperationContract(IsInitiating = false, IsTerminating = true)]
void EndSession();
}
在服務類中增加對EndSession方法的實現。
public void EndSession()
{
Console.WriteLine("會話結束。");
}
看到變化了嗎?
我們在使用OperationContractAttribute定義操作協定時,設置了兩個屬性:
a、IsInitiating:如果為真,則當調用該操作時就啟用會話。
b、IsTerminating:如果為真,則說明當該用該操作時終結會話。
所以,上面的例子是,當調用SetValue時開始會話,當調用EndSession方法后會話結束,在選擇作為結束會話的方法時,最好使用返回值為void或者單向通訊(One Way)的方法,這樣,不用等待客戶結束才結束會話,因為單向通訊,不需要向客戶端回復消息,因為它被調用后就可以馬上終止會話了。
ServiceReference1.ServiceClient client = null;
public MainWindow()
{
InitializeComponent();
client = new ServiceReference1.ServiceClient();
// 關閉通道
//this.Closing += (frmsender, frmargs) => client.Close();
}
然后,在調用完GetValue后馬上就EndSession,看看這回又會發生什么。
private void button2_Click(object sender, RoutedEventArgs e)
{
int v = client.GetValue();
this.textBox2.Text = v.ToString();
client.EndSession();
}
這樣,一旦三個方法調用完之后,會話結束,服務實例也解放了。

再說明一下,IsInitiating = true的操作開始新會話,IsTerminating = true的操作終結會話。

浙公網安備 33010602011771號