暂时未整理 已打印

一个http请求的详细过程

我们来看当我们在浏览器输入http://www.mycompany.com:8080/mydir/index.html,幕后所发生的一切。

首先http是一个应用层的协议,在这个层的协议,只是一种通讯规范,也就是因为双方要进行通讯,大家要事先约定一个规范。

1.连接 当我们输入这样一个请求时,首先要建立一个socket连接,因为socket是通过ip和端口建立的,所以之前还有一个DNS解析过程,把www.mycompany.com变成ip,如果url里不包含端口号,则会使用该协议的默认端口号。

DNS的过程是这样的:首先我们知道我们本地的机器上在配置网络时都会填写DNS,这样本机就会把这个url发给这个配置的DNS服务器,如果能够找到相应的url则返回其ip,否则该DNS将继续将该解析请求发送给上级DNS,整个DNS可以看做是一个树状结构,该请求将一直发送到根直到得到结果。现在已经拥有了目标ip和端口号,这样我们就可以打开socket连接了。

2.请求 连接成功建立后,开始向web服务器发送请求,这个请求一般是GETPOST命令(POST用于FORM参数的传递)。GET命令的格式为:  GET 路径/文件名 HTTP/1.0
文件名指出所访问的文件,HTTP/1.0指出Web浏览器使用的HTTP版本。现在可以发送GET命令:

GET /mydir/index.html HTTP/1.0

3.应答 web服务器收到这个请求,进行处理。从它的文档空间中搜索子目录mydir的文件index.html。如果找到该文件,Web服务器把该文件内容传送给相应的Web浏览器。

为了告知浏览器,,Web服务器首先传送一些HTTP头信息,然后传送具体内容(即HTTP体信息),HTTP头信息和HTTP体信息之间用一个空行分开。
常用的HTTP头信息有:
  ① HTTP 1.0 200 OK  这是Web服务器应答的第一行,列出服务器正在运行的HTTP版本号和应答代码。代码"200 OK"表示请求完成。
  ② MIME_Version:1.0 它指示MIME类型的版本。
  ③ content_type:类型 这个头信息非常重要,它指示HTTP体信息的MIME类型。如:content_type:text/html指示传送的数据是HTML文档。
  ④ content_length:长度值 它指示HTTP体信息的长度(字节)。


4.关闭连接:当应答结束后,Web浏览器与Web服务器必须断开,以保证其它Web浏览器能够与Web服务器建立连接。


下面我们具体分析其中的数据包在网络中漫游的经历

在网络分层结构中,各层之间是严格单向依赖的。“服务”是描述各层之间关系的抽象概念,即网络中各层向紧邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语(primitive),如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。网络中的n层总要向n+1层提供比n-1层更完备的服务,否则n层就没有存在的价值。

传输层实现的是“端到端”通信,引进网间进程通信概念,同时也要解决差错控制,流量控制,数据排序(报文排序),连接管理等问题,为此提供不同的服务方式。通常传输层的服务通过系统调用的方式提供,以socket的方式。对于客户端,要想建立一个socket连接,需要调用这样一些函数socket() bind() connect(),然后就可以通过send()进行数据发送。

现在看数据包在网络中的穿行过程:

应用层

首先我们可以看到在应用层,根据当前的需求和动作,结合应用层的协议,有我们确定发送的数据内容,我们把这些数据放到一个缓冲区内,然后形成了应用层的报文data。

传输层

这些数据通过传输层发送,比如tcp协议。所以它们会被送到传输层处理,在这里报文打上了传输头的包头,主要包含端口号,以及tcp的各种制信息,这些信息是直接得到的,因为接口中需要指定端口。这样就组成了tcp的数据传送单位segment。tcp是一种端到端的协议,利用这些信息,比如tcp首部中的序号确认序号,根据这些数字,发送的一方不断的进行发送等待确认,发送一个数据段后,会开启一个计数器,只有当收到确认后才会发送下一个,如果超过计数时间仍未收到确认则进行重发,在接受端如果收到错误数据,则将其丢弃,这将导致发送端超时重发。通过tcp协议,控制了数据包的发送序列的产生,不断的调整发送序列,实现流控和数据完整。

网络层

然后待发送的数据段送到网络层,在网络层被打包,这样封装上了网络层的包头,包头内部含有源及目的的ip地址,该层数据发送单位被称为packet。网络层开始负责将这样的数据包在网络上传输,如何穿过路由器,最终到达目的地址。在这里,根据目的ip地址,就需要查找下一跳路由的地址。首先在本机,要查找本机的路由表,在windows上运行route print就可以看到当前路由表内容,有如下几项:
Active Routes Default Route Persistent Route.

整个查找过程是这样的:
(1)根据目的地址,得到目的网络号,如果处在同一个内网,则可以直接发送。
(2)如果不是,则查询路由表,找到一个路由。
(3)如果找不到明确的路由,此时在路由表中还会有默认网关,也可称为缺省网关,IP用缺省的网关地址将一个数据传送给下一个指定的路由器,所以网关也可能是路由器,也可能只是内网向特定路由器传输数据的网关。
(4)路由器收到数据后,它再次为远程主机或网络查询路由,若还未找到路由,该数据包将发送到该路由器的缺省网关地址。而数据包中包含一个最大路由跳数,如果超过这个跳数,就会丢弃数据包,这样可以防止无限传递。路由器收到数据包后,只会查看网络层的包裹数据,目的ip。所以说它是工作在网络层,传输层的数据对它来说则是透明的。

如果上面这些步骤都没有成功,那么该数据报就不能被传送。如果不能传送的数据报来自本机,那么一般会向生成数据报的应用程序返回一个“主机不可达”或 “网络不可达”的错误。

 

windows下主机的路由表为例,看路由的查找过程
======================================================================
Active Routes:
Network Destination            Netmask                      Gateway              Interface                  Metric
0.0.0.0                                 0.0.0.0                       192.168.1.2           192.168.1.101           10
127.0.0.0                             255.0.0.0                   127.0.0.1               127.0.0.1                   1
192.168.1.0                         255.255.255.0           192.168.1.101       192.168.1.101           10
192.168.1.101                     255.255.255.255       127.0.0.1               127.0.0.1                   10
192.168.1.255                     255.255.255.255       192.168.1.101       192.168.1.101           10
 224.0.0.0                            240.0.0.0                   192.168.1.101       192.168.1.101           10
255.255.255.255                 255.255.255.255       192.168.1.101       192.168.1.101           1
Default Gateway:                192.168.1.2

Network Destination 目的网段  
Netmask 子网掩码  
Gateway 下一跳路由器入口的ip,路由器通过interfacegateway定义一调到下一个路由器的链路,通常情况下,interfacegateway是同一网段的。
Interface 到达该目的地的本路由器的出口ip(对于我们的个人pc来说,通常由机算机A的网卡,用该网卡的IP地址标识,当然一个pc也可以有多个网卡)。

网关这个概念,主要用于不同子网间的交互,当两个子网内主机A,B要进行通讯时,首先A要将数据发送到它的本地网关,然后网关再将数据发送给B所在的网关,然后网关再发送给B
默认网关,当一个数据包的目的网段不在你的路由记录中,那么,你的路由器该把那个数据包发送到哪里!缺省路由的网关是由你的连接上的default gateway决定的,也就是我们通常在网络连接里配置的那个值。

通常interfacegateway处在一个子网内,对于路由器来说,因为可能具有不同的interface,当数据包到达时,根据Network Destination寻找匹配的条目,如果找到,interface则指明了应当从该路由器的那个接口出去,gateway则代表了那个子网的网关地址。

第一条      0.0.0.0   0.0.0.0   192.168.1.2    192.168.1.101   10
0.0.0.0代表了缺省路由。该路由记录的意思是:当我接收到一个数据包的目的网段不在我的路由记录中,我会将该数据包通过192.168.1.101这个接口发送到192.168.1.2这个地址,这个地址是下一个路由器的一个接口,这样这个数据包就可以交付给下一个路由器处理,与我无关。该路由记录的线路质量 10。当有多个条目匹配时,会选择具有较小Metric值的那个。

第三条      192.168.1.0   255.255.255.0  192.168.1.101   192.168.1.101  10
直联网段的路由记录:当路由器收到发往直联网段的数据包时该如何处理,这种情况,路由记录的interfacegateway是同一个。当我接收到一个数据包的目的网段是192.168.1.0时,我会将该数据包通过192.168.1.101这个接口直接发送出去,因为这个端口直接连接着192.168.1.0这个网段,该路由记录的线路质量 10 (因interfacegateway是同一个,表示数据包直接传送给目的地址,不需要再转给路由器)。

一般就分这两种情况,目的地址与当前路由器接口是否在同一子网。如果是则直接发送,不需再转给路由器,否则还需要转发给下一个路由器继续进行处理。

 

查找到下一跳ip地址后,还需要知道它的mac地址,这个地址要作为链路层数据装进链路层头部。这时需要arp协议,具体过程是这样的,查找arp缓冲,windows下运行arp -a可以查看当前arp缓冲内容。如果里面含有对应ipmac地址,则直接返回。否则需要发生arp请求,该请求包含源的ipmac地址,还有目的地的ip地址,在网内进行广播,所有的主机会检查自己的ip与该请求中的目的ip是否一样,如果刚好对应则返回自己的mac地址,同时将请求者的ip mac保存。这样就得到了目标ipmac地址。

链路层

mac地址及链路层控制信息加到数据包里,形成Frame,Frame在链路层协议下,完成了相邻的节点间的数据传输,完成连接建立,控制传输速度,数据完整。

物理层

物理线路则只负责该数据以bit为单位从主机传输到下一个目的地。

下一个目的地接受到数据后,从物理层得到数据然后经过逐层的解包 到 链路层 到 网络层,然后开始上述的处理,在经网络层 链路层 物理层将数据封装好继续传往下一个地址。

在上面的过程中,可以看到有一个路由表查询过程,而这个路由表的建立则依赖于路由算法。也就是说路由算法实际上只是用来路由器之间更新维护路由表,真正的数据传输过程并不执行这个算法,只查看路由表。这个概念也很重要,需要理解常用的路由算法。而整个tcp协议比较复杂,跟链路层的协议有些相似,其中有很重要的一些机制或者概念需要认真理解,比如编号与确认,流量控制,重发机制,发送接受窗口。

 

tcp/ip基本模型及概念


物理层

设备,中继器(repeater,集线器(hub)。对于这一层来说,从一个端口收到数据,会转发到所有端口。


链路层

协议:SDLCSynchronous Data Link ControlHDLCHigh-level Data Link Control) ppp协议独立的链路设备中最常见的当属网卡,网桥也是链路产品。集线器MODEM的某些功能有人认为属于链路层,对此还有些争议认为属于物理层设备。除此之外,所有的交换机都需要工作在数据链路层,但仅工作在数据链路层的仅是二层交换机。其他像三层交换机、四层交换机和七层交换机虽然可对应工作在OSI的三层、四层和七层,但二层功能仍是它们基本的功能。

因为有了MAC地址表,所以才充分避免了冲突,因为交换机通过目的MAC地址知道应该把这个数据转发到哪个端口。而不会像HUB一样,会转发到所有滴端口。所以,交换机是可以划分冲突域滴。


网络层

四个主要的协议:   
网际协议IP:负责在主机和网络之间寻址和路由数据包。     
地址解析协议ARP:获得同一物理网络中的硬件主机地址。     
网际控制消息协议ICMP:发送消息,并报告有关数据包的传送错误。     
互联组管理协议IGMP:被IP主机拿来向本地多路广播路由器报告主机组成员。

该层设备有三层交换机,路由器。


传输层

两个重要协议 TCP 和 UDP 

端口概念:TCP/UDP 使用 IP 地址标识网上主机,使用端口号来标识应用进程,即 TCP/UDP 用主机 IP 地址和为应用进程分配的端口号来标识应用进程。端口号是 16 位的无符号整数, TCP 的端口号和 UDP 的端口号是两个独立的序列。尽管相互独立,如果 TCP 和 UDP 同时提供某种知名服务,两个协议通常选择相同的端口号。这纯粹是为了使用方便,而不是协议本身的要求。利用端口号,一台主机上多个进程可以同时使用 TCP/UDP 提供的传输服务,并且这种通信是端到端的,它的数据由 IP 传递,但与 IP 数据报的传递路径无关。网络通信中用一个三元组可以在全局唯一标志一个应用进程:(协议,本地地址,本地端口号)。

也就是说tcpudp可以使用相同的端口。

可以看到通过(协议,源端口,源ip,目的端口,目的ip)就可以用来完全标识一组网络连接。

应用层

基于tcpTelnet FTP SMTP DNS HTTP 
基于udpRIP NTP(网落时间协议)和DNS DNS也使用TCPSNMP TFTP

 深入理解IIS工作原理 

基本概念:

1. 站点程序:在IIS中我们所新建的站点,可以为其配置ASP.net以及应用程序池的版本。

2. 虚拟目录:用来将不同的文件夹中的内容通过虚拟目录组织到同一个文件夹中,相当于一个应用程序,也对应着一个应用程序池。

3. 应用程序池:《微软概念》是将一个或多个应用程序连接到一个或多个工作进程集合的配置。因为应用程序池中的应用程序与其他应用程序被工作进程边界分隔,所以某个应用程序池中的应用程序不会受到其他应用程序池中的应用程序所产生的问题的影响。

4. 工作进程隔离模式:防止一个应用程序或站点停止了而影响另一个应用程序或站点。应用程序池直接从内核接收请求而非WWW服务。

5. W3WP.exeIIS的工作进程池,每一个w3wp.exeIIS的一个web园,一般在IIS6.0中出现,一般的工作进程池只有一个web园,但是如果在工作进程池的web园中最大工作进程数被修改可能出现多个w3wp.exe,或者多个工作进程池同事工作也可能出现多个w3wp.exe。它的启动用户是系统内置安全账户network service

6. Web Gargen:指一个应用程序可以再多个进程(w3wp.exe)中来执行,一次请求使用其中的一个。用这个的主要目的是提高程序的可用性。当其中一个进程发生错误,那么也不会影响其他进程。发生错误的进程可以根据规则关闭,而其他的进程则可以继续工作。

对于IIS6IIS7,每个应用程序池都会创建一个w3wp.exe进程。但是并不是所有情况都是一个应用程序池对应一个w3wp.exe进程。web garden。或者一些异常发生的时候,就会一个应用程序池对应多个w3wp.exe进程。应用程序池会在没有请求的时候定时回收,或者发生错误的时候,自动重新建立一个处理进程w3wp.exe进程,如果运气好,就会看到没有配置web garden时,一个应用程序池对应多个w3wp.exe进程。

同样可以对虚拟目录来设置应用程序池,设置方法和设置站点的应用程序池是一样的。同样也可以进行Web Gargen的配置。

在应用程序池的属性选项卡中,能找到关于IIS对于w3wp进程的监控功能,它提供时间参数设置(隔多少时间来ping一次工作进程),在多少时间内未返回值则将此进程视为timeout,或者死掉的状态,具体数值可以再IIS手册中查到相关值,此时我们可以通过使用程序语言读取系统日志文件来跟踪到此进程的ID,之后可以使用IISAPP.vbs得到该进程对应的应用程序池,从而可以定位到该程序池所对应的应用程序以及可以通过对应用程序池操作来恢复此故障。

关于HTTP请求的处理(ASP级别)

IIS 6ASP.net请求处理过程:

IIS5.X是通过Inetlnfo.exe监听request并把request分发到work process。换句话说,在IIS5.X中对request的监听和分发是在user mode中进行,在IIS6中,这种工作被移植到kernel mode中进行,所有的这一切都是通过一个新的组件:http.sys来负责。

PS:免用户应用程序访问或者修改关键的操作系统数据,windows提供了两种处理器访问模式:用户模式(User Mode)和内核模式(kernel mode)。一般地,用户程序运行在user mode下,而操作系统代码运行在kernel mode下,kernel mode的代码允许访问所有系统内存和所有CPU指令。在user mode下,http.sys接收到一个机遇aspxhttp request,然后它会根据IIS中的Metabase查看该基于该requestapplication属于哪个application pool,如果该application pool不存在,则创建之,否则直接将request发送到对应的application poolqueue中。每个application pool对应着一个worker processsw3wp.exe,毫无疑问他是运行在user mode下的。在IIS Metabse中维护着application pool 和 worker processMappingWASWeb Administrative Service根据这样一个mapping,将存在于某个application poos queuerequest传递到对应的worder process(如果没有,就穿件一个这样的进程)。在worker process初始化的时候,加载ASP.net ISAPIasp.net ISAPI进而加载CLR,通过APPManagerAppDomainFactoryCreate方法为Application穿件一个Application Domain,通过ISAPIRuntimeProcessRequest处理Request,进而将流程进入到ASP.net http runtime pipeline

ASP.NET页面与IIS底层交互和工作原理详解 

第一回:

 

引言

 

我查阅过不少Asp.Net的书籍,发现大多数作者都是站在一个比较高的层次上讲解Asp.Net。他们耐心、细致地告诉你如何一步步拖放控件、设置控件属性、编写CodeBehind代码,以实现某个特定的功能。

 

这种做法,实际上是回答了“如何去做”的问题,却没有回答“为什么可以这样做”的问题。

 

尽管我很推崇 悉江华 先生的《圣殿祭祀的Asp.Net开发详解》一书,但当我翻看了一下其对角色(Role) 和 用户(Member)的讲解时,我决定跳过去直接读后面的章节。因为我发现他也随了大流,对这部分的讲解停留在“如何去做”的层面上。我相信像悉先生 这样的牛人是不可能不了解底层运作原理的,仅仅是因为那本书原本就已经很厚了吧。

 

当你按“如何去做”所讲解的内容去开发程序的时候,对于你的用户,你仍是一名程序员;但对于实现了MembershipProvider 和 RoleProvider 抽象类的微软开发人员来说,你已经成了他们的一个用户。

 

NOTE:我既不反对一些作者只讲解“如何去做”,也不反对你只学“如何去做”,这样也有它的好处,就是可以快速开发。我只是建议多掌握一点底层知识,对一些问题会有更好的理解。

 

希望通过这一系列文章的讲解,可以让你更好的理解Asp.Net的运作原理和做以了解。

Http请求处理流程概述

 

思考“为什么在地址栏输入www.tracefact.net就可以看到张子阳的个人空间?”,类似于思考“为什么苹果是往地上掉不是往天上飘?”。对于普通访问者来说,这就像每天太阳东边升起西边落下一样是理所当然的;对于很多程序员来说,认为这个与己无关,不过是系统管理员或者网管员的责任。毕竟,IIS是 Windows 的一个组件,又不是 Asp.Net 的一个组成部分。而实际上,从你轻拍回车到页面呈现在你眼前的十分之一秒内,IIS.Net Framework已经做了大量的幕后工作。

 

你可能觉得了解这些幕后工作是如何运作的无关紧要,作为程序员的你只要保证开发出的程序可以高效地运行就可以了。然而,在开发过程中,你却发现常常需要使用诸如 HttpContext 这样的类。这个时候,你可曾思考过这些类的构成和类的实体是如何创建的?你可能简单地回答:HttpContext代表当前请求的一个上下文环境。可你又知道IIS FrameworkAsp.Net 是如何协同工作处理每个Http请求、如何区分不同的请求、IISFrameworkAsp.Net三者之间的数据如何流动么?

 

回答上面这些问题,首先需要了解IIS是如何处理页面请求的,这也是理解 Form验证模式和Windows 验证模式 的基础。

Http请求刚刚到达服务器的时候

 

当服务器接收到一个 Http请求的时候,IIS 首先需要决定如何去处理这个请求(NOTE:服务器处理一个.htm页面和一个.aspx页面肯定是不一样的么)。那IIS依据什么去处理呢?―― 根据文件的后缀名。

 

服务器获取所请求的页面(NOTE:也可以是文件,比如 jimmy.jpg)的后缀名以后,接下来会在服务器端寻找可以处理这类后缀名的应用程序,如果IIS找不到可以处理此类文件的应用程序,并且这个文件也没有受到服务器端的保护(NOTE:一个受保护的例子就是 App_Code中的文件,一个不受保护的例子就是你的js脚本),那么IIS将直接把这个文件返还给客户端。

 

能够处理各种后缀名的应用程序,通常被称为 ISAPI 应用程序(NOTEInternet Server Application Programe Interface,互联网服务器应用程序接口)。虽然这 ISAPI 听上去还挺气派,也算是“应用程序”呢,但仔细看看它的全称就明白了:它实际上只是一个接口,起到一个代理的作用,它的主要工作是映射所请求的页面(文件)  和与此后缀名相对应的实际的处理程序。

 

让我们更进一步地看一下 ISAPI ,看看它到底是什么样子,请按下面的步骤进行:

 

打开IIS

选择随意一个站点,鼠标右键,“属性”。

选择“主目录”选项卡。

选择“配置”。

你应该会看到如下的画面:

1. 应用程序配置

 

很清楚地就可以看到,所有IIS所能处理,或者叫 ISAPI 所提供代理服务的 文件类型 及其相对应的实际的后台处理程序都在这里清楚地列出来了。

我们找到 .aspx 的应用处理程序,然后点“编辑”,会出现下面的画面:

2. 编辑.aspx文件的处理程序

 

 

 

一路看到这里,可以看出,所有的.aspx文件实际上都是由 aspnet_isapi.dll 这个程序来处理的,当IIS把对于.aspx页面的请求提交给了aspnet_isapi.dll以后,它就不再关心这个请求随后是如何处理的了。现在我们应该知道:Asp.Net 只是服务器(IIS)的一个组成部分而已,它是一个 ISAPI扩展。

 

这里需要注意两点:

 

  1. 当你修改“限制为”后,可以限制页面(文件)只能以某种特定方式访问
  2. “确认文件是否存在”是实现 URL 地址映射的关键选项,我以后会专门讲述。

理解宿主环境(Hosting

 

从本质上讲,Asp.Net 主要是由一系列的类组成,这些类的主要目的就是将Http请求转变为对客户端的响应。HttpRuntime类是Asp.Net的一个主要入口,它有一个称作 ProcessRequest 的方法,这个方法以一个 HttpWorkerRequest 类作为参数。HttpRuntime 类几乎包含着关于单个 Http请求的所有信息:所请求的文件、服务器端变量、QueryStringHttp 头信息 等等。Asp.Net 使用这些信息来加载、运行正确的文件,并且将这个请求转换到输出流中,一般来说,也就是HTML页面。

 

NOTE:二般来说,也可以是张图片。

 

当 Web.config文件的内容发生改变 或者 .aspx文件发生变动的时候,为了能够卸载运行在同一个进程中的应用程序(NOTE:卸载也是为了重新加载),Http请求被分放在相互隔离的应用程序域中。

 

NOTE:可能你以前就听过应用程序域,但是不了解怎么回事,应用程序域就是 AppDomain

 

对于IIS来说,它依赖一个叫做 HTTP.SYS 的内置驱动程序来监听来自外部的 HTTP请求。在操作系统启动的时候,IIS首先在HTTP.SYS中注册自己的虚拟路径。

 

NOTE:实际上相当于告诉HTTP.SYS哪些URL是可以访问的,哪些是不可以访问的。举个简单的例子:为什么你访问不存在的文件会出现 404 错误呢?就是在这一步确定的。

 

如果请求的是一个可访问的URLHTTP.SYS会将这个请求交给 IIS 工作者进程。

 

NOTEIIS6.0中叫做 w3wp.exeIIS5.0中叫做 aspnet_wp.exe

 

每个工作者进程都有一个身份标识 以及 一系列的可选性能参数。

 

NOTE:可选性能参数,是指诸如 回收机制的设置、超时时间设置 等等。

 

接下来进行的事情就是上一章节讲述的 ISAPI 了。

 

NOTE:这部分的内容相关性比较强,为了让大家好理解,我最后还是决定把 ISAPI 放到前面了,可能全系列完成的时候会再调整吧。

 

除了映射文件与其对应的处理程序以外,ISAPI 还需要做一些其他的工作:

a) 从HTTP.SYS中获取当前的Httq请求信息,并且将这些信息保存到 HttpWorkerRequest 类中。

b) 在相互隔离的应用程序域AppDomain中加载HttpRuntime

c) 调用 HttpRuntimeProcessRequest方法。

接下来才是程序员通常编写的代码所完成的工作了,然后,IIS 接收返回的数据流,并重新返还给 HTTP.SYS,最后,HTTP.SYS 再将这些数据返回给客户端浏览器。

OK,现在你看到张子阳的空间主页了。

3.Asp.Net 的宿主环境

 

理解管道(Pipeline

在前面两章中,我们在一个相对比较低的层次上讨论了从发出Http请求到看到浏览器输出这转瞬即逝的十分之一秒内IIS和 Framework 所做的事情。但是我们忽略了一个细节:程序员编写的代码是如何在这一过程中衔接的,本章我们就来看看这个问题。

当Http请求进入 Asp.Net Runtime以后,它的管道由托管模块(NOTE:Managed Modules)和处理程序(NOTE:Handlers)组成,并且由管道来处理这个 Http请求。

4. 理解 Http 管道

 

 

我们按编号来看一下这幅图中的数据是如何流动的。

1. HttpRuntime将Http请求转交给 HttpApplication,HttpApplication代表着程序员创建的Web应用程序。HttpApplication创建针对此Http请求的 HttpContext对象,这些对象包含了关于此请求的诸多其他对象,主要是HttpRequest、HttpResponse、HttpSessionState等。这些对象在程序中可以通过Page类或者Context类进行访问。、

2. 接下来Http请求通过一系列Module,这些Module对Http请求具有完全的控制权。这些Module可以做一些执行某个实际工作前的事情。

3. Http请求经过所有的Module之后,它会被HttpHandler处理。在这一步,执行实际的一些操作,通常也就是.aspx页面所完成的业务逻辑。可能你会觉得在创建.aspx页面并没有体会到这一过程,但是,你一定知道,.aspx 页面继承自Page类,我们看一下Page类的签名:

public class Page : TemplateControl, IHttpHandler{
    // 代码省略
}

可以看到,Page类实现了IHttpHandler接口,HttpHandler也是Http请求处理的最底层。

4.HttpHandler处理完以后,Http请求再一次回到Module,此时Module可以做一些某个工作已经完成了之后的事情。

NOTE:注意我用红色标识的字,然后回想一下:Asp.Net 中是不是有众多的 Inserting 、Inserted 之类成对的事件?其实,这里讲述的就是为什么Asp.Net可以将一个Insert操作分成前后两部分,然后再分别进行事件拦截的幕后原理。

如果我们将注意力只集中在Http请求、HttpHandler和HttpModule上,不去考虑HttpContext和HttpApplication,那么图4.可以简化成下面这样:

图5.Http请求在HttpHandler 和 HttpModule 中的流动方向

 

总结

 

本文中,我首先概要介绍了这系列文章将要为大家讲述的主题。然后,我提出了部分程序员存在的一个问题:在一个比较高的层次上学习和使用Asp.Net。

 

随后,我以一个访问我个人空间首页的例子,引出了本文主要讲述的三个内容:

 

Http请求刚刚到达时IIS时,IIS 所做的工作。

Http请求的宿主环境。

Http管道。

希望这篇文章能给你带来帮助。

 

第二回:

 

引言

 

在 Part.1 Http请求处理流程 一文中,我们了解了Http请求的处理过程以及其它一些运作原理。我们知道Http管道中有两个可用接口,一个是IHttpHandler,一个是IHttpModule,但在Part.1中,我并没有详细讲述如何对它们进行编程,只是轻描淡写地一笔带过。所谓学以致用,前面已经介绍了不少概念和原理。在本文中,我们通过几个范例来了解 IHttpHandler,看看掌握这些原理的实际用途。

 

IHttpHandler 概述

 

可能和我一样,很多Asp.Net开发人员都有过Asp的背景,以至于我们在开发程序的时候,通常都是在“页面级”上思考,也就是说我们现在正在做的这个页面应该有什么样的功能,是进行一个问卷调查还是一个数据库查询等等。而很少在“请求级”思考,考虑有没有办法来通过编码的方式来操控一个Http请求。

 

实际上,Framework提供了一系列的接口和类,允许你对于Http请求进行编程,而实现这一操作的一个主要的接口,就是 IHttpHandler(另一个是IHttpModule)。

 

应该还记得第一节中我们提到过 ISAPI,它根据文件名后缀把不同的请求转交给不同的处理程序。但是仔细看看就会发现:几乎一大半的文件都交给 aspnet_isapi.dll 去处理了。很明显,aspnet_isapi.dll 不可能对每种文件采用同一种方式处理,那么 aspnet_isapi.dll 是如何更进一步处理不同的文件,交由谁去处理呢?为了搞清楚这个问题,我们需要打开机器上C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\ 目录下的web.config 文件。

 

NOTE:我查阅了很多资料,都说是在 machine.config 中,但实际上 v2.0.50727 下的machine.config中httpHandlers结点是这样的:<httpHandlers />,并没有给出详细的处理程序,在Web.config中才能看到。而v1.1.4322 下的machine.config中却有。

 

找到httpHandlers结点,应该可以看到如下这样的代码(做了省略):

 

<httpHandlers>

... ... //略

<add path="*.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" /><add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="True" />

   <add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="True" />

   <add path="*.asax" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />

<add path="*.ascx" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />

   <add path="*.config" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />

   <add path="*.cs" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />

   <add path="*" verb="GET,HEAD,POST" type="System.Web.DefaultHttpHandler" validate="True" />

   ... ... //略 

</httpHandlers>

 

可以看到,在<httpHandlers>结点中将不同的文件类型映射给不同的Handler去处理,对于.aspx来说,是由System.Web.UI.PageHandlerFactory来处理。而对于.cs来说,是由System.Web.HttpForbiddenHandler 处理,从ForbiddenHandler名字中出现的Forbidden (翻译过来是“禁止”)可以看出,这个Handler可以避免我们的源码被看到。

 

NOTE:System.Web.UI.PageHandlerFactory 是一个IHttpHandlerFactory,而不是一个单一的HttpHandler,IHttpHandlerFactory用来做什么后面会说明。

 

上面列出的是.Net Framework在处理Http请求时的所采用的默认Handler。而如果我们要用编程的方式来操控一个Http请求,我们就需要实现IHttpHandler接口,来定制我们自己的需求。

 

IHttpHandler的定义是这样的:

 

public interface IHttpHandler{

    void ProcessRequest(HttpContext context);

    bool IsReusable { get; }

}

 

由上面可以看出IHttpHandler要求实现一个方法和一个属性。其中 ProcessRequest,从名字(处理请求)看就知道这里应该放置我们处理请求的主要代码。

 

IsReusable属性,MSDN上是这样解释的:获取一个值,该值指示其他请求是否可以使用 IHttpHandler 实例。也就是说后继的Http请求是不是可以继续使用实现了该接口的类的实例,一般来说,我把它设置成true。

 

那么实现此接口的类形式应该是这样的:

 

public class CustomHandler : IHttpHandler{

    public void ProcessRequest(HttpContext context)  {

       // 处理请求的代码

    }

    public bool IsReusable {

       get { return true; }

    }

}

 

而为了能使用这个自定义的HttpHandler,我们需要在应用程序目录下的Web.config中注册它。

 

 <system.web>

    <httpHandlers>

      <add path="*.jpg" verb="*" type="MyNameSpace.MyClass, MyDllName" />

    </httpHandlers>

 </system.web>

 

应该发现这与之前在C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\目录下web.config中看到的几乎完全一样。这里,path指的是请求的文件名称,可以使用通配符扩大范围,也可以明确指定这个handler仅用于处理某个特定的文件(比如说:filename.aspx)的请求。verb指的是请求此文件的方式,可以是post或get,用*代表所有访问方式。type属性由“,”分隔成两部分,第一部分是实现了接口的类名,第二部分是位于Bin目录下的编译过的程序集名称。

 

NOTE:如果你新建一个项目,并且在项目下创建HandlerTest.cs,然后让站点引用该项目,那么在生成解决方案的时候会自动将编译好的.dll文件添到Bin目录中。

    NOTE:MyDll只写程序集名,不要加后面的.dll。

 

使用HttpHandler实现图片防盗链

 

有了之前这么多的准备知识,实现现在的目标就容易得多了:

 

NOTE:这个例子,以及下面的一个例子均来自于《Maximizing ASP.NET Real World, Object-Oriented Development》一书:

 

Step.1:创建文件 CustomHandler.cs,代码如下:

 

using System;

using System.Web;

 

namespace CustomHandler{

    public class JpgHandler : IHttpHandler{

       public void ProcessRequest(HttpContext context){

           // 获取文件服务器端物理路径

           string FileName = context.Server.MapPath(context.Request.FilePath);

           // 如果UrlReferrer为空,则显示一张默认的禁止盗链的图片

           if (context.Request.UrlReferrer.Host == null){

              context.Response.ContentType = "image/JPEG";

              context.Response.WriteFile("/error.jpg");

           }else{

              // 如果 UrlReferrer中不包含自己站点主机域名,则显示一张默认的禁止盗链的图片

             if (context.Request.UrlReferrer.Host.IndexOf("yourdomain.com") > 0){

                  context.Response.ContentType = "image/JPEG";

                  context.Response.WriteFile(FileName);

              }else{

                  context.Response.ContentType = "image/JPEG";

                  context.Response.WriteFile("/error.jpg");

              }

           }

       }

 

       public bool IsReusable{

           get{ return true; }

       }

    }

}

 

Step.2 编译这个文件

 

csc /t:library /r:System.Web.dll CustomHandler.cs

 

Step.3 将编译好的 CustomHandler.dll 拷贝到站点的 Bin 目录下。

 

Step.4 在Web.Config 中注册这个Handler。

 

<system.web>

    <httpHandlers>

      <add path="*.jpg" verb="*" type="CustomHandler.JpgHandler, CustomHandler" />

    </httpHandlers>

 </system.web>

 

OK,诸位可以按步骤自行测试一下,这里就不赘述了。

 

通过IhttpHandler实现图片验证码

 

也可以在一个.ashx文件中实现IHttpHandler,而不是采用这种提前编译的方式。

 

Step.1 打开Vs2005,“添加新项”,“一般处理程序”。新建文件后,VS会自动在文件中添加如下的代码:

 

<%@ WebHandler Language="C#" Class="Handler" %>

 

using System;

using System.Web;

 

public class Handler : IHttpHandler {

    public void ProcessRequest (HttpContext context) {

        context.Response.ContentType = "text/plain";

        context.Response.Write("Hello World");

    }

 

    public bool IsReusable {

        get {

            return false;

        }

    }

}

 

Step.2 将代码改写成如下所示:

 

<%@ WebHandler Language="C#" Class="Handler" %>

 

using System;

using System.Drawing;

using System.Drawing.Imaging;

using System.Text;

using System.Web;

using System.Web.SessionState;

 

public class Handler : IHttpHandler, IRequiresSessionState {

 

    public void ProcessRequest(HttpContext context) {

       context.Response.ContentType = "image/gif";

       //建立Bitmap对象,绘图

       Bitmap basemap = new Bitmap(200, 60);

       Graphics graph = Graphics.FromImage(basemap);

       graph.FillRectangle(new SolidBrush(Color.White), 0, 0, 200, 60);

       Font font = new Font(FontFamily.GenericSerif, 48, FontStyle.Bold, GraphicsUnit.Pixel);

       Random r = new Random();

       string letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ";

       string letter;

       StringBuilder s = new StringBuilder();

       

       //添加随机的五个字母

       for (int x = 0; x < 5; x++) {

           letter = letters.Substring(r.Next(0, letters.Length - 1), 1);

           s.Append(letter);

           graph.DrawString(letter, font, new SolidBrush(Color.Black), x * 38, r.Next(0, 15));

       }

       

       //混淆背景

       Pen linePen = new Pen(new SolidBrush(Color.Black), 2);

       for (int x = 0; x < 6; x++)

           graph.DrawLine(linePen, new Point(r.Next(0, 199), r.Next(0, 59)), new Point(r.Next(0, 199), r.Next(0, 59)));

              

       //将图片保存到输出流中       

       basemap.Save(context.Response.OutputStream, ImageFormat.Gif);

       context.Session["CheckCode"] = s.ToString();   //如果没有实现IRequiresSessionState,则这里会出错,也无法生成图片

       context.Response.End();      

    }

 

    public bool IsReusable {

       get { return true; }

    }

}

 

需要特别注意的是,Handler类不仅需要实现 IHttpHandler接口(这个显然),为了在这个Handler类中使用SessionState,还需要实现IRequiresSessionState接口,对于这个接口,MSDN的解释是这样的:Specifies that the target HTTP handler requires read and write access to session-state values. This is a marker interface and has no methods.(翻译过来是:指定当前Http Handler需要对SessionState值的读写访问权。这是一个标记接口,没有任何方法)。

 

而实际上,IRequiresSessionState的接口定义是这样的:

 

public interface IRequiresSessionState{}

 

可见,这个接口没有任何需要实现的方法或属性,大家只要记得:如果想在HttpHandler中使用SessionState,必须实现这个接口,实际上也就是在类的标头将这个接口加进去。

 

Step.3 新建一个ImageCode.aspx页面,在HTML代码中写下:

 

<img src="Handler.ashx" alt="图片验证码" />

 

OK,在浏览器中打开ImageCode.aspx,应该可以看到如下所示:

 

利用HttpHandler创建自定义后缀Rss源

 

RSS如今已经可以说是随处可见,而RSS的实现方式,通常是在一个.aspx的CodeBehind文件中写一个XML文件,然后加载到Response的OutputStream中, Rss源通常是Rss.aspx这种形式的。通过第一章学到的ISAPI的知识,再结合本章学到的关于HttpHandler的知识,很容易想到:我们可以自定一个以 .rss 作为后缀名的文件来实现 Rss 源,比如说Article.rss。现在我们就一步步来实现它:

 

NOTE:关于RSS的更多内容,可以参阅我编译的 在Web站点中创建和使用RSS源。本文不再解释Rss是什么,如何创建Rss源,为了文章的独立性,仅给出创建过程。

 

Step.1 创建范例数据库

 

Create Table RssSample

(

    SampleId      Int Identity(1,1)    Not Null,

    Title         Varchar(100)          Not Null Constraint uq_Title Unique,

    Author        Varchar(50)              Not Null,

    PubDate       DateTime              Not Null Default GetDate(),

    [Description] Varchar(500)          Not Null,

    Link          Varchar(150)          Not Null

 

    Constraint pk_RssSample Primary Key(SampleId)

)

-- 插入范例数据

Insert Into RssSample(Title, Author, [Description], Link)

Values('标题1', '作者1', '文章摘要1', 'http://127.0.0.1/#' )

 

-- 省略 ....

 

Step.2 建立站点,在App_Code目录下建立RssFeedsLib.cs文件。

 

using System;

using System.Data;

using System.Data.SqlClient;

using System.IO;

using System.Web;

using System.Xml;

using System.Text;

 

namespace RssFeadsLib {

    public class RssGenerator {

       public static string GetRSS() {

           MemoryStream ms = new MemoryStream();

           XmlTextWriter writer = new XmlTextWriter(ms, null);

           SqlConnection conn = new SqlConnection("Data Source=.;Initial Catalog=Sample;User ID=sa;Password=sa");       //修改这里成你的数据库连接

           SqlCommand cmd = new SqlCommand("select * from RssSample order by pubdate desc", conn);

 

           conn.Open();

           SqlDataReader reader = cmd.ExecuteReader();

           writer.WriteStartElement("rss");

           writer.WriteAttributeString("version", "2.0");

           writer.WriteStartElement("channel");

           // Channel 下的结点静态写入

           writer.WriteElementString("title", "TraceFact.Net 技术文章");

           writer.WriteElementString("link", "http://www.tracefact.net");

           writer.WriteElementString("description", "Dedicated to asp.net...");

           writer.WriteElementString("copyright", "Copyright (C) 2007");

           writer.WriteElementString("generator", "My RSS Generator");

           // Item 结点从数据库读取

           while (reader.Read()) {

              writer.WriteStartElement("item");

              writer.WriteElementString("author", reader.GetString(reader.GetOrdinal("Author")));

              writer.WriteElementString("title",             reader.GetString(reader.GetOrdinal("title")));

              writer.WriteElementString("link", reader.GetString(reader.GetOrdinal("Link")));

              writer.WriteElementString("description", reader.GetString(reader.GetOrdinal("Description")));

              writer.WriteElementString("pubDate", reader.GetDateTime(reader.GetOrdinal("PubDate")).ToString(@"ddd, dd MMM yyyy 12:00:00 tt "));

              writer.WriteEndElement();

           }

 

           writer.WriteEndElement();

           writer.WriteEndElement();

           reader.Close();

           conn.Close();

 

           writer.BaseStream.Flush();

           writer.Flush();

           ms.Flush();

 

           // 将流转换成String并返回

           byte[] data = new byte[ms.Length];

           ms.Seek(0, SeekOrigin.Begin);

           ms.Read(data, 0, data.Length);

           ms.Close();

           return UTF8Encoding.UTF8.GetString(data);

       }

    }

}

 

Step.3 创建可以处理 .rss 后缀名的 RssHandler

 

我们在这个 RssFeedsLib命名空间下,再添加一个类,这个类用于处理对 .rss 后缀名文件的Http请求。

 

public class RSSHandler:IHttpHandler{

    public bool IsReusable

    {

       get {return false;}

    }

 

    public void ProcessRequest(HttpContext context){

       context.Response.ContentType = "text/xml";

       string str = RssGenerator.GetRSS();

       context.Response.Write(str);

    }

}

 

Step.4 在Web.config中进行配置

 

<httpHandlers>

    <add path="*.rss" type="RssFeadsLib.RSSHandler" verb="GET" />

</httpHandlers>

 

 

 

NOTE:因为这个类和命名空间位于App_Code中,这里就不需要再手动编译RssFeadsLib.cs然后将编译好的.dll应用程序集放到Bin目录中了。至于为什么可以这样,将会在 《Asp.Net 构架与安全机制 Part.5 – 页面生存周期与编译模型》中解释。

 

Step.5 在IIS 对ISAPI进行设置。

 

应该还记得在Part.1中如何在IIS中设置ISAPI来进行文件与处理程序映射:

 

打开IIS,选择本范例所用的站点,右键,选择“属性”。

选择“主目录”选项卡,点击“配置...”按钮。

点击“添加”,设置“可执行文件”为“C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll”,设置“扩展名”为“.rss”,点“确定”。

注意,不要勾选“检查文件是否存在”复选框,这样不用创建文件,只要在地址栏输入任意以.rss后缀结尾的文件名,均会交由上面创建的Handler去处理,而不管这个文件是否存在,也不管请求的是Article.rss还是Sample.rss。

进行了这些设置以后,现在IIS就知道如何去处理对.rss后缀名文件的请求了。

 

Step.6 测试范例

 

这个时候,随便打开一个页面,比如空白的Default.aspx,然后我们在地址栏将文件改为:Article.rss(改成abc.rss也是一样),敲回车,应该可以看到如下的画面。

 

 

 

IHttpHandlerFactory 概述

 

现在假设我们有这样的需求,我们不仅想要处理 .rss 后缀名,还想要能够处理 .atom后缀名,假设处理atom的类命名为AtomHandler,那么我们的Web.config该如何设置呢?我想应该是这样的:

 

<httpHandlers>

<add path="*.rss" type="RssFeadsLib.RSSHandler" verb="GET" />

<add path="*.atom" type="RssFeadsLib.AtomHandler" verb="GET" />

</httpHandlers>

 

如果我们有很多个HttpHandler分别映射不同后缀名的请求,这样我们的Web.config会变得很冗长,或者,我们只有在程序运行时才能确切地知道使用哪个Handler,这个时候,可以考虑实现 IHttpHandlerFactory来完成这一过程。

 

IHttpHandlerFactory的定义是这样的:

 

public interface IHttpHandlerFactory{

    IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated);

    void ReleaseHandler(IHttpHandler handler);

}

 

可见,需要实现两个方法,分别是 GetHandler() 和 ReleaseHandler()。

 

GetHandler(),返回实现了IHttpHandler接口的类的实例。

ReleaseHandler(),使得Factory可以重复使用一个已经存在的Handler实例。

对于上面 .atom 和 .rss 的问题,我们可以这样来实现 IHttpHandlerFactory接口:

 

class HandlerFactory:IHttpHandlerFactory{

    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated){

       string path = context.Request.PhysicalPath;

       if (Path.GetExtension(path) == ".rss"){

           return new RSSHandler();

       }

 

       if (Path.GetExtension(path) == ".atom"){

           return new ATOMHandler();

       }

       return null;

    }

 

    public void ReleaseHandler(IHttpHandler handler){

    }

}

 

这时,在Web.Config 中<system.web>节点下进行如下设置即可:

 

<httpHandlers>

<add path="*.rss,*.atom" type=" RssFeadsLib.HandlerFactory" verb="GET" />

</httpHandlers>

 

但是,这不能简化IIS中ISAPI的设置,还是需要手动去对.rss和.atom分别设置。

 

总结

 

在本文中,我们首先讨论了aspnet_isapi.dll 如何将对不同后缀名文件的请求分发给相应的处理程序,如何查看Framework默认的处理程序Handler。

 

然后,我们通过三个实例,图片防盗链、图片验证码、处理自定义后缀名请求,详细讲解了IHttpHandler的实现方法和使用过程。

 

最后,我向大家概要地介绍了IHttpHandlerFactory接口。 

 

第三回:

 

引言

 

Http 请求处理流程 和 Http Handler 介绍 这两篇文章里,我们首先了解了Http请求在服务器端的处理流程,随后我们知道Http请求最终会由实现了IHttpHandler接口的类进行处理(应该记得Page类实现了IHttpHandler)。从 Http 请求处理流程 一文的最后的一幅图中可以看到,在Http请求由IHttpHandler处理之前,它需要通过一系列的Http Module;在请求处理之后,它需要再次通过一系列的Http Module,那么这些Http Module是如何组成的?用来做什么呢?本文将对Http Module作以介绍。

 

Http Module概述

 

暂时先不考虑我们自己实现Http Module的情况。在.Net中,Http Module 是实现了IHttpModule接口的程序集。IHttpModule 接口本身并没有什么好大写特写的,由它的名字可以看出,它不过是一个普普通通的接口而已。实际上,我们关心的是实现了这些接口的类,如果我们也编写代码实现了这个接口,那么有什么用途。一般来说,我们可以将Asp.Net中的事件分成三个级别,最顶层是 应用程序级事件、其次是页面级事件、最下面是控件级事件,事件的触发分别与 应用程序周期、页面周期、控件周期紧密相关。而 Http Module 的作用是与应用程序事件 密切相关的。

 

我们通过Http Module在Http请求管道(Pipeline)中注册期望对应用程序事件做出反应的方法,在相应的事件触发的时候(比如说BeginRequest事件,它在应用程序收到一个Http请求并即将对其进行处理时触发),便会调用Http Module注册了的方法,实际的工作在这些方法中执行。.Net 本身已经有很多的Http Module,其中包括 表单验证Module(FormsAuthenticationModule), Session 状态Module(SessionStateModule),输出缓存Module (OutputCacheModule)等。

 

注册 Http Module

 

在注册我们自己编写的 Http Module 之前,先来看看Asp.Net中已经有的HttpModule。与 Http Handler类似,我们需要打开机器上C:\WINDOWS\Microsoft.NET\Framework\ v2.0.50727\CONFIG 目录下的 web.config 文件。找到 <httpModules/> 结点,应该可以看到下面的内容:

 

<httpModules>

    <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />

    <add name="Session" type="System.Web.SessionState.SessionStateModule" />

    <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />

    <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />

    <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />

    <add name="RoleManager" type="System.Web.Security.RoleManagerModule" />

    <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />

... 略

</httpModules>

 

我们先从结点上看,type属性与上一节所说的http handler结点的type属性类似,都代表了相应的程序集。但是,与http handler 不同,module只提供了一个name属性,没有诸如 path这样指定某一特定(或者用通配符 * 代表某一种类)文件的处理程序。这是与Module的特点相关的,我们知道 module 是响应应用程序周期中触发的事件,对于所有提交到aspnet_isapi.dll的请求都一样,即便请求只是像类似http://www.tracefact.net/images/logo.gif 这样获取一张图片而已(对ISAPI进行过设置以后,默认aspnet_isapi.dll不接手图片文件)。

 

与Http handler类似,在这册我们自己的http module 时,假设类名为ModuleDemo,位于myNameSpace命名空间下,程序集名称为myDll,我们只需将myDll.dll拷贝到Bin目录下,并在站点的 web.config 文件 system.web 结点下创建 httpModules 结点:

 

<system.web>

    <httpModules>

       <add name="CustomModuleName" type="myNameSpace.ModuleDemo, myDll"/>

    </httpModules>

</system.web>

 

type属性由分号“,”分为两部分,前面是命名空间及类名,也就是类型名;后面是程序集名。如果我们将代码创建在App_Code目录中,则不需要再指定程序集名。

 

name属性由我们自己命名,不一定与类名相同,此处我将它命名为“CustomModuleName”。我们可以通过应用程序(HttpApplication)的Modules属性获取HttpModuleCollection集合,然后通过name属性,进一步获取HttpModule对象。

 

通过name属性,我们还可以在global.asax中文件中编写自定义HttpModule暴露出的事件的处理程序,它采用的格式是:void ModuleName_EventName(object sender, EventArgs e)。我们将在后面做更详细介绍。

 

Asp.Net 内置的 Http Modules

 

下面这张表格列出了C:\WINDOWS\Microsoft.NET\Framework\ v2.0.50727\CONFIG下的Web.Config中的 Asp.Net 内置的Http Modules 及其主要作用。

 

名称

类型

功能

OutputCache

System.Web.Caching.OutputCacheModule

页面级输出缓存

Session

System.Web.SessionState.SessionStateModule

Session状态管理

WindowsAuthentication

System.Web.Security.WindowsAuthenticationModule

用集成Windows身份验证进行客户端验证

FormsAuthentication

System.Web.Security.FormsAuthenticationModule

用基于Cookie的窗体身份验证进行客户端身份验证

PassportAuthentication

System.Web.Security.PassportAuthenticationModule

MS护照进行客户身份验证

RoleManager

System.Web.Security.RoleManagerModule

管理当前用户角色

UrlAuthorization

System.Web.Security.UrlAuthorizationModule

判断用户是否被授权访问某一URL

FileAuthorization

System.Web.Security.FileAuthorizationModule

判断用户是否被授权访问某一资源

AnonymousIdentification

System.Web.Security.AnonymousIdentificationModule

管理Asp.Net应用程序中的匿名访问

Profile

System.Web.Profile.ProfileModule

管理用户档案文件的创立 及相关事件

ErrorHandlerModule

System.Web.Mobile.ErrorHandlerModule

捕捉异常,格式化错误提示字符,传递给客户端程序

 

IHttpModule接口

 

看了这么多理论知识,本节将开始动手写点程序,实现自己的Http Module。我们首先需要看下IHttpModule 接口,它包括下面两个方法:

 

public void Init(HttpApplication context);

public void Dispose();

 

Init():这个方法接受一个HttpApplication对象,HttpApplication代表了当前的应用程序,我们需要在这个方法内注册 HttpApplication对象暴露给客户端的事件。可见,这个方法仅仅是用来对事件进行注册,而实际的事件处理程序,需要我们另外写方法。

 

整个过程很好理解:

 

① 当站点第一个资源被访问的时候,Asp.Net会创建HttpApplication类的实例,它代表着站点应用程序,同时会创建所有在Web.Config中注册过的Module实例。

② 在创建Module实例的时候会调用Module的Init()方法。

③ 在Init()方法内,对想要作出响应的HttpApplication暴露出的事件进行注册。(仅仅进行方法的简单注册,实际的方法需要另写)。

④ HttpApplication在其应用程序周期中触发各类事件。

⑤ 触发事件的时候调用Module在其Init()方法中注册过的方法。

NOTE:如果你不了解事件注册等相关内容,请参阅 C#中的委托与事件 一文。

Dispose():它可以在进行垃圾回收之前进行一些清理工作。

 

综上所述:实现一个 IHttpModule 的模板一般是这样的:

 

public class ModuleDemo:IHttpModule

{

    public void Init(HttpApplication context) {

       // 注册HttpApplication应用程序 BeginRequest 事件

       // 也可以是其他任何HttpApplication暴露出的事件

       context.BeginRequest += new EventHandler(context_BeginRequest);

    }

 

    void context_BeginRequest(object sender, EventArgs e) {

       HttpApplication application = (HttpApplication)sender;

       HttpContext context = application.Context;

       // 做些实际的工作,HttpContext对象都获得了,剩下的基本可以自由发挥了

    }

 

    public void Dispose() {

    }

}

 

通过Http Module向Http请求输出流中写入文字

 

本例中,我们仅用BeginRequest事件和 EndRequest 事件对 Http Module 的使用作以说明。我们通过这个范例,了解 Http Module 基本的使用方法。

 

首先,请创建一个新的站点,在App_Code目录中添加类文件: ModuleDemo.cs:

 

public class ModuleDemo:IHttpModule

{

    // Init方法仅用于给期望的事件注册方法

    public void Init(HttpApplication context) {

       context.BeginRequest += new EventHandler(context_BeginRequest);

       context.EndRequest += new EventHandler(context_EndRequest);

    }

 

    // 处理BeginRequest 事件的实际代码

    void context_BeginRequest(object sender, EventArgs e) {

       HttpApplication application = (HttpApplication)sender;

       HttpContext context = application.Context;

       context.Response.Write("<h1 style='color:#00f'>来自HttpModule 的处理,请求到达</h1><hr>");

    }

 

    // 处理EndRequest 事件的实际代码

    void context_EndRequest(object sender, EventArgs e) {

       HttpApplication application = (HttpApplication)sender;

       HttpContext context = application.Context;

       context.Response.Write("<hr><h1 style='color:#f00'>来自HttpModule的处理,请求结束</h1>");

    }

       

    public void Dispose() {

    }

}

 

上面的代码很简单,它注册了 HttpApplication实例的 BeginRequest 事件 和 EndRequest事件,事件处理方法的作用仅仅是在http请求开始和结束的时候,给http请求的输入流中分别写入不同的内容。

 

接下来在 Web.config 的 System.web 结点中写入以下内容:

 

<system.web>

    <httpModules>

       <add name="MyModule" type="ModuleDemo" />

    </httpModules>

</system.web>

 

然后,打开建立站点时自动创建的 Default.aspx文件,在里面打几个字,为了做区分,我输入的是:位于.aspx页面上的文字。然后,我们在浏览器中打开它,应该会看到像这样:

 

 

 

然后我们再新建一个 Default2.aspx,在浏览器中浏览,可以看到,两个页面的效果相同。这说明对于不同的两个文件,http Module都起了作用,可见它确实是位于应用程序级,而非页面级。

 

现在,我们再打开站点中的一张图片文件,发现显示出的是一个红叉叉,为什呢?因为Http Module 针对是http 请求,而不是某个或某一类文件,所以当请求一张图片的时候,我们编写的http Module依然会起作用,将文字插入到二进制图片中,破坏了文件格式,自然只能显示红叉叉了。

 

NOTE:如果你发现你的图片显示正常,请不要惊讶,事情是这样的:回想一下第一节我们讨论到的,对于图片文件,由IIS直接处理,并不会交由aspnet_isapi.dll,所以,Module无法捕获对于图片类型文件的请求。解决方法就是在IIS中进行设置一下。

    这里需要提请注意的是:如果你使用Vs2005自带的Local Server,那么你无需对IIS进行设置,所有的不论图片还是任何文件类型,都会交由aspnet_isapi.dll处理。

 

遍历Http Module集合

 

现在,我们通过遍历 HttpModuleCollection 集合来查看注册给应用程序的所有 Http Module 的名称。

 

新建一个文件 RegisteredModules.aspx,在代码后置文件中添加如下方法:

 

private string ShowModules() {

    HttpApplication app = Context.ApplicationInstance; //获取当前上下文的HttpApplication环境

    HttpModuleCollection moduleCollection = app.Modules; //获取所有Module集合

 

    // 获取所有的 Module 名称

    string[] moduleNames = moduleCollection.AllKeys;

 

    System.Text.StringBuilder results = new System.Text.StringBuilder();    //遍历结果集

 

    foreach (string name in moduleNames) {

       // 获得Module名称

       results.Append("<b style='color:#800800'>名称:" + name + "</b><br />");

        // 获得Module类型

       results.Append("类型:" + moduleCollection[name].ToString() + "<br />"); 

    }

 

    return results.ToString();

}

 

然后在Page_Load方法中输出一下:

 

protected void Page_Load(object sender, EventArgs e)

{

    Response.Write(ShowModules());

}

 

我们应该可以看到下面这样的画面:

 

 

 

与之前列出的那张表格比较一下,可以看出是几乎完全一致的(多了一个DefaultAuthentication)。另外注意上图的倒数第四行,那不是我们自己定义的Module么?name为MyModule,类型为ModuleDemo。

 

Global.asax文件与 Http Module

 

早在asp时代,大家就知道这个文件了。它主要用于放置对于 应用程序事件或者 Session事件的响应程序。大家熟悉的有Application_Start、Application_End、Session_Start、Session_End 等。

 

在asp.net中,Glabal不仅可以注册应用程序和Session事件,还可以注册Http Module暴露出的事件;不仅可以注册系统Module的事件,也可以注册我们自己义的Module暴露出的事件。在具体介绍之前,这里需要首先注意两点:

 

在每处理一个Http请求时,应用程序事件都会触发一遍,但是Application_Start和 Application_End 例外,它仅在第一个资源文件被访问时被触发。

Http Module无法注册和响应Session事件,对于Session_Start 和 Session_End,只能通过Glabal.asax来处理。

好了,我们现在修改之前 ModuleDemo 范例程序,给它像下面这样给它添加一个事件(为了使程序简洁一些,我做了简化):

 

public class ModuleDemo : IHttpModule {

 

    // 声明一个事件

    public event EventHandler ExposedEvent;

 

    // Init方法仅用于给期望的事件注册方法

    public void Init(HttpApplication context) {

       context.BeginRequest += new EventHandler(context_BeginRequest);

    }

 

    // 处理BeginRequest 事件的实际代码

    void context_BeginRequest(object sender, EventArgs e) {

       HttpApplication application = (HttpApplication)sender;

       HttpContext context = application.Context;

       context.Response.Write("<h3 style='color:#00f'>来自HttpModule的处理,请求到达</h3><hr>");

       

       OnExposedEvent(new EventArgs()); // 调用方法

    }

 

    protected override void OnExposedEvent(EventArgs e) {

       if (ExposedEvent != null) // 如果Global中有注册

           ExposedEvent(this, e);   // 调用注册了的方法

    }

    

    public void Dispose() {

    }

}

 

接下来,我们在站点中创建一个 Global.asax 文件,在里面添加如下代码,注意到格式是:void 模块名_事件名(object sender, EventArgs e)。

 

void MyModule_ExposedEvent(object sender, EventArgs e)

{

     Response.Write("<h3 style='color:#800800'>来自 Global.asax 的文字</h2>");

}

 

现在,我们打开之前的页面,应该可以见到这样,可见,我们成功的将 Glabal.asax文件与我们自己定义的Http Module所暴露出的事件 ExposedEvent 联系到了一起:

 

 

 

总结

 

本文简单地介绍了什么是Http Module。我们首先了解了Http Module的作用,然后查看了Asp.Net 内置的Module,接着我们介绍了IHttpModule接口,并通过了一个简单的范例实现了此接口,最后我们讨论了 Http Module与 Global.asax 文件的联系。

 

本文仅仅是对IHttpModule作以简单介绍,对其更多的实际应用,会在后续文章中补充。

 

希望这篇文章能给你带来帮助!

posted @ 2016-01-26 15:04  秋水惜朝  阅读(287)  评论(0编辑  收藏  举报