简介
网络程序开发者们遇到的最普遍的问题就是如何在无状态的基于HTTP协议的交互中保持状态信息有许多聪明的办法可以解决HTTP协议的无状态问题例如对每个请求重复发送应用程序数据包使用HTTP认证机制来将请求映射到特定的用户使用Cookie来存储一系列请求的状态等在技术中提供了一个非常有效的方案来保持状态该方案隐藏了所有高难度的具有挑战性的工作的细节用户只需简单地使用SystemWebSessionStateHttpSessionState类同时你也可以像在程序的Web页面(aspx)中那样在Web Service的方法中使用这个类只有一点小小的不同
的Session对象概述
的Session状态信息是通过两个机制保持其一是使用Cookie当客户端发送一个请求到服务器端时服务器将发回一个附加HTTP SetCookie头的响应信息而Cookie的值就是以键/值对的形式保存在该信息里边在对同一服务器的所有的同步请求中客户端在HTTP Cookie头中发送Cookie键/值对然后服务器可以将并发的请求同初始的请求对应起来使用一个保存会话的ID的cookie来保持会话状态该ID标识被用来为特定的用户找到与其对应的HttpSessionState类的实例类HttpSessionState仅仅提供了一个通用的数据集你可以在其中保存你需要的任何信息
用来保持状态的另外一种机制是无须使用Cookie一些浏览器被用户设置为禁止使用Cookie或者干脆就不支持Cookie提供了一种机制来解决这个问题它的主要原理是将一个请求重定向到一个包含状态ID的URL当该请求被接受到时这个嵌在URL中的ID被截取下来服务器通过该ID找到合适的HttpSessionState类的实例这种方式在HTTP协议的使用GET方式的请求中工作的很好但是的XML Web Service代码中会出现问题
必须指出的是有些时候把信息直接存储在Cookie中要比存储在Session中更好避免使用Session可以节省服务器资源而且你也无须考虑一些烦人的问题比如定位一个特定的Session对象Session对象因为请求的长时间的延迟而被移除或者在服务器上没必要地保留直到过期然而如果你有一些包含你不希望与你提供的服务的使用者共享的执行信息或者有一些你不希望通过未加密的信道传输的私有数据或者你认为将这些数据插入HTTP协议头中是不切实际的那么你就应该使用中的HttpSessionState它将使你轻松解决这些问题HttpSessionState类返回一个索引键用以将一个特定的用户映射到一个为该用户保存信息的HttpSessionState类的实例总之无论是的HttpSessionState类还是HTTP的Cookie都可以在 Web Service中使用
为什么要在XML Web Service中使用基于HTTP的机制来实现状态保持呢?
在SOAP请求中有许多方法来保持状态一个切实可行的方法就是在SOAP头中包含一些像ASP中的会话ID的信息然而问题在于你不得不) 仍然要自己编写服务器端代码并且 ) 确信你的客户会像对待HTTP Cookie一样对待你的包含会话ID的SOAP头并且将它附加到每个请求中回传给你当然有很多时候使用SOAP头的方法会很方便但是也有很多时候还不如使用基于HTTP协议的方法
很容易在中使用Session来保持状态信息HttpSessionState类为你封装了存储Session状态的细节问题绝大多数的客户端已经能够明白他们必须返回服务器设置的cookie而且HttpSessionState类也支持在SOAP通信中常用的底层传输因此很明显使用的Session机制会是满足状态控制要求的明智的选择
使服务器支持Session
在中对Web方法的状态支持默认是关闭的你必须为每个要使用Session状态的Web方法显式地激活Session支持激活Session支持的方法是添加一个EnableSession选项到你的函数的WebMethod属性中并且将其值设置为true下面的代码演示了如何激活Session并且在方法中访问Session状态信息
[]
<WebMethod(EnableSession:=True)> _
Public Function IncrementSessionCounterX() As Integer
Dim counter As Integer
If ContextSession(Counter) Is Nothing Then
counter =
Else
counter = ContextSession(Counter) +
End If
ContextSession(Counter) = counter
Return counter
End Function
如你所料如果你为一个Web方法激活了Session支持并不意味着其它的Web方法的Session支持也被激活事实上如果Web方法的EnableSession选项没有被显式地设置为true那么ContextSession属性的值将是null
假设通过设置nfig文件禁止session那么即使你在WebMethod属性中使用了EnableSession选项ContextSession的值也将一直是nullnfig文件中的/configuration/systemweb/sessionState项有一个mode参数它决定了你的程序使用何种方法来保持Session状态该参数默认设置为InProc这时HttpSessionState对象将简单地保存在进程的内存区如果被设置为Off那么程序的Session支持就被关闭了
从服务器端看来的session状态的有效范围仅仅是某一个给定的应用程序这就意味着一个HttpSessionState类的实例只能被一个特定用户向某一个虚拟目录发出的所有Session被激活的请求所使用也就是说使用同一个会话ID的向其它的虚拟目录的请求将导致不能找到对应的session对象——因为会话ID不是为该应用程序设定的并不区分对ASPX和ASMX文件的请求直到该请求需要使用Session对象因此理论上你可以在一个Web方法调用和一个普通的ASPX文件之间共享Session状态信息然而我们将看到也有些客户端的问题使这个想法变得不那么容易实现
当设置一个HTTP cookie你可以指定其过期时间过期时间指定在多久的时间内客户端应该将该cookie回传给服务器如果一个cookie没有被设置过期时间那么它仅仅在该进程处理请求的时间内被回传例如IE将一直回传cookie除非你关闭了浏览器的特定窗口的用来保存会话ID的Cookie没有过期时间因此如果一台客户机上的多个进程向你的服务器上发送HTTP请求它们也不会共享同一个HttpSessionState对象甚至两个进程同时运行也是这样
如果你要处理来自同一个进程的并发的Web Service调用那么这些请求将在服务器上被排序从而使得在某一时刻只有一个请求被执行的Web Service不像普通ASPX页面支持允许多请求的并发进程的对HttpSessionState对象的只读访问所有Session被激活的Web方法调用都具有read/write访问的权限因此必须对之进行排序
客户端的问题在你的WebService中成功的使用HttpSessionState的功能事实上依赖于对用户的一些假设首先也是最重要的一点如果你是用默认的HTTP Cookie模式来保存Session状态你的客户端就必须支持cookie如果你是用无cookie的机制来支持Session那么你的客户端必须能够并且愿意重定向到一个新的URL该URL由原来的URL中插入会话ID而得到结果将表明这并不是一个无足轻重的问题它关系到你能否成功地部署你的程序
所有工作都依赖于浏览器
如果你是用Microsoft Visual 来开发 Web Service应用程序那么默认的调试方法就是打开IE访问你的asmx文件通常系统将提供一个可以调用你的Web方法的友好的界面这是一个调试你的Web Service代码的很好的途径如果你已经将Web方法的EnableSession选项设置为true它被非常漂亮地支持甚至如果你打开了无cookie的Session支持客户端浏览器也可以完美地完成这项工作你的Session对象将如你所愿地工作
然而大多数的Web Service请求不是来自浏览器而是来自应用程序中的Web引用我们如何使框架的添加Web引用的特性呢?让我们来看一看
添加Web引用的问题
我将使用我们前面看到的代码段来创建一个简单的XML Web Service记起来了吧?这个Web方法被称作IncrementSessionCounter它仅仅是简单地把一个整数存储在HttpSessionState对象中然后每次调用则将它加并且返回当前值从客户端浏览器我们可以看到这个数字的值随着调用次数的增加而增加
下一步我创建了一个简单的WinForm应用程序并且将上述的Web Service添加到Web引用中下面就是调用我的Web Service的代码
这里并没有与Session打交道
Private Sub Button_Click(ByVal sender As SystemObject _
ByVal e As SystemEventArgs) Handles ButtonClick
Dim proxy As New localhostService()
Dim ret As Integer
ret = proxyIncrementSessionCounter()
LabelText = Result: & CStr(ret)
End Sub
当我第一次调用Web Service时一切正常Web方法返回这就是那个Session变量的应有的初始值现在我点击Button来再次调用这个Web方法我希望看到的返回值是可惜的是无论我点击多少次Button返回值一直都是
你也许会怀疑原因就是我每次都创建了一个新的proxy类的实例去调用Web方法因此每次我点击按钮都会丢失上一次调用时的cookie不幸的是即使你将proxy类的初始化代码移到窗体的构造函数中然后对每次Web方法调用使用同一个proxy类的实例你还是不可能看到返回值有增加的迹象
问题在于cookieWeb Service代码并未从调用请求中发现有效的会话ID因此它每次被调用都创建一个全新的HttpSessionState对象并且返回它的初始值因为作为客户端的proxy类是从类SystemWebServiceProtocolsSoapHttpClientProtocol继承的它不包含SystemNetCookieContainer类的实例因此没有地方来存放返回的cookie为了解决这个问题我对代码做了如下一些修改
使用了ASPNET的session
但是并不是无Cookie的session
Private Cookies As SystemNetCookieContainer
Private Sub Button_Click(ByVal sender As SystemObject _
ByVal e As SystemEventArgs) Handles ButtonClick
Dim proxy As New localhostService()
Dim ret As Integer
为proxy类设置cookie容器
If Cookies Is Nothing Then
Cookies = New SystemNetCookieContainer()
End If
proxyCookieContainer = Cookies
ret = proxyIncrementSessionCounter()
LabelText = Result: & CStr(ret)
End Sub
现在代码工作正常了!每点击一次Button我都可以看到返回值增加注意到我并不是在函数中声明变量Cookies的它是窗体类的一个私有成员因为如果希望每次都返回同一个会话ID给服务器的话就必须在每次请求中使用CookieContainer类的同一个实例这就解释了为什么SoapHttpClientProtocol类默认不自动地设置的cookie容器正应为此你可以在多个SoapHttpClientProtocol类的实例中共享一个cookie容器而不是为其每个实例自动地创建一个新的cookie容器
无cookie的Session从Web Service的开发者的角度来看你可以想到相当多的人在试图使用你的Web服务时忘记在客户端代理类中添加Cookie容器聪明的开发者或许灵光一闪就会发现无cookie的Session应该可以出色地解决这个问题如果将nfig文件中sessionState元素的cookieless参数设置为true你将会发现通过浏览器界面调用Web方法时session变量工作正常但是如果你在Visual 中通过添加Web引用来调用它时依然存在着一些问题
为了研究无cookie的session我决定使用上面已经使用过的代码看看它能否在session状态被设置为cookieless的服务器环境中能否工作正常我也不想费心去删除cookie容器的相关代码因为我希望得到能在两种session状态下都正常工作的代码作为一个天生的乐观主义者我一个字也不改就直接运行它令人失望的事发生了——不过也不是完全没有想到我不得不面对这个异常
An unhandled exception of type SystemNetWebException occurred in systemwebservicesdll
Additional information: The request failed with the error message:
<html><head><title>Object moved</title></head><body>
<h>Object moved to <a /HttpSessionState/(lzpsnhhcfoahmaip)/serviceasmx>here</a></h>
</body></html>
发生了什么呢?原来HTTP请求收到的不是 OK响应如果你熟悉HTTP协议你或许可以从响应中的HTML代码中发现这是一个 Found响应这意味着该请求被重定向到超链接中指定的地址返回HTML代码是很明智的这样如果一个浏览器因为某些原因不支持重定向的话它可以把代码显示出来或者在重定向过程中显示这些代码直到重定向完成注意到超链接中包含了一个有趣的字符串(lzpsnhhcfoahmaip)显然我们可以推断这就是的会话ID它被嵌入了我们要重定向到的位置的URL中在客户端代理中我们需要做的仅仅是重新发送请求到这个新的URL
无须再在Win WinInet API编程中跋涉我们可以直接找到proxy类的一个属性允许自动重定向用外行人的说法就是如果我们接收到一个 Found响应就直接将请求重新发送到相应中HTTP位置头所指示的URL当Visual 的智能提示显示proxy类的AllowAutoRedirect属性时我感到这东西真是机灵得可爱我马上就在代码中加上如下一行
proxyAllowAutoRedirect = True
我认为这仍然比创建一个CookieContainer类并关联到proxy类要容易得多于是我又一次运行程序很不幸我遭遇了如下异常(为了简洁起见有所删节)
An unhandled exception of type SystemInvalidOperationException occurred
in systemwebservicesdll
Additional information: Client found response content type of text/html; charset=utf
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客户端在收到 Found响应时发送HTTP GET请求到响应中指定的地址是合情合理的即使初始请求是HTTP POST这种方式下浏览器工作得很好因为开始几乎所有的请求都是HTTP GET类型的只有当你试图传递数据到一个URL时才会出现上述失败的结果
理由是在传送的数据中可能包含潜在的敏感数据因此你需要确认是否用户真的想向新的资源传送数据显然如果你转向基于重定向设置的新地址你就没能确认用户是否真的允许将他们的数据发送到新的地址因此数据并没有被发送而代之以简单的HTTP GET请求
我对代码做了如下修改捕获 Found异常提示用户同意重定向他们的请求然后再次在新的位置调用我的Web方法
同时使用基于Cookie和Cookie的Session
Private Cookies As SystemNetCookieContainer
Private webServiceUrl as Uri
Private Sub Button_Click(ByVal sender As SystemObject _
ByVal e As SystemEventArgs) Handles ButtonClick
Dim proxy As New localhostService()
Dim ret As Integer
设置proxy类的Cookie容器
If Cookies Is Nothing Then
Cookies = New SystemNetCookieContainer()
End If
proxyCookieContainer = Cookies
设置proxy类的URL
If webServiceUrl Is Nothing Then
webServiceUrl = New Uri(proxyUrl)
Else
proxyUrl = webServiceUrlAbsoluteUri
End If
Try
ret = proxyIncrementSessionCounter()
Catch we As WebException
如果我们想检测HTTP状态码
那么就需要一个HttpWebResponse类的实例
If TypeOf weResponse Is HttpWebResponse Then
Dim HttpResponse As HttpWebResponse
HttpResponse = weResponse
If HttpResponseStatusCode = HttpStatusCodeFound Then
这是一个 Found响应提示用户是否进行重定向
If MsgBox(StringFormat(redirectPrompt _
HttpResponseHeaders(Location)) _
MsgBoxStyleYesNo) = _
MsgBoxResultYes Then
用户选择Yes重新尝试新的URL
webServiceUrl = New Uri(webServiceUrl _
HttpResponseHeaders(Location))
Button_Click(sender e)
Return
End If
End If
End If
Throw we
End Try
LabelText = Result: & CStr(ret)
End Sub
现在的Session工作正常了在你的应用程序中你可以根据情况自行决定是否提示用户重定向HTTP POST请求举一个例子如果你正在调用一个Web Service你也许就不希望出现任何可见的对话框
这样看来要使的Session完全正常地工作还真不是很容易但是应该意识到上面的代码所展示的原理在其他情况下一样的有用例如任何平台上的任何Web Service只要它使用HTTP cookie都需要一个cookie容器类似地也许有很多其他的原因导致当你向一个Web Service服务器发送请求时收到 Found响应在一个复杂的应用程序中调用Web Service时可能有许多特殊的情形需要你去处理cookie和重定向的问题就是两种这样的情形你应该将之作为你的Web Service调用代码中最基本的部分
结 论
在你调用Web方法的过程中对状态保持是非常有用的你必须意识到当你使用手边的浏览器界面测试你的Web Service时你并没有面对客户端程序必须处理的问题幸运的是这些问题并不是很难解决