簡介
有許多聰明的辦法可以解決HTTP協議的無狀態問題,例如對每個請求重復發送應用程序數據包、使用HTTP認證機制來將請求映射到特定的用戶、使用CookIE來存儲一系列請求的狀態等。在ASP.net技術中提供了一個非常有效的方案來保持狀態,該方案隱藏了所有高難度的,具有挑戰性的工作的細節,用戶只需簡單地使用System.Web.SessionState.HttpSessionState類。同時,你也可以像在ASP.Net程序地Web頁面(.ASPx)中一樣在Web Service的方法中使用這個類,只有一點小小的不同。
ASP.Net的Session對象概述
ASP.net的Session狀態信息在根本上通過兩個機制保持。其一是使用Cookie。當客戶端發送一個請求到服務器端時,服務器將發回一個附加HTTP Set-Cookie頭的響應信息,而Cookie的值就是以鍵/值對的形式保存在該信息裡邊。在對同一服務器的所有的同步請求中,客戶端在HTTP Cookie頭中發送CookIE鍵/值對。然後服務器可以將並發的請求同初始的請求對應起來。ASP.Net使用一個保存會話的ID的cookIE來保持會話狀態。該ID標識被用來為特定的用戶找到與其對應的HttpSessionState類的實例。類HttpSessionState僅僅提供了一個通用的數據集,你可以在其中保存你需要的任何信息。
ASP.net用來保持狀態的另外一個機制工作時無須使用CookIE。一些浏覽器被用戶設置為禁止使用cookie或者干脆就不支持CookIE,ASP.net提供了一種機制來解決這個問題,它的主要原理是將一個請求重定向到一個包含ASP.net狀態ID的URL。當該請求被接受到時,這個嵌在URL中的ID被截取下來,服務器通過該ID找到合適的HttpSessionState類的實例。這種方式在HTTP協議的使用GET方式的請求中工作的很好,但是在.Net的XML Web Service代碼中會出現問題。
必須指出的是,有些時候把信息直接存儲在CookIE中要比存儲在Session中更好。避免使用Session可以節省服務器資源,而且你也無須考慮一些煩人的問題,比如定位一個特定的Session對象、Session對象因為請求的長時間的延遲而被移除或者在服務器上沒必要地保留直到過期。然而,如果你有一些包含你不希望與你提供的服務的使用者共享的執行信息,或者有一些你不希望通過未加密的信道傳輸的私有數據,或者你認為將這些數據插入HTTP協議頭中是不切實際的,那麼你就應該使用ASP.net中的HttpSessionState,它將使你輕松解決這些問題。HttpSessionState類返回一個索引鍵,用以將一個特定的用戶映射到一個為該用戶保存信息的HttpSessionState類的實例。總之,無論是ASP.net的HttpSessionState類還是HTTP的CookIE都可以在ASP.Net Web Service中使用。
在XML Web Service中使用基於HTTP的機制來實現狀態保持的理由
在SOAP請求中有許多方法來保持狀態。一個切實可行的方法就是在SOAP頭中包含一些像ASP中的會話ID的信息,然而問題在於你不得不:
1) 仍然要自己編寫服務器端代碼。
2) 確信你的客戶會像對待HTTP CookIE一樣對待你的包含會話ID的SOAP頭並且將它附加到每個請求中回傳給你。
當然有很多時候使用SOAP頭的方法會很方便,但是也有很多時候還不如使用基於HTTP協議的方法。
很容易在ASP.net中使用Session來保持狀態信息,HttpSessionState類為你封裝了存儲Session狀態的細節問題。絕大多數的客戶端已經能夠明白他們必須返回服務器設置的cookIE,而且HttpSessionState類也支持在SOAP通信中常用的底層傳輸。(問題)因此,很明顯,使用ASP.Net的Session機制會是滿足狀態控制要求的明智的選擇。
使服務器支持Session
在ASP.Net中,對Web方法的狀態支持默認是關閉的,你必須為每個要使用Session狀態的Web方法顯式地激活Session支持。激活Session支持的方法是添加一個EnableSession選項到你的函數的WebMethod屬性中,並且將其值設置為true。下面的代碼演示了如何激活Session並且在方法中訪問Session狀態信息。
[VB.Net]
_
Public Function IncrementSessionCounterX() As Integer
Dim counter As Integer
If Context.Session("Counter") Is Nothing Then
counter = 1
Else
counter = Context.Session("Counter") + 1
End If
Context.Session("Counter") = counter
Return counter
End Function
如你所料,如果你為一個Web方法激活了Session支持,並不意味著其它的Web方法的Session支持也被激活。事實上,如果Web方法的EnableSession選項沒有被顯式地設置為true,那麼Context.Session屬性的值將是null。
假設通過設置web.config文件禁止session,那麼即使你在WebMethod屬性中使用了EnableSession選項,Context.Session的值也將一直是null。web.config文件中的/configuration/system.web/sessionState項有一個mode參數,它決定了你的ASP.net程序使用何種方法來保持Session狀態。該參數默認設置為“InProc”,這時HttpSessionState對象將簡單地保存在ASP.net進程的內存區。如果被設置為“Off”,那麼ASP.Net程序的Session支持就被關閉了。
從服務器端看來,ASP.net的session狀態的有效范圍僅僅是某一個給定的ASP.net應用程序,這就意味著一個HttpSessionState類的實例只能被一個特定用戶向某一個虛擬目錄發出的所有Session被激活的ASP.net請求所使用,也就是說,使用同一個會話ID的向其它的虛擬目錄的請求將導致ASP.net不能找到對應的session對象——因為會話ID不是為該ASP.net應用程序設定的。ASP.Net並不區分對ASPX和ASMX文件的請求,直到該請求需要使用Session對象,因此,理論上你可以在一個Web方法調用和一個普通的ASPX文件之間共享Session狀態信息。然而,我們將看到也有些客戶端的問題使這個想法變得不那麼容易
當設置一個HTTP cookie,你可以指定其過期時間。過期時間指定在多久的時間內,客戶端應該將該cookIE回傳給服務器。如果一個cookie沒有被設置過期時間,那麼它僅僅在該進程處理請求的時間內被回傳。例如,IE將一直回傳cookIE,除非你關閉了浏覽器的特定窗口。ASP.Net的用來保存會話ID的CookIE沒有過期時間,因此,如果一台客戶機上的多個進程向你的服務器上發送HTTP請求,它們也不會共享同一個HttpSessionState對象,甚至兩個進程同時運行也是這樣。
如果你要處理來自同一個進程的並發的Web Service調用,那麼這些請求將在服務器上被排序,從而使得在某一時刻只有一個請求被執行。ASP.Net的Web Service不像普通.ASPX頁面,支持允許多請求的並發進程的對HttpSessionState對象的只讀訪問,所有Session被激活的Web方法調用都具有read/write訪問的權限,因此必須對之進行排序。
客戶端的問題
在你的WebService中成功的使用HttpSessionState的功能事實上依賴於對用戶的一些假設。首先,也是最重要的一點,如果你是用默認的HTTP Cookie模式來保存Session狀態,你的客戶端就必須支持cookie;如果你是用無cookIE的機制來支持Session,那麼你的客戶端必須能夠並且願意重定向到一個新的URL,該URL由原來的URL中插入會話ID而得到。結果將表明,這並不是一個無足輕重的問題,它關系到你能否成功地部署你的程序。
所有工作都依賴於浏覽器
如果你是用Microsoft Visual Studio.net來開發ASP.Net Web Service應用程序,那麼默認的調試方法就是打開IE訪問你的.asmx文件。通常,系統將提供一個可以調用你的Web方法的友好的界面,這是一個調試你的Web Service代碼的很好的途徑。如果你已經將Web方法的EnableSession選項設置為true,它被非常漂亮地支持,甚至如果你打開了無cookIE的Session支持,客戶端浏覽器也可以完美地完成這項工作,你的Session對象將如你所願地工作。
然而,大多數的Web Service請求不是來自浏覽器,而是來自應用程序中的Web引用。我們如何使用.Net框架的“添加Web引用”的特性呢?讓我們來看一看。
添加Web引用的問題
我將使用我們前面看到的代碼段來創建一個簡單的XML Web Service。記起來了吧?這個Web方法被稱作IncrementSessionCounter,它僅僅是簡單地把一個整數存儲在HttpSessionState對象中,然後每次調用則將它加1,並且返回當前值。從客戶端浏覽器我們可以看到這個數字的值隨著調用次數的增加而增加。
下一步,我創建了一個簡單的WinForm應用程序,並且將上述的Web Service添加到Web引用中。下面就是調用我的Web Service的代碼:
' 這裡並沒有與Session打交道
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim proxy As New localhost.Service1()
Dim ret As Integer
ret = proxy.IncrementSessionCounter()
Label1.Text = "Result: " & CStr(ret)
End Sub
當我第一次調用Web Service時,一切正常,Web方法返回1,這就是那個Session變量的應有的初始值。現在我點擊Button1來再次調用這個Web方法,我希望看到的返回值是2。可惜的是,無論我點擊多少次Button1,返回值一直都是1。
你也許會懷疑原因就是我每次都創建了一個新的proxy類的實例去調用Web方法,因此每次我點擊按鈕,都會丟失上一次調用時的cookIE。不幸的是,即使你將proxy類的初始化代碼移到窗體的構造函數中,然後對每次Web方法調用使用同一個proxy類的實例,你還是不可能看到返回值有增加的跡象。
問題在於cookie。Web Service代碼並未從調用請求中發現有效的會話ID,因此它每次被調用都創建一個全新的HttpSessionState對象,並且返回它的初始值1。因為作為客戶端的proxy類是從類System.Web.Service.Protocols.SoapHttpClIEntProtocol繼承的,它不包含System.Net.CookieContainer類的實例,因此,沒有地方來存放返回的cookIE。為了解決這個問題,我對代碼做了如下一些修改:
' 使用了ASP.Net的session
' 但是並不是無CookIE的session.
Private CookIEs As System.Net.CookIEContainer
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim proxy As New localhost.Service1()
Dim ret As Integer
' 為proxy類設置cookIE容器
If CookIEs Is Nothing Then
CookIEs = New System.Net.CookIEContainer()
End If
proxy.CookieContainer = CookIEs
ret = proxy.IncrementSessionCounter()
Label1.Text = "Result: " & CStr(ret)
End Sub
現在代碼工作正常了!每點擊一次Button1,我都可以看到返回值增加1。注意到我並不是在函數中聲明變量CookIEs的,它是窗體類的一個私有成員,因為如果希望每次都返回同一個會話ID給服務器的話,就必
須在每次請求中使用CookieContainer類的同一個實例。這就解釋了為什麼SoapHttpClientProtocol類默認不自動地設置的cookie容器。正應為此,你可以在多個SoapHttpClientProtocol類的實例中共享一個cookie容器,而不是為其每個實例自動地創建一個新的cookIE容器。
無cookIE的Session
從Web Service的開發者的角度來看,你可以想到相當多的人在試圖使用你的Web服務時忘記在客戶端代理類中添加Cookie容器。聰明的開發者或許靈光一閃,就會發現無cookie的Session應該可以出色地解決這個問題。如果將web.config文件中sessionState元素的cookIEless參數設置為“true”,你將會發現,通過浏覽器界面調用Web方法時,session變量工作正常,但是如果你在Visual Studio.Net中通過“添加Web引用”來調用它時,依然存在著一些問題。
為了研究無cookie的session,我決定使用上面已經使用過的代碼,看看它能否在session狀態被設置為cookIEless的服務器環境中能否工作正常。我也不想費心去刪除cookIE容器的相關代碼,因為我希望得到能在兩種session狀態下都正常工作的代碼。作為一個天生的樂觀主義者,我一個字也不改就直接運行它。令人失望的事發生了——不過也不是完全沒有想到,我不得不面對這個異常:
An unhandled exception of type 'System.Net.WebException' occurred in system.web.services.dll
Additional information: The request failed with the error message:
--
Object moved to here.
發生了什麼呢?原來HTTP請求收到的不是“200 OK”響應。如果你熟悉HTTP協議,你或許可以從響應中的HTML代碼中發現這是一個“302 Found”響應,這意味著該請求被重定向到超鏈接中指定的地址。返回Html代碼是很明智的,這樣如果一個浏覽器因為某些原因不支持重定向的話,它可以把代碼顯示出來,或者在重定向過程中顯示這些代碼直到重定向完成。注意到超鏈接中包含了一個有趣的字符串“(l2z3psnhh2cf1oahmai44p21)”,顯然,我們可以推斷這就是ASP.Net的會話ID,它被嵌入了我們要重定向到的位置的URL中。在客戶端代理中,我們需要做的僅僅是重新發送請求到這個新的URL。
無須再在Win32 WinInet API編程中跋涉,我們可以直接找到proxy類的一個屬性允許自動重定向。用外行人的說法,就是如果我們接收到一個“302 Found”響應,就直接將請求重新發送到相應中HTTP位置頭所指示的URL。當Visual Studio.Net的智能提示顯示proxy類的AllowAutoRedirect屬性時,我感到這東西真是機靈得可愛。我馬上就在代碼中加上如下一行:
proxy.AllowAutoRedirect = True
我認為這仍然比創建一個CookIEContainer類並關聯到proxy類要容易得多,於是我又一次運行程序。很不幸,我遭遇了如下異常(為了簡潔起見有所刪節):
An unhandled exception of type 'System.InvalidOperationException' occurred
in system.web.services.dll
Additional information: ClIEnt found response content type of 'text/Html; charset=utf-8',
but expected 'text/XML'.
The request failed with the error message: …
如果你看到錯誤消息的內容,你會發現你所看到的HTML頁面跟你浏覽.ASMX文件的頁面一樣。問題是,為什麼當我傳送XML(以SOAP封裝了的形式)到Web Service服務器時它返回的卻是HTML代碼?結果證實,你並沒有在SOAP封裝中發送HTTP POST請求,而僅僅發送了一個簡單的沒有內容的HTTP GET請求,因此你的Web Service服務端理所當然地假設這個請求來自浏覽器,於是它返回普通的Html響應。為什麼會這樣呢?
如果你了解HTTP協議,你會發現一個HTTP客戶端在收到“302 Found”響應時發送HTTP GET請求到響應中指定的地址是合情合理的,即使初始請求是HTTP POST。這種方式下浏覽器工作得很好,因為開始幾乎所有的請求都是HTTP GET類型的,只有當你試圖傳遞數據到一個URL時,才會出現上述失敗的結果。
理由是在傳送的數據中可能包含潛在的敏感數據,因此你需要確認是否用戶真的想向新的資源傳送數據。顯然如果你轉向基於重定向設置的新地址,你就沒能確認用戶是否真的允許將他們的數據發送到新的地址。因此數據並沒有被發送,而代之以簡單的HTTP GET請求。
我對代碼做了如下修改,捕獲“302 Found”異常,提示用戶同意重定向他們的請求,然後再次在新的位置調用我的Web方法。
' 同時使用基於Cookie和CookIE的Session
Private CookIEs As System.Net.CookIEContainer
Private webServiceUrl as Uri
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim proxy As New localhost.Service1()
Dim ret As Integer
' 設置proxy類的CookIE容器
If CookIEs Is Nothing Then
CookIEs = New System.Net.CookIEContainer()
End If
proxy.CookieContainer = CookIEs
' 設置proxy類的URL
If webServiceUrl Is Nothing Then
webServiceUrl = New Uri(proxy.Url)
El
se
proxy.Url = webServiceUrl.AbsoluteUri
End If
Try
ret = proxy.IncrementSessionCounter()
Catch we As WebException
' 如果我們想檢測HTTP狀態碼
' 那麼就需要一個HttpWebResponse類的實例
If TypeOf we.Response Is HttpWebResponse Then
Dim HttpResponse As HttpWebResponse
HttpResponse = we.Response
If HttpResponse.StatusCode = HttpStatusCode.Found Then
' 這是一個“302 Found”響應,提示用戶是否進行重定向
If MsgBox(String.Format(redirectPrompt, _
HttpResponse.Headers("Location")), _
MsgBoxStyle.YesNo) = _
MsgBoxResult.Yes Then
' 用戶選擇Yes,重新嘗試新的URL
webServiceUrl = New Uri(webServiceUrl, _
HttpResponse.Headers("Location"))
Button1_Click(sender, e)
Return
End If
End If
End If
Throw we
End Try
Label1.Text = "Result: " & CStr(ret)
End Sub
現在ASP.Net的Session工作正常了。在你的應用程序中,你可以根據情況自行決定是否提示用戶重定向HTTP POST請求。舉一個例子,如果你正在調用一個Web Service,你也許就不希望出現任何可見的對話框。
這樣看來,要使ASP.Net的Session完全正常地工作還真不是很容易。但是,應該意識到上面的代碼所展示的原理在其他情況下一樣的有用。例如,任何平台上的任何Web Service,只要它使用HTTP cookie,都需要一個cookIE容器,類似地,也許有很多其他的原因導致當你向一個Web Service服務器發送請求時收到“302 Found”響應。在一個復雜的應用程序中調用Web Service時,可能有許多特殊的情形需要你去處理,cookIE和重定向的問題就是兩種這樣的情形,你應該將之作為你的Web Service調用代碼中最基本的部分。
結 論
在你調用Web方法的過程中,ASP.Net對狀態保持是非常有用的,你必須意識到,當你使用手邊的浏覽器界面測試你的Web Service時,你並沒有面對客戶端程序必須處理的問題。幸運的是,這些問題並不是很難解決。