WEB SERVICE大概是.NET中最引人瞩目的一个技术“亮点”了。事实上Microsoft在不遗余力的推动WEB SERVICE,力图使之成为新一代的分布式计算模式的标准。WEB SERVICE是Microsoft在COM/DCOM之后的最重要的技术革新。与以往不同的是,Microsoft这次的革新是建立在业界开放的标准之上的。SOAP,HTTP, WSDL以及UDDI并非Microsoft所有,所以在WEB SERVICE上的技术投入并不会将你“绑”在Microsoft身上。SUN, IBM以及ORACLE都提供自己的WEB SERVICE方案,所以你有选择,可以使用非Microsoft的其它方案,至少在理论上是这样。
简言之,WEB SERVICE其实就是在Web之上提供服务(SERVICE〕。客户端可以调用远程服务器端函数并得到结果,就像RMI,DCOM和COBRA一样。不同的是客户端和服务器端的“对话”使用的是业界标准而不是JAVA或Microsoft独有的技术。由于采用了HTTP作缺省通讯协议,使得WEB SERVICE可以透过各个企业,公司的防火墙,真正实现跨INTERNET的分布式计算。也因为HTTP,使得WEB SERVICE在本质上一些先天的限制,就像其它的WEB应用程序一样。HTTP是一种无态的(STATELESS)通讯协议,所以在HTTP之上的WEB SERVICE如何保持“状态(STATE)”就成为一个有趣的话题。在这里,我想就这个问题做一些讨论。
从一个简单的例子看WEB SERVICE的“无态性”
在Microsoft提供的VISUAL STUDIO.NET中,集成的开发环境将很多WEB SERVICE的繁琐细节隐藏了起来,尽量的想使开发者感到方便和简单,就像在开发普通的类(CLASS)和函数那样。但是你千万不能被表象所蒙蔽,从而导致一些最基本的错误。请看下面这个小例子。
class Student : System.Web.Services.WebService
{
private String name;
public String Name
{
get{return name;}
set{name=value;}
}
}
|
一眼看上去,这个Student类没有什么毛病。另外,你在客户端想这样的使用它。
...
localhost.Student student = new localhost.Student();
student.Name = "Lao Wang";
String myInfo = student.Name();
...
|
如果这不是一个WEB SERVICE,那么客户端和服务器端都没有问题。但是在WEB SERVICE里,属性(PROPERTY)是不能成为公共接口的一部分的(PUBLIC INTERFACE),所以get和set是行不通的。
OK,你可能会这样修改一下这段小程序
class Student : System.Web.Services.WebService
{
private String name;
[WebMethod]
public String getName()
{
return name;
}
[WebMethod]
public void setName(String aName)
{
name = aName;
}
}
|
客户端的程序为
...
localhost.Student student = new localhost.Student();
student.setName("Lao Wang");
String myInfo = student.getName();
...
|
这下应该没有问题了吧。但当你运行客户端程序的时候,你发现结果和你预料的不一样。getName函数返回值是NULL。奇怪,刚刚设置的"Lao Wang"哪儿去了呢?
如果你遇到这样的问题,那说明你被集成环境给蒙蔽了,没有知道程序究竟是怎么执行的。看起来你只创建了一个Student实例(Object),并两次调用它的函数。但实际情况却非如此。你没有创建一个Student实例,你只是创建了Student类的代理类的实例(Proxy Class)。Student类的实例是在服务器端创建的,而且是两次。你在客户端两次的函数调用在服务器端生成了两个实例,每个实例响应一次函数调用。两个实例之间没有任何联系,所以你把第一个实例的Name设为"Lao Wang"对于第二个实例来讲是全然无知的。现在,你可能体会到"无态"的涵义了吧。
如果你知道一些WEB SERVICE的背景REMOTING的话,这一点就更好理解了。WEB SERVICE的实例创建是属于REMOTING中"单一调用"方式的(SINGLE CALL)。意思是每一次的函数调用都会在服务器端创建一个新的实例。(REMOTING还支持Singleton和ClientActivation两种方法)
那么解决如何保存"状态"呢?就前面的这个小问题来说,你可以这样修改一下程序。
class Student : System.Web.Services.WebService
{
private String name;
[WebMethod (EnableSession=true)]
public String getName()
{
return (String)Session["NAME"];
}
[WebMethod (EnableSession=true)]
public void setName(String aName)
{
Session["NAME"] = aName;
}
}
|
客户端的程序为
...
localhost.Student student = new localhost.Student();
student.CookieContainer = new System.Net.CookieContainer();
student.setName("Lao Wang");
String myInfo = student.getName();
...
|
这样一改,问题就解决了。其"奥秘"在于你引入了ASP.NET中的"会话状态"管理机制(SESSION STATE MANAGEMENT)。如果你做过ASP或ASP.NET开发的话,你就回马上明白这一点。就象我前面所说, WEB SERVICE是在WEB上提供的SERVICE,WEB上的许多东西在WEB SERVICE里同样适用。"会话状态"管理机制是WEB SERVICE中最普遍采用的一种保存"状态"的方法,它将信息存在于服务器端,所以适宜保存比较敏感的信息,例如信用卡号码,银行帐号什么的。并且SESSION中可以存放各种复杂的数据结构甚至是COM对象的实例,这使得它可以用来实现很复杂的状态保存。
SESSION的一些基本常识
1. Session Cookie
ASP.NET中,会话(Session)是用一个120位的字符串(String)来表示的。ASP.NET负责生成和管理这个变量。它被存放在一个特殊的Session Cookie里(ASP.NET_SessionId)。在ASP.NET里,这个变量可以嵌在URL里,但在WEB SERVICE里这个选项不被支持。在WEB SERVICE里使用Cookie使得它必须和HTTP一起使用。如果你想用SMTP协议来和WEB SERVICE通讯,那么Cookie将不再能使用。好在在多数情况下,这并不是一个问题。
2. 如何使用"会话状态"
象前面的例子那样,设置WebMethod的EnableSession属性为"True"就可以了。"会话状态"的使用是基于函数的而不是基于类的。这样设计的原因是为了提高性能。如果一个函数使用了EnableSession属性,即使函数体里没有使用任何状态变量,ASP.NET还是要做对于状态进行例行检查的,这是不必要的浪费。所以Web Method的缺省状态是EnableSession=False。值的注意的是EnableSession=False只表示这个函数不使用"会话状态",并不表示没有"会话状态"。因为设置它为True或False并不回生成或销毁"会话状态",只表示这个函数是否使用"会话状态"。
如果你定义的类是System.Web.Services.WebService的子类,那么你可以直接使用"会话对象"(Session Object),就像前面的例程那样。或者你可以通过System.Web.HttpContext.Current.Session来得到"会话对象"。
Session mySession = System.Web.HttpContext.Current.Session;
String name = mySession["NAME"];
|
在客户端,程序里要有相应的调整。客户端的代理类从System.Web.Services.Protocols.SoapHttpClientProtocol中继承了许多有用的属性,其中CookieContainer就是一个。它的作用是存放每一个WebMethod请求中发送和接收的Cookies。如果在客户端想使用"会话",那么只要创建一个CookieContainer的实例就可以了。
localhost.Student student = new localhost.Student();
student.CookieContainer = new System.Net.CookieContainer();
|
在有些时候,在释放了代理对象的的引用后(Proxy Object Reference〕,你可能还想再使用"会话状态"中的变量。这种情况下,你需要把CookieContainer里的Cookie或是部分Cookie拿出来存在什么地方。
localhost.Student student = new localhost.Student();
//创建一个System Uri对象
Uri uri = new Uri("http://yourServer")
//从代理对象中获取Cookies
System.Net.CookieCollection cookies = student.CookieContainer().GetCookies(uri);
|
当你拿到cookies对象后,你可将它存放在什么地方以后使用,或是传给其它使用同一WEB SERVICE类或对象,只要是"会话"还没有过期(Expire)。
3."会话状态"的四种配置方式。
"会话状态"可以通过修改machine.config文件以及Web.config文件来设置。machine.config文件是用来配置整个计算机的,而Web.config是针对具体每一个WEB SERVICE的。通常我们是通过设置这个文件来改变"会话状态"模式的。下面给出的是一个比较典型的web.config文件的有关片段。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.Web>
...
<!-- SESSION STATE SETTINGS
By default ASP .NET uses cookies to identify which requests belong to a
particular session.
If cookies are not available, a session can be tracked by adding a
session identifier to the URL.
To disable cookies, set sessionState cookieless="true".
-->
<sessionState
mode="InProc"
stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1;user id=sa;password="
cookieless="false"
timeout="20"
/>
...
</system.Web>
</configuration>
|
ASP.NET中,mode有4个可选值。
a. Off -- 禁止WEB SERVICE和WebMethod使用会话状态。
b. InProc -- 缺省设置。会话的状态存放在IIS的进程里,这种设置有最快的存取速度,但可扩展性能不好。别的进程以及其它计算机的进程无法存取InProc里的会话状态变量。对于开发人员在开发调试时,这是最常用的选择。
c. StateServer -- 使用专用的进程来管理"会话状态"。这是ASP.NET引入的一个新技术。.NET提供了一个后台进程来专门管理会话状态,这使得会话状态不再局限在一台计算机上,多台提供Web服务的计算机可以共用一台计算机的一个后台进程来专门管理会话状态,使得会话状态变量在WEB FARM中的使用成为可能,从而极大的提升了程序的可扩充性能。但是StateServer的会话状态变量存取要跨越进程甚至是网络,其速度比InProc要慢许多,而且多台Web服务器共享一个后台进程来管理"会话状态"会有潜在的可靠性问题。一旦这个后台进程出现问题,那么多台Web服务器将同时丢掉所有的状态信息。用术语来说,StateServer 是一个"Signle Point Failure"。在理论上讲,这不是一种理想的方案。
另外如果多台Web服务器共享一个StateServer,那么你可能还要修改machine.config里的machine key(用来加密会话状态数据的)。多台Web服务器要使用同样的设置。validationKey和decryptionKey是一个40-128位长的16进制的字符串。例如
<machineKey
validationKey="123456789adcdef123456789adcdef123456789adcdef123456789adcdef"
decryptionKey="abcd123456789adcdefabcd123456789adcdefaaa123456789adcdef000"
validation=SHA1"
|
(图为管理ASP.NET会话状态的后台进程,C:\WINNT\Microsoft.NET\Framework\v1.0.3705\aspnet_state.exe)
d. SqlServer -- 使用SQL数据库来管理会话状态变量。其想法和StateServer一样,是想让多台Web服务器可以共享会话状态变量(存放在SQL中的tempdb数据库中),使用数据库可能会比后单一台进程更稳定,扩充性能更好,但它的存取速度也是最慢的。是否使用它要根据你的具体需要来确定。
Timeout的设置很好理解。如果Timeout=20表示如果20分钟没有收到请求,会话状态将会被清理。如果你想做进一步的努力,你可以提供一个WebMethod,客户端程序可以通过调用它来立即释放会话状态变量。当然,如果客户端程序不调用它,你也没有办法强迫。
[WebMethod (EnableSession=true)]
public String ReleaseSession()
{
Session.Abandon();
}
|
客户端的程序相应为
...
localhost.Student student = new localhost.Student();
student.CookieContainer = new System.Net.CookieContainer();
student.setName("Lao Wang");
String myInfo = student.getName();
student.ReleaseSession();
...
|
无态WEB SERVICE的设计模式
虽然ASP.NET提供了很多简便易用的方法来方便状态管理,但在理论上讲,"有态"的WEB SERVICE总是要加重服务器端的负荷,存在可扩展性的问题。那么能否把状态信息存放在客户端,由客户端程序维护呢?这种想法导致了另外一种设计思路,即"无态"的WEB SERVICE。在具体实现过程中,通常有以下这样几种作法。
1. Custom Cookies
作过WEB开发的程序员都知道,客户端的Cookie可以用来记录状态信息。在WEB SERVICE里客户端的Cookie同样适用。具体来说,可以把如何使用客户端用户自定义的Cookie归纳为以下4步。
a. 为WEB SERVICE的URL创建Cookie对象。
b. 在Cookie对象插入信息。
c. 把Cookie对象拼接到Resonse对象中。这样Cookie将会传到客户端。以后客户端发出的请求将会夹带这个Cookie。
d. WebMethod从客户端的请求中提取Cookie信息,并做相应处理。
class Student : System.Web.Services.WebService
{
private String name;
[WebMethod (EnableSession=true)]
public String getName()
{
HttpCookie cookie = Context.Request.Cookies["USER_INFO"];
return cookie.Values["NAME"];
}
[WebMethod (EnableSession=true)]
public void setName(String aName)
{
HttpCookie cookie = new HttpCookie("USER_INFO");
cookie.Values.Add("NAME", aName);
Context.Response.AppendCookie(cookie);
}
}
|
从上面的这个小例程来看,使用Custom Cookies同样可以保存"状态"信息。但需要指出的是客户端用户自定义的Cookie有许多具体的局限。
a. 只能和HTTP协议一同使用。
b. 只能保存字符串信息,不够安全并且信息长度也有限制。
c. 用户无法控制Custom Cookies何时被送出。
基于这些特点,一般不建议使用这种技术来保存重要的信息。Custom Cookies适宜用来传送一些小量的辅助性信息,在服务器端,如果这些信息没能拿到,应不至于影响函数的正常执行。
2. SOAP Headers
SOAP消息有三个部分:信封(Envelope),信头(Headers〕和信体(Body)。通常我们只使用信封和信体,信头是没有被使用的。使用信头可以存放客户端的状态信息。在程序中使用SOAP信头要做以下几步。
a. 定义SOAP Header的类
public class UserHeader : System.Web.Services.Protocol.SoapHeader
{
public String userName;
}
|
b. 改动服务器端的程序
class Student : System.Web.Services.WebService
{
private UserHeader userHeader;
[WebMethod, Soapeader("userHeader")]
public String getName()
{
return userHeader.userName;
}
}
|
c. 更改客户端的程序
...
localhost.UserHeader header = new localhost.UserHeader();
header.userName = "Lao Wang";
//生成SimpleHeaderService实例
localhost.SimpleHeaderService service = new localhost.SimpleHeaderService();
service.UserHeaderValue = header;
String myInfo = student.getName();
student.ReleaseSession();
...
|
当你使用SOAP Header后,代理类中会自动生成一些程序代码,使你可以方便的在客户端使用信头。
当你正式的使用这种技术的时候,你回发现事实上的情况可能比这个小例程要复杂。之所以不想在这里展开叙述它是因为它现在还有些兼容性的问题没有解决。SUN,Microsoft以及IBM对信头的实现不尽相同,有的软件产品可能还不支持这一技术,所以在选用它的时候要考虑兼容性的问题。另外这一技术局限于只能用SOAP的方法来访问WebMethod,HTTP GET和HTTP POST都不支持SOAP信头。这也许会成为影响你决定的一个因素。
事实上。你可以在服务器端使用HTTP的Application以及Cache来存放信息,就像在ASP.NET应用程序里那样,但严格的讲它们存放的是进程内的公共信息,而不是使用进程的某一个客户的私有信息,和本文讨论的话题略有距离,所以不再赘述了。
零零总总讲了一些关于WEB SERVICE中状态管理的一些技术和它们各自的优缺点,希望能对你有些帮助。