在高需求的Web应用程序中,保持客户端UI的响应性一直是其重要特性之一。许多网站,如Expedia、Travelocity等,使用中间的“请稍候...”页面来解决这个问题。而像Google Maps这样的网站则使用客户端特性,例如XMLHTTP,来在不移动用户当前页面的情况下进行请求。本文的目的是展示如何结合使用XMLHTTP和ASP.NET,以使最终用户体验更加可接受。
从架构上讲,ASP.NET的一个关键特性是其对象具有短暂的生命周期。这对于高需求网站来说非常重要,因为它允许应用程序快速高效地执行,同时还能保持高水平的并发用户。保持ASP.NET网站响应性的关键因素是了解控制页面执行的框架结构。
许多开发者都知道,在ASP.NET中创建一个简单的网页需要使用System.Web.UI.Page类。然而,有些人可能不知道还有其他方式来处理对应用程序的请求。这就是System.Web.IHttpHandler和System.Web.IHttpAsyncHandler接口发挥作用的地方。
ASP.NET中所有的请求执行都是通过System.Web.IHttpHandler接口进行的。如果查看System.Web.UI.Page类的类图,会看到它继承自System.Web.UI.TemplateControl并实现了System.Web.IHttpHandler接口。类定义和对象层次结构如下:
public class Page : TemplateControl, IHttpHandler
IHttpAsyncHandler接口继承自IHttpHandler,并定义了两个额外的方法,BeginProcessRequest和EndProcessRequest。这些方法构成了在ASP.NET中处理异步调用所需的机制。
对于这种场景,最好使用IHttpAsyncHandler而不是Page对象,因为Page对象每个请求都会执行额外的工作(子控件渲染、事件处理通知等)。有关IHttpAsyncHandler的更多信息,请参见MSDN文档。
在ASP.NET应用程序中实现线程有三种简单的方法:
前两种方法为应用程序提供了快速启动工作线程的方式。但不幸的是,它们会损害应用程序的整体性能,因为它们消耗了ASP.NET用于处理HTTP请求的线程池中的线程。换句话说,如果在一个请求中调用了五个池/委托线程,应用程序将只剩下20个可用线程(默认情况下,ASP.NET为每个应用程序提供25个工作线程)。解决这个问题的方法是使用第三种选项,因为创建的线程不属于应用程序池。
如果想更多地了解ASP.NET中的线程处理,请参见参考资料部分。
本文使用的例子是一个简单的天气服务。用户界面允许用户输入美国邮政编码以检查城市当前的天气状况。选择Web服务调用的原因是它提供了一个关于如何使用ASP.NET的异步功能和XMLHTTP的“现实世界”示例。
当用户从列表中选择一个输入的邮政编码并点击“检查天气”按钮时,会创建两个异步请求,一个发送给天气检查处理器,另一个发送给Web服务。使用DHTML进度条在执行此过程时让用户“娱乐”。一旦客户端收到响应,进度条就会停止,UI也会更新。
这个示例背后的主要类是AsyncTaskHandler类。这个类映射到一个.ashx文件,以便ASP.NET可以连接请求链。这个类实现了两个接口,前面提到的IHttpAsyncHandler和System.Web.SessionState.IRequiresSessionState接口,后者用于告诉ASP.NET这个处理器可以访问会话状态。然后,这个类使用两个类AsyncRequestResult和AsyncRequest来处理请求。
AsyncRequestResult实现了System.IAsyncResult接口,以便ASP.NET可以调用执行回调所需的适当方法。AsyncRequest类用于调用外部天气Web服务并将响应写回等待的客户端。对象之间的协作可以通过以下图表看到:
处理器一收到请求,就创建一个AsyncRequestResult类型的对象。使用这个对象作为构造函数参数,处理器然后创建一个AsyncRequest类型的对象。最后,处理器创建一个System.Thread.Thread类型的工作线程,并使用AsyncRequest的Process方法,并将创建的AsyncRequestResult返回给ASP.NET和客户端。
大部分核心逻辑发生在AsyncRequest.Process方法中。在这个方法中,使用相关的AsyncRequestResult对象调用天气信息Web服务,并将HTML片段返回给客户端。该方法如下所示:
public void Process()
{
try
{
// 从AsyncRequestResult对象获取邮政编码
string strZip = Result.AsyncState as string;
int zipCode = Int32.Parse(strZip);
// 获取天气信息
string message = Result.GetWeather(zipCode);
// 写回客户端
Result.Context.Response.Write(message);
}
finally
{
// 告诉ASP.NET请求已完成
Result.Complete();
}
}
AsyncRequestResult.GetWeather方法下对天气Web服务的实际调用如下所示:
public string GetWeather(int zipCode)
{
string message = "";
try
{
// 调用Web服务
ExtendedWeatherInfo info = weatherService.GetExtendedWeatherInfo(zipCode);
// 格式化消息
message = string.Format("{0} - {1}
Current Temperature: {2}a
Feels Like: {3}.",
zipCode.ToString(), info.Info.Location, info.Info.Temprature, info.Info.FeelsLike);
}
catch
{
message = "An error was encountered while calling web service.";
}
return message;
}
现在已经对代码的工作方式有了快速的了解,让看看客户端如何创建请求并处理响应。
要通过JavaScript以编程方式进行HTTP请求,需要使用XMLHTTP。很长一段时间以来,微软在其MSXML解析器中捆绑了一个版本的这项技术。要创建一个绑定到此功能的JavaScript对象,请执行以下操作:
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
这个函数将对URL进行完整的异步请求,并附加一个回调函数:
function postRequest(url)
{
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
// 'true'指定这是一个异步调用
xmlhttp.Open("POST", url, true);
// 为调用注册回调
xmlhttp.onreadystatechange = function()
{
if(xmlhttp.readyState == 4)
{
var response = xmlhttp.responseText;
divResponse.innerHTML += "" + response + "
";
stopProgressBar();
}
};
// 发送实际请求
xmlhttp.Send();
}
从这里开始,在等待异步处理器执行长时间处理请求的同时,向用户显示一个简单的进度条,告诉他们他们的请求正在处理中。
以下图表显示了XMLHTTP请求的执行过程:
这个过程的最终结果如下:
能够异步执行长时间运行的任务对于任何规模的Web应用程序来说都是一个巨大的好处。希望本文能向开发者展示解决这个设计问题的另一种方式。同时,希望开发者在为ASP.NET应用程序添加线程时更加注意。