ASP.NET Web API服务端框架核心是一个独立于具体寄宿环境的消息处理管道,它不关心请求消息来源于何处,响应消息又回归于何方。说得具体点,这个由若然HttpMessageHandler的有序组合构成的消息处理管道并没有考虑对请求的监听、接收和响应,因为它们工作的方式取决于具体的寄宿方法。在Self Host寄宿模式下,请求的监听、接收和和最终响应是如何解决的呢?[本文已经同步到《How ASP.NET Web API Works?》]
和WCF服务一样,我们可以采用Self Host的方式将Web API寄宿于任何一种类型的托管应用中,比如Windows Form应用、WPF应用、控制台应用以及Windows Service。其实上不但Self Host模式下的WCF和ASP.NET Web API在“外在”的表现形式下相似,在内部实现原理上也一致。总的来说,ASP.NET Web API通过一个类型为HttpBinding的Binding实现了对请求的监听、接收和响应。
对于WCF具有基本了解的读者应该都知道:WCF是一个基于消息的分布式通信框架,消息交换借助于客户端和服务端对等的终结点(Endpoint)来完成,而终结点由经典的ABC(Address、Binding、Contract)三元素组成。与ASP.NET Web API类似,WCF提供了一个名为“信道栈(Channel Stack)”的管道来处理消息,组成该管道的信道(Channel)相当于HttpMessageHandler,而这个管道的缔造者就是Binding。
WCF中的Binding不仅仅创建服务端用于接收请求回复响应的管道,同时创建客户端发送请求接收响应的管道,而且模型本身也比较复杂,所以我们不可能对其进行详细讨论。如果读者对WCF比较感兴趣,可以参阅《WCF之绑定模型》,由于ASP.NET Web API只是利用HttpBinding创建服务端消息处理管道,所以我们只讨论Binding的服务端模式。
从结构上讲,一个Binding同时若个BindingElement的有序组合,对于最终创建的信道栈来说,每个Channel都对应着一个BindingElement。但是BindingElement并非直接创建对应的Channel,它直接创建的实际上是一个名为ChannelListener的对象,后者创建相应的Channel。针对服务端的Binding模型如右图所示。
顾名思义,ChannelListener用于请求的监听。当Binding对象开启(调用其Open方法),每个BindingElement会创建各自的ChannelListener,并按照对应的顺序连接成串,位于底部(面向传输层)的ChannelListener被绑定到某个端口进行请求的监听。一旦探测到抵达的请求,它会利用所有ChannelListener创建的Channel组成的管道来接收并处理该请求。对于最终需要返回的响应消息,则按照从上到下的顺序被这个管道进行处理并最终返回给客户端。
对于组成这个用于接收处理请求消息和处理发送响应消息的信道栈来说,有两种类型的Channel是必不可少的,一种面向传输层用于发送和接收消息的TransportChanenl,另一种则是在消息发送前对其编码并在接收后对其解码的MessageEncodingChannel。这两种类型的Channel对应的ChannelListener和BindingElement分别称为TransportChannelListener/TransportBindingElement和MessageEncodingChannelListener和MessageEncodingBindingElement。
Binding最终存在的目的在于创建用于处理和传输消息的信道栈,组成信道栈的每一个Channel对应着一个BidningElement,所以Binding本身处理消息的能力由其BindingElement的组成来决定。我们可以通过分析BindingElement的组成来了解消息最最终是如何处理的,现在我们就来分析ASP.NET Web API在Self Host模式下使用的HttpBinding由哪些BindingElement构成。
如左图所示,HttpBinding仅仅由两种必需BindingElement构成,TransportBindingElement的类型决定于最终采用的传输协议。具体来说,如果采用单纯的HTTP协议,那么最终采用的TransportBindingElement类型为HttpTransportBindingElement,当我们采用HTTPS协议的情况下,对应的类型则为HttpsTransportBindingElement。由于TransportBindingElement是面向传输层的,所以网络协议决定其类型,这应该很好理解。
我们现在着重来分析与消息编码与解码相关的BindingElement,从左图可以看出这是一个HttpMessageEncodingBindingElement类型(这是一个定义在程序集System.Web.Http.SelfHost.dll中的内部类型)的对象,它最终会创建一个MessageEncoder对象完成针对消息的编码和解码工作。消息编码在WCF的消息处理管道中的意义在于:将传输层接收到的二进制数据经过解码以生成一个消息对象,需要发送的消息对象进行编码转换成交由传输层发送的二进制数据。
对于ASP.NET Web API的消息处理管道来说,请求消息和响应消息的类型分别是HttpRequestMessage和HttpResponseMessage,所以这个HttpMessageEncoder解码后生成的请求消息必须能够转换成一个HttpRequestMessage对象,而响应消息(这里指的是针对WCF的响应消息)实际上对于一个HttpResponseMessage对象的封装,并且应该按照HttpResponseMessage的“语义”进行编码。
我们不妨来看看这个具体的消息是一个怎样的对象,实际上该消息对象的类型为具有如下定义的HttpMessage,这同样是定义在程序集System.Web.Http.SelfHost.dll中的内部类型。我们可以看出,HttpMessage实际上是对一个HttpRequestMessage或者HttpResponseMessage对象的封装,两个方法GetHttpRequestMessage和GetHttpResponseMessage分别用于提取被封装的HttpRequestMessage和HttpResponseMessage对象。这个两个方法均具有一个布尔类型的参数extract,表示是否“抽取”被封装的HttpRequestMessage和HttpResponseMessage对象,如果传值为True,被封装的对象将被设置为Null。
internal sealed class HttpMessage : Message { //其他成员 public HttpMessage(HttpRequestMessage request); public HttpMessage(HttpResponseMessage response); public HttpRequestMessage GetHttpRequestMessage(bool extract); public HttpResponseMessage GetHttpResponseMessage(bool extract); }
再将我们的关注点拉回到HttpBinding及其创建的用于接收请求发送响应的管道。开启后的HttpBinding会创建ChannelListener监听请求,一旦探测到抵达的请求,ChannelListener会先利用基于HTTP或者HTTPS的TransportChannel来接收请求。接收的二进制数据会被由HttpMessageEncodingBindingElement创建的MessageEncoder用于生成一个HttpRequestMessage对象,该对象进而被封装成一个HttpMessage对象。后面传入ASP.NET Web API消息处理管道的HttpRequestMessage是直接通过调用GetHttpRequestMessage方法从该HttpMessage对象中提取的。
当ASP.NET Web API消息处理管道完成了请求的处理并最终生成一个HttpResponseMessage对象表示最终的响应,该对象同样先被封装成一个HttpMessage对象。在通过传输层将响应返回给客户端之前,同样需要利用HttpMessageEncodingBindingElement创建的MessageEncoder对其进行解码,而解码的内容实际上就是调用GetHttpResponseMessage方法提取的HttpResponseMessage。
当我们采用Self Host寄宿模式将一个非Web的应用程序作为Web API的宿主时,实际上最终网络监听任务是由HttpBinding创建的ChannelListener来完成的,HttpBinding创建的信道栈最终实现了对请求的接收和对响应的发送。为了让读者对此具有深刻的认识,我们通过一个简单的实例来演示如何使用HttpBinding实现对请求的监听、接收和响应。
我们创建一个空的控制台程序作为监听服务器,它相当于Self Host寄宿模式下的宿主程序。在添加相应程序集引用后,我们定义了如下一个Main方法。在这个方法中我们创建了一个HttpBinding,然后调用其BuildChannelListener<IReplyChannel>方法创建了一个ChannelListener对象,并且指定了监听地址("http://127.0.0.1:3721")。在开启ChannelListener之后,我们调用其AcceptChannel方法创建了Channel对象,并调用Open方法将其开启。我们最后在一个While循环中调用Channel对象的ReceiveRequest方法进行请求的监听和接收。
class Program { static void Main(string[] args) { Uri listenUri = new Uri("http://127.0.0.1:3721"); Binding binding = new HttpBinding(); //创建、开启信道监听器 IChannelListener<IReplyChannel> channelListener = binding.BuildChannelListener<IReplyChannel>(listenUri); channelListener.Open(); //创建、开启回复信道 IReplyChannel channel = channelListener.AcceptChannel(TimeSpan.MaxValue); channel.Open(); //开始监听 while (true) { //接收输出请求消息 RequestContext requestContext = channel.ReceiveRequest(TimeSpan.MaxValue); PrintRequestMessage(requestContext.RequestMessage); //消息回复 requestContext.Reply(CreateResponseMessage()); } } }
对于成功接收的消息,我们调用具有如下定义的PrintRequestMessage方法将相关的信息打印出来。通过上面的介绍我们知道这个接收到的消息实际上是一个HttpMessage对象,由于这是一个内部类型,所以我们只能以反射的方式调用其GetHttpRequestMessage方法获取被封装的HttpRequestMessage对象。得到表示请求的HttpRequestMessage对象之后,我们将请求地址和所有报头输出到控制台上。
private static void PrintRequestMessage(Message message) { MethodInfo method = message.GetType().GetMethod("GetHttpRequestMessage"); HttpRequestMessage request = (HttpRequestMessage)method.Invoke(message, new object[]{false}); Console.WriteLine("{0, -15}:{1}", "RequestUri", request.RequestUri); foreach (var header in request.Headers) { Console.WriteLine("{0, -15}:{1}", header.Key, string.Join("," ,header.Value.ToArray())); } }
在对请求进行处理之后,我们需要创建一个消息对该请求予以响应,响应消息的创建是通过具有如下定义的。如下面的代码片断所示,我们定义了一个Employee类型,而响应的内容就是一个序列化为JSON的Employee对象。我们首先创建了一个响应状态为“200, OK”的HttpResponseMessage对象,并将其Content属性设置为一个ObjectContent<Employee>对象。顾名思义,ObjectContent直接将指定的对象作为响应的内容,对指定对象的序列化通过指定的MediaTypeFormatter来完成。在这里的ObjectContent是对一个Employee对象的封装,并利用指定的JsonMediaTypeFormatter对象将其序列化成JSON格式。
private static Message CreateResponseMessage() { HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); Employee employee = new Employee("001","Zhang San","123456", "[email protected]"); response.Content = new ObjectContent<Employee>(employee, new JsonMediaTypeFormatter()); string httpMessageTypeName = "System.Web.Http.SelfHost.Channels.HttpMessage, System.Web.Http.SelfHost"; Type httpMessageType = Type.GetType(httpMessageTypeName); return (Message)Activator.CreateInstance(httpMessageType, new object[] { response }); } public class Employee { public string Id { get; set; } public string Name { get; set; } public string PhoneNo { get; set; } public string EmailAddress { get; set; } public Employee(string id, string name, string phoneNo, string emailAddress) { this.Id = id; this.Name = name; this.PhoneNo = phoneNo; this.EmailAddress = emailAddress; } }
最终的响应消息依然是一个HttpMessage对象,它是对我们创建的HttpResponseMessage对象的封装。限于“内部类型”的限制,我们也不得不采用反射的方式来创建这么一个HttpMessage对象。
当我们直接运行该程序后,实际上就已经启动了针对基地址"http://127.0.0.1:3721"的监听器。现在我们通过浏览器对这个监听器发起请求,为了使请求更像一个针对Web API的调用,我们将请求地址设置为“http://127.0.0.1:3721/employees/001”(看起来好像是获取某个编号为001的员工信息)。如右图所示,通过浏览器发送的请求相关信息会显示在控制台上,而浏览器上也会显示基于JSON格式的员工信息。