ASP.NET中你可以通过继承IHttpHandler这个接口来实现一个同步(Synchronous)处理用户请求的类。比如你希望对于一切类型为fakephp的请求都通过你的Http Hanlder来处理,你可以实现以下这个类:
using System;
using System.Web;
public class FakePHPHttpHandler : IHttpHandler {
public void ProcessRequest(HttpContext context) {
//let's pretend we can handle PHP stuff
}
public bool IsReusable {
get { return true; }
}
}
然后通过在IIS里将你的dll注册为.fakephp的handler就可以了。这些在以下的MSDN文档里都有介绍:
http://msdn2.microsoft.com/en-us/library/system.web.ihttphandler.aspx
这里想说的是如何实现一个异步(Asynchronous)d的HTTP Handler。说起来其实也简单,只要实现IHttpAsyncHandler这个接口就好了。
IHttpAsyncHandler有两个method:
BeginProcessRequest
Initiates an asynchronous call to the HTTP handler.
EndProcessRequest
Provides an asynchronous process End method when the process ends.
这显然是和.NET Framework中标准的Asynchronous Programming Model (或者叫“异步模式”, Asynchronous Pattern)是一致的:
参考: Asynchronous Programming Overview
异步模式的优势是ASP.NET的worker thread不会等待BeginProcessRequest返回而是会掉头去接收其他的用户请求(当然你也可以要求worker thread等待,不过这样就等于变成了Synchronous Handler)。因为通常处理一个web request的后台时间需要比较长。假设你每秒只能处理10个用户请求,在同步模式下如果有11个人同时访问你的服务,就有一个人会看到500 Internal Server Error之类的错误消息了。但如果是异步模式,worker thread只要调用BeginProcessRequest,而根据Asynchronous Programming Overview,BeginProcessRequest应该立刻返回(“立刻”的含义是它不应该进行长时间的操作,而应该调用QueueUserWorkItem之类的API将耗时的任务放到新线程里执行),这样worker thread就可以腾出手去接收下一个user request了。
注:ASP.NET的max worker thread上限可以通过processModel configuration element里的maxWorkerThreads属性来改变(参考:Improving ASP.NET Performance 以及 processModel element),但最大的值也只是100 (range from 5 to 100, default 20)。
由此引出的问题自然是:当异步操作完成时, ASP.NET是如何知道并做相应处理的。这有一下几种选择:
1) 当调用BeginProcessRequest的时候,ASP.NET可以同时传入一个AsyncCallback的delegate,而在你完成异步操作后,你应该调用这个回调函数来通知ASP.NET。
2) ASP.NET可以不停地查看IAsyncResult (这个是BeginProcessRequest的返回值)IsCompleted属性来确认异步操作是否已经完成了,当然,当你完成异步操作时,你有义务将IsCompleted设成true。
3) ASP.NET也可以等待AsyncWaitHandle的信号,AsyncWaitHandle是IAsyncResult的另一个属性,这个和经典的Win32里waiting on kernel object是类似的。
4) ASP.NET可以直接调用EndProcessRequest。
注意:3) 和 4) 是Asynchronous Programming Overview里规定的标准的blocking execution的方式,也就是说,如果你的主线程在异步操作完成前无法再做任何工作时,它可以通过3) 或者 4)来等待异步操作的完成。
从理论上来说,你应该保证你的IHttpAsyncHandler能满足以上所有4种方式。但现实中,你未必一定如此做。那么哪些是我们必须实现以匹配ASP.NET的要求的呢?或者ASP.NET究竟是如何实现异步调用及返回的呢?
事实上,ASP.NET采用了方法1,也就是说,在调用BeignProcessRequest的时候,ASP.NET传入了一个AsyncCallback,而你应该在完成异步操作后调用这个callback,而在这个AsyncCallback里,ASP.NET又调用了你的EndProcessRequest来做收尾工作。
根据上面的讨论,我们可以如下设计我们的Asynchronous Http Hanlder:
public class AsyncFakePHPHttpHandler : IHttpAsyncHandler
{
private void ProcessRequestCallback(object state)
{
AsyncResult async = (AsyncResult)state;
// this is where the real work is done
ProcessRequest(async.context);
async.isCompleted = true;
if (null != async.asyncCallback)
async.asyncCallback(async);
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback asyncCallback, object state)
{
AsyncResult async = new AsyncResult(context, asyncCallback, state);
// if the callback is null, we can return immediately and let EndProcessRequest do all the job
// if callback is not null, we will use our thread pool to execute the necessary asynchronous operations
// what happens in ASP.NET is that the callback in NOT null, so QueueUserWorkItem will be used
if (null != async.asyncCallback)
threadPool.QueueUserWorkItem(ProcessRequestCallback, async);
return async;
}
// this design also satisfies method 4), we implement it this way to follow the Asynchronous Pattern as much as we can
public void EndProcessRequest(IAsyncResult result)
{
AsyncResult async = (AsyncResult)result;
if (null == async.asyncCallback)
ProcessRequest(async.context);
}
总结一下实现异步Http Handler的要点:
1) 所有在BeginProcessRequest中的耗时操作(比如IO什么的)都应该采用异步调用(比如BeginRead/EndRead)或者生成新的线程去执行。不错,你可以设计一个blocking BeginProcessRequest,没有人能阻止你这么做。But that's a BAD BAD idea.
2) 实现BeginProcessRequest/EndProcessRequest的目的是允许ASP.NET来异步调用你的Http Handler
3) 你应该创建一个实现IAsyncResult接口的类,在BeginProcessRequest中你会生成一个该类的实例并返回给ASP.NET(注意BeginProcessRequest的返回值类型)。而根据Asynchronous Pattern,ASP.NET在调用EndProcessRequest的时候会把这个实例再传回给你,你可以用这个实例来判断你所执行的任务的当前状态。
4) 我个人感觉比较容易导致困惑的是这里“两段式”的异步调用操作。首先ASP.NET是通过BeginProcessRequest/EndProcessRequest来异步调用我们的Http Handler的。然后我们在BeginProcessRequest又再次用异步模式(用QueueUserWorkItem或者其他的Begin*/End*操作)去完成真正的工作。实际上第二步的异步调用才是真正生成另一个thread来处理工作的地方。ASP.NET调用我们的BeginProcessRequest只是一种形式上的协议通知,因为是我们告诉ASP.NET:Hey,我是一个异步的handler。ASP.NET说:那好吧,既然你这么说的,我就用你异步的接口来调用你。事实上,在HttpRuntime的源代码中,可以看到ASP.NET的操作如下:
if (app is IHttpAsyncHandler) {
// asynchronous handler
IHttpAsyncHandler asyncHandler = (IHttpAsyncHandler)app;
context.AsyncAppHandler = asyncHandler;
asyncHandler.BeginProcessRequest(context, _handlerCompletionCallback, context);
}
else {
// synchronous handler
app.ProcessRequest(context);
FinishRequest(context.WorkerRequest, context, null);
}
====================================================================
最后的题外话,关于IIS/ASP.NET处理请求的工作流也是一个很有趣的问题,下面的这篇文章很棒(but the author's English writing kinda sucks, : ) )。比如,从中我们可以知道,整个流程中其实牵涉了两个Queue,一个是Kernel mode下的,一个是User mode下的,而后者就是ASP.NET所使用的Application Queue,而ASP.NET的worker thread就是从这个Queue里去取下一个需要处理的请求的。