Self Host模式下的ASP. NET Web API是如何进行请求的监听与处理的?
构成ASP.NET Web API核心框架的消息处理管道既不关心请求消息来源于何处,也不需要考虑响应消息归于何方。当我们采用Web Host模式将一个ASP.NET应用作为目标Web API的宿主时,实际上是由ASP.NET管道解决了这两个问题。具体来说,ASP.NET自身的URL路由系统借助于HttpControllerHandler这个自定义的HttpHandler实现了ASP.NET管道和ASP.NET Web API管道之间的“连通”,但是在Self Host寄宿模式下,请求的监听、接收和响应又是如何实现的呢?[本文已经同步到《How ASP.NET Web API Works?》]
目录
一、HttpBinding模型
Binding模型
HttpBinding
实例演示:直接利用HttpBinding进行请求的接收和响应
二、HttpSelfHostServer
HttpSelfHostConfiguration
HttpSelfHostServer与消息处理管道
实例演示:创建自定义HttpServer模拟HttpSelfHostServer的工作原理
一、HttpBinding模型
和WCF服务一样,我们可以采用Self Host模式将Web API寄宿于任何一种类型的托管应用程序下,宿主可以是一个Windows Form应用、WPF应用、控制台应用以及Windows Service。Self Host模式下的WCF和ASP.NET Web API不仅外在表现形式极为相似,其实在内部实现原理上也是一致的。
Binding模型
对于WCF具有基本了解的读者应该都知道,它是一个基于消息的分布式通信框架,消息交换借助于客户端和服务端对等的终结点(Endpoint)来完成,而终结点由经典的ABC(Address、Binding、Contract)三元素组成。WCF同样具有一个处理消息的管道,这个管道是一组Channel的有序组合,WCF下的Channel相对于ASP.NET Web API下的HttpMessageHandler。
WCF的消息处理管道的缔造者是作为终结点三要素之一的Binding。Binding不仅仅为服务端创建用于接收请求回复响应的管道,同时也为客户端创建发送请求接收响应的管道。Binding模型本身也相对比较复杂,所以我们不可能对其进行详细讨论。如果读者对此比较感兴趣,可以参阅《WCF的绑定模型》。由于ASP.NET Web API只是利用HttpBinding创建服务端消息处理管道,所以我们只讨论Binding的服务端模型。
从结构上讲,一个Binding是若干BindingElement对象的有序组合。对于最终创建的消息处理管道来说,每个Channel都对应着一个BindingElement。BindingElement并非直接创建对应的Channel,由它直接创建的实际上是一个名为ChannelListener的对象,Channel由ChannelListener创建。右图基本揭示了Binding的服务端模型。
顾名思义,ChannelListener用于请求的监听。当Binding对象开启(调用其Open方法)时,每个BindingElement会创建各自的ChannelListener。这些ChannelListener按照对应BindingElement的顺序连接成串,位于底部(面向传输层)的ChannelListener被绑定到某个端口进行请求的监听。一旦探测到抵达的请求,它会利用由所有ChannelListener创建的Channel组成的管道来接收并处理该请求。对于最终需要返回的响应消息,则按照从上到下的顺序被这个管道进行处理并最终返回给客户端。
对于这个由Channel组成消息处理管道来说,有两种类型的Channel是必不可少的。一种是面向传输层用于发送和接收消息的TransportChannel,另一种被称为MessageEncodingChannel则负责对接收的消息实施解码并对发送的消息实施编码。TransportChannel由TransportChannelListener创建,而后者由TransportBindingElement创建。与之类似,MessageEncodingBindingElement是MessageEncodingChannelListener的创建者,而后者又是MessageEncodingChannel的创建者。
如果采用Self Host寄宿模式,请求的监听是由一个类型为HttpBinding的Binding对象创建的ChannelListener管道来完成的,由它创建的管道实现了针对请求的接收和针对响应的回复。HttpBinding类型定义在“System.Web.Http.SelfHost.Channels”命名空间下,我们接下来对它进行详细讲述。
如左图所示,
当我们直接运行该程序后,实际上就已经启动了针对基地址"http://127.0.0.1:3721"的监听器。现在我们通过浏览器对这个监听器发起请求,为了使请求更像一个针对Web API的调用,我们将请求地址设置为“http://127.0.0.1:3721/employees/001”(看起来好像是获取某个编号为001的员工信息)。如右图所示,通过浏览器发送的请求相关信息会显示在控制台上,而浏览器上也会显示基于JSON格式的员工信息。
接下来HttpSelfHostServer从生成的HttpMessage中提取被封装的HttpRequestMessage对象,并直接分发给后续的HttpMessageHandler作进一步处理。对于最终返回的表示响应的HttpResponseMessage对象,HttpSelfHostServer将其封装成一个HttpMessage对象并利用消息处理管道返回给客户端。在通过传输层发送响应消息之前,HttpMessage会先编码。通过上面的介绍我们知道整个编码工作完全是针对被HttpMessage封装的HttpResponseMessage对象进行的,在HttpResponseMessage中保存的响应内容就是客户端接收到的内容。左图基本揭示了Self Host寄宿模式下整个消息处理管道的结构。
实例演示:创建自定义HttpServer模拟HttpSelfHostServer的工作原理
通过上面的介绍,我想读者朋友们应该对Self Host模式下消息处理管道如何进行请求的监听、接收、处理和响应已经有了全面的了解。如果我们能够创建一个自定义的HttpServer来模拟HttpSelfHostServer的工作原理,我想大家对此的印象一定更加深刻。在本章内容即将完结之前,我们就来完成这么一个演示实例。
我们通过继承HttpServer创建如下一个用于模拟HttpSelfHostServer的MyHttpSelfHostServer类型。两者对于请求的监听、接收和响应的实现原理是一致的,不同之处在于HttpSelfHostServer基本采用异步的操作方式,MyHttpSelfHostServer采用同步编程方式。
1: public class MyHttpSelfHostServer: HttpServer
2: {
3: public Uri BaseAddress { get; private set; }
4: public IChannelListenerChannelListener { get; private set; }
5:
6: public MyHttpSelfHostServer(HttpConfiguration configuration, Uri baseAddress)
7: : base(configuration)
8: {
9: this.BaseAddress = baseAddress;
10: }
11:
12: public void Open()
13: {
14: HttpBinding binding = new HttpBinding();
15: this.ChannelListener = binding.BuildChannelListener(this.BaseAddress);
16: this.ChannelListener.Open();
17:
18: IReplyChannel channnel = this.ChannelListener.AcceptChannel();
19: channnel.Open();
20:
21: while (true)
22: {
23: RequestContext requestContext = channnel.ReceiveRequest(TimeSpan.MaxValue);
24: Message message = requestContext.RequestMessage;
25: MethodInfo method = message.GetType().GetMethod("GetHttpRequestMessage");
26: HttpRequestMessage request = (HttpRequestMessage)method.Invoke(message, new object[] {true});
27: TaskprocessResponse = base.SendAsync(request, new CancellationTokenSource().Token);
28: processResponse.ContinueWith(task =>
29: {
30: string httpMessageTypeName = "System.Web.Http.SelfHost.Channels.HttpMessage, System.Web.Http.SelfHost";
31: Type httpMessageType = Type.GetType(httpMessageTypeName);
32: Message reply = (Message)Activator.CreateInstance(httpMessageType, new object[] { task.Result });
33: requestContext.Reply(reply);
34: });
35: }
36: }
37:
38: public void Close()
39: {
40: if (null != this.ChannelListener && this.ChannelListener.State == CommunicationState.Opened)
41: {
42: this.ChannelListener.Close();
43: }
44: }
45: }
MyHttpSelfHostServer的只读属性BaseAddress表示监听基地址,该属性直接在构造函数中指定。与HttpSelfHostServer不同的是,用于创建MyHttpSelfHostServer提供的配置对象是一个HttpConfiguration对象而不再是HttpSelfHostConfiguration。
在Open方法中,我们根据提供的监听基地址利用HttpBinding对象创建一个ChannelListener对象,MyHttpSelfHostServer的只读属性ChannelListener引用的也正是这个对象。在开启该ChannelListener之后,我们调用其AccpetChannel方法创建信道栈,最终返回位于栈顶的Channel。在该Channel开启的情况下,我们在一个“永不终止”的While循环中调用其ReceiveRequest方法进行请求的监听。
当信道栈成功接收请求消息后(这是一个HttpMessage对象),我们从中提取出被封装的HttpRequestMessage对象,并将其作为参数调用SendAsync方法,表示请求的HttpReuqestMessage自此进入了消息处理管道这个流水车间。
调用SendAsync方法返回的是一个Task
为了验证我们自定义的MyHttpSelfHostServer是否能够替代“原生”的HttpSelfHostServer,我们在一个控制台中定义了如下一个继承自ApiController的ContactsController。它具有两个重载的Action方法Get,前者用于返回所有的联系人列表,后者返回指定ID的某个联系人信息。
1: public class ContactsController : ApiController
2: {
3: private static Listcontacts = new List
4: {
5: new Contact{ Id="001", Name = "张三", PhoneNo="123", EmailAddress="zhangsan@gmail.com"},
6: new Contact{ Id="002",Name = "李四", PhoneNo="456", EmailAddress="lisi@gmail.com"}
7: };
8:
9: public IEnumerableGet()
10: {
11: return contacts;
12: }
13:
14: public Contact Get(string id)
15: {
16: return contacts.FirstOrDefault(c => c.Id == id);
17: }
18: }
19:
20: public class Contact
21: {
22: public string Id { get; set; }
23: public string Name { get; set; }
24: public string PhoneNo { get; set; }
25: public string EmailAddress { get; set; }
26: }
在作为入口的Main方法中我们编写了如下一段简单的“寄宿”程序。我们根据创建的HttpConfiguration对象和指定的监听基地址(“http://127.0.0.1:3721”)创建了一个MyHttpSelfHostServer对象。在调用Open方法开始监听之前,我们注册了一个URL模板为“http://127.0.0.1:3721”的HttpRoute。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: using (MyHttpSelfHostServer httpServer = new MyHttpSelfHostServer(new HttpConfiguration(), new Uri("http://127.0.0.1:3721")))
6: {
7: httpServer.Configuration.Routes.MapHttpRoute(
8: name : "DefaultApi",
9: routeTemplate : "api/{controller}/{id}",
10: defaults : new { id = RouteParameter.Optional });
11:
12: httpServer.Open();
13: Console.Read();
14: }
15: }
16: }
运行该程序之后,这个“宿主”程序便开始进行请求的监听。现在我们直接利用浏览器对定义在ContactsController中的两个Action方法Get发起请求,通过注册的HttpRoute和“请求的HTTP方法直接作为Action名称”的原理,我们使用的URL分别为“http://127.0.0.1:3721/api/contacts”和“http://127.0.0.1:3721/api/contacts/001”。如右图所示,我们期望的联系人信息直接以XML的形式显示在浏览器中,由此可见我们自定义的MyHttpSelfHostServer“不辱使命”。