Apache源代码全景分析(第1卷):体系结构与核心模块
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 Apache功能

作为一个标准的HTTP服务器,Apache同时支持HTTP0.9、HTTP/1.0及HTTP/1.1协议,实现了协议中规定的绝大多数内容。同时还实现了协议中并没有规定的内容,比如虚拟主机。

本节我们概要地介绍Apache中提供的各种基本功能。

1.3.1 虚拟主机

虚拟主机(Virtual Host)是指在一个机器上运行多个 Web 站点的机制 (比如:www.company1.comwww.company2.com)。虚拟主机的实现包括以下三种方式。

(1)Web服务器中配备多个IP地址,并且每一个逻辑Web服务器使用一个IP地址。这种虚拟主机的实现技术被称为“基于IP”,这是最简单的虚拟主机的实现机制,但是这种机制存在一些问题,比如扩展性的问题。一台机器所能存在的物理 IP 地址总是有限的,因此对于一个专门的ISP而言,如果要提供大量的虚拟主机,则会存在相当大的困难。另外一个存在的问题就是IP地址的有限性,目前Web站点的数目远远超过IP地址的数目,因此,以IP地址区分虚拟主机,则会使Web站点的发展受到限制。

(2)Web服务器只有一个IP地址,不同的Web服务器使用不同的端口进行侦听。因此这种服务器的请求 URI 中必须明确地给出端口,而不能使用默认的 Web 端口 80,比如http://127.0.0.1:8900。这种虚拟主机的实现技术可称为“基于端口”。这种策略存在的问题是用户必须显式给出请求的端口,这对大部分用户来说显然是不太方便的。如果忘记输入端口号或输入一个错误的端口号,则会使用错误的虚拟主机。

(3)Web服务器只有一个IP地址,同时多个域名被映射到该IP地址上。所有的Web服务器侦听同一个端口。服务器通过 HTTP 请求头中的 HOST 域对请求进行区分。对于HTTP 1.1协议而言,该域是必须具备的,而低于HTTP 1.1的协议则未必如此。因此从这个意义上说,只有HTTP 1.1协议才可以支持这种基于“HOST域”的协议。

Apache中支持上面三个方式的虚拟主机,而且通过mod_vhost_alias模块,可以使得类似的虚拟主机配置起来非常容易,减轻了管理员的负担。

1.3.2 内容协商

Apache 中对于同一个文档可能会保存多个不同的版本,比如英语版、法语版、日语版及中文版,等等。不同的用户可能需要不同的版本,比如中国的用户可能只关心中文版,如果没有中文版,也可以考虑英文版;而日本的用户可能更希望安装日文版。语言的不同是一方面,另外还可能文档的格式也不一样。最简单的例子就是Apache安装后显示的标准页面。

那么,我们怎么选择最适合给定用户的版本呢?有两种办法可以进行这种客户端和服务器端资源的协商:服务器端驱动的内容协商和客户端驱动的内容协商。这两种协商是相互正交的,因此它们可以单独或混合使用。混合使用中有一种被称为“透明的协商方法”,可在缓存使用由初始服务器提供的代理驱动协商信息,为后续的请求提供服务器驱动协商发生时,使用该方法。

服务器端驱动的内容协商

对于服务器端驱动的内容协商,毫无疑问,需要发送到客户端的文档版本由服务器端决定。通过使用“Accept”请求域字段,客户端会提供一系列的它能够接受处理的格式列表,根据这个请求域,服务器会选择最适合客户端的内容。服务器的选择可能基于语言、内容编码、请求消息中特殊报文字段的内容及隶属于请求的其他信息,比如网络IP地址等。

服务器端驱动的协商在下面的情况比较有利:从可用表示中选择的算法很难用于描述用户代理,或者服务器向和第一个应答一起把它的“最佳猜测”发送给客户端。如果最佳猜测对于用户来说已经足够好,就可以避免一个后续请求交互时间的延迟。为了改善服务器的猜测,客户端可以包含Accept、Accept-Language、Accept-Encoding。

服务器端驱动的协商具有以下几个缺点。

(1)服务器不可能准确判断出对于一个给定的用户什么是“最佳”的协商,因为这需要全面了解客户端的能力和对应答的使用。比如用户是希望在显示器上看到,还是希望将其打印到纸上阅读。

(2)客户端在一个请求中描述自己能力的效率很低,另一方面,如果描述能力很强,又会对用户的隐私造成泄漏。

(3)服务器端驱动的协商使得初始服务器的实现及对请求生成应答的算法变得复杂。

(4)服务器端驱动的协商可能会限制一个公共缓存的能力,使其对多个用户的请求使用同样的应答。

Apache支持HTTP/1.1规范中定义的“服务器驱动”的内容协商,可以完全支持Accept、Accept-Language、Accept-Charset、Accept-Encoding 请求头,这些是 RFC2295 和 RFC2296中定义的实验协商协议,但是不支持这些RFC中定义的“功能协商”。

客户端驱动的内容协商

客户端驱动的协商由浏览器端完成,它不属于HTTP服务器的一部分,我们不做过多的讨论。

1.3.3 持续连接

在HTTP/1.0版本中,对于每一个单一的HTTP请求,客户端和服务器端之间都必须建立一次TCP连接。在HTTP刚开发出来的时候,HTML文档通常只包含HTML文件,在这种情况下,对一个请求建立一次连接是很合适的。但是随着Web技术的发展,一个Web页面中会包含非常多的多媒体数据,比如图片、音频、视频、FLASH 等。据统计,对于一个稍微大型一点的 Web 站点的主索引页面,如果要将它的整个文档下载到客户端,至少需要150次不同的文件请求。因此,如果打开每一个文件请求,然后关闭,那么也就是说我们至少需要打开和关闭TCP连接150次,这显然不是一个明智的选择。一方面,打开和关闭的不断重复会造成文档下载时间过长;另一方面,也会给服务器造成非常大的负载压力。

基于对上面问题的解决,持续连接的概念被引入,尽管这个概念并不是标准的HTTP/1.1协议中所含有的。所谓持续连接,就是某个连接在打开后不立即关闭,而是继续使用,后续的数据传输都基于该连接。因此,对于某一个 Web 页面来说,不管其中包含了视频还是图片等,都是基于该连接被传送的。为了使用持续连接,客户端和服务器端之间会使用“Connection:keep-alive”请求域;默认情况下连接就是持续连接,除非进行了特殊的指定。如果客户端和服务器端的某一方不愿意使用持续连接,它只需要设置“Connection:close”请求域,另一方一旦接收到“Connection:close”,就会在当前请求处理完毕后关闭当前请求。Apache中提供了配置指令,允许限制同一连接上的处理请求的数目,以及处理超时的时间,一旦超过该处理时间,所有的连接都将被关闭。

1.3.4 缓存

HTTP通常位于分布式信息系统中,在这些系统中,可以通过采用缓存应答的方式改善系统的性能。HTTP/1.1 协议中包含了大量的元素,尽可能地使缓存产生更好的效果。通过缓存,可以加快对客户端的响应速度。

HTTP/1.1 协议中缓存的设计目标就是在很多情况下降低发送请求的必要性,以及在很多其他情况下降低发送完整应答的必要性。前者就使你少了很多操作所需要的网络回合的数量——它使用过期机制来实现这一目的。后者减少了网络带宽需求——通过验证机制达到这一目的。

HTTP/1.1协议中提供了一些非常有用的缓存字段。

Expires

Expires字段声明了一个网页或URL地址不再被浏览器缓存的时间,一旦超过了这个时间,浏览器都应该联系原始服务器。RFC告诉我们:“由于推断的失效时间也许会降低语义透明度,应该被谨慎使用,同时我们鼓励原始服务器尽可能提供确切的失效时间”。

对于一般的纯静态页面,如HTML、gif、jpg、css、js,默认安装的Apache服务器,不会在响应头添加这个字段。Firefox浏览器接收到相应命令后,如果发现没有Expires字段,浏览器根据文件的类型和“Last-Modified”字段来推断出一个合适的失效时间,并存储在客户端。推测出的时间一般是接收到响应后的三天左右。

Cache-Control

Cache-Control字段中可以声明多种元素,例如no-cache、must-revalidate、max-age=0等。这些元素用来指明页面被缓存的最大时限,如何被缓存的,如何被转换到另一个不同的媒介,以及如何被存放在持久媒介中的。但是,任何一个 Cache-Control指令都不能保证隐私性或数据的安全性。“private”和“no-store”指令可以为隐私性和安全性方面提供一些帮助,但是它们并不能用于替代身份验证和加密。

Apache 的 mod_cern_meta 模块允许文件级 http 响应头的控制,同时它也可以配置Cache-Control头(或任何其他头)。响应头文件是放在原始目录的子目录中,根据原始文件名所命名的一个文件。具体用法请参阅Apache的官方网站。其中Cache-Control:max-age表示失效日期。如果没有启动mod_cern_meta模块,Apache服务器会把Expires字段中的日期换算成以秒为单位的一个 delta值,赋值给max-age。如果启动mod_cern_meta模块,并且配置了max-age值,Apache会用它覆盖Expires字段。同时,max-age隐含了Cache-Control:public。这样,浏览器接收到的Cache-Control:max-age和Expires值就是一致的。

如果失效日期 Cache-Control:max-ag 等于零或为负值,浏览器会在对应的缓存中把Expires设置为1970-01-01 08:00:00。

Last-Modified

Last-Modified和ETag是条件请求(Conditional Request)相关的两个字段。如果一个缓存收到了针对一个页面的请求,它发送一个验证请求询问服务器页面是否已经更改,在HTTP头里面带上“ETag”和“If Modify Since”头。服务器根据这些信息判断是否更新了信息,如果没有更新,就返回HTTP 304(NotModify);如果更新了,就返回HTTP 200和更新的页面内容,并且携带新的“ETag”和“LastModified”。

使用这个机制,能够避免重复发送文件给浏览器,不过仍然会产生一个HTTP请求。

一般纯静态页面本身都会有 Last-Modified 信息,Apache 服务器会读取页面文件中的Last-Modified信息,并添加到HTTP响应头部。

对于动态页面,如果在页面内部没有通过函数强制加上 Last-Modified,例如 header ("Last-Modified: " . gmdate ( "D, d M Y H:i:s") . " GMT"),Apache服务器会把当前时间作为Last-Modified返回给浏览器。

无论是纯静态页面还是动态页面,Firefox 浏览器巧妙地按照接收到服务器响应的时间设置缓存页面的Last-Modified,而不是按照http响应头中的Last-Modified字段。

ETag

既然有了Last-Modified,为什么还要用ETag字段呢?因为如果在一秒钟之内对一个文件进行两次更改,Last-Modified就会不正确。因此,HTTP/1.1利用Entity Tag头提供了更加严格的验证。

Apache服务器在默认情况下,会对所有的静态、动态文件的响应头添加ETag字段。在Apache的httpd.conf文件中可以通过FileETag指令配置该选项。

FileETag指令配置了当文档基于一个文件时用以创建 ETag(entity tag)响应头的文件的属性。在Apache 1.3.22及以前,ETag的值是对文件的索引节(INode)、大小(Size)和最后修改时间(MTime)进行Hash后得到的。如果一个目录的配置包含了“FileETag INode MTime Size”,而其中一个子目录包含了“FileETag –INode”,那么这个子目录的设置(并会被其下任何没有进行覆盖的子目录继承)将等价于“FileETag MTime Size”。

在多台负载平衡的服务器环境下,同一个文件会有不同的ETag或文件修改日期,浏览器每次都会重新下载。设置“FileETag None”可以使响应头不再包含ETag字段。

1.3.5 访问控制和安全

对于Web站点上存在的一些私有文件,我们必须确保这些文件在绝对的受控范围之内,比如 passwd。通过认证、授权和访问控制等一系列的安全措施,可以确保受控资料的安全性。

认证(Authentication)、授权(Authorization)及账户确认(Accounting)三者合起来称为AAA模块。

访问控制

AAA 安全措施的第一个措施就是访问控制。访问控制意味着服务器会基于用户不能控制的请求特性进行限制访问,比如客户端计算机的IP地址。Apache中只有一个模块实现了访问控制。如果用户在访问控制的范围之外,那么它将无法访问服务器。

认证

认证意味着用户要具有它所声称的身份,与大多数认证一样,HTTP认证也是通过提供用户名称和共享密码或口令实现的,然后服务器再根据所发送的口令与它所知道的口令进行对比。如果两者匹配,就可以认为你具有它所生成的身份。如果两者不匹配,Web服务器就会向浏览器发送一条消息,要求重新提供正确的用户名称和口令。服务器会在用户数据库中查找用户ID和密码组合是否存在。如果找到,则意味着该用户是认证通过的。这些数据库可能是一个简单的文本文件、一个类似于MySQL或Oracle的关系数据库,或者是操作系统本身所提供的授权机制,也有可能是类似于LDAP2、NIS3或NTLM4的用户管理服务。

认证包括两种:基本认证和摘要认证。

授权

尽管认证(authentication)和授权(authorization)两者的关系非常接近,但是二者之间还是存在着很大的区别。授权通常发生在认证之后。一旦用户认证通过,则只能说明它具有合法的身份,但并不意味着它就具有对特定资源访问的权限。对于服务器中的一些特殊的资源,管理员通常会严格限制访问。Apache 通过解析全局及本地的配置文件.htaccess 来决定用户的授权身份。如果用户正在请求自己没有访问权限的页面,就可能只通过认证阶段而通不过授权阶段。从用户角度来看,不能获得授权类似于不能获得认证:浏览器都会要求他们再次输入用户名和密码。

对资源的访问可能限制在特定的域,浏览器所在机器的网络、地址或特定的用户及用户组。Apache 通过解析全局的及局部配置文件来决定用户对特定资源的访问权限。如果管理员允许,Web内容提供者还可以通过本地配置文件.htaccess限制对他们的文档的访问。

HTTP服务器中认证和授权的流程如图1-12所示。

图1-12 HTTP服务器中认证授权流程

1.3.6 动态内容生成

一个Web服务器最简单的功能就是将存放于服务器上的静态的HTML文件发送给客户端。如果Web提供者须修改返回的HTML,那么它必须手工更改这些文件。如果页面的数目很少,那么这种工作量容易被忽略,但是对于一个大的 Web 站点而言,如果有成千上万的页面,那么显然不合适。随着Internet的发展,以及多媒体页面的增长,生成动态网页的需求也随之增长。基于 Web 的应用从最简单的个人地址本、留言本到复杂的在线银行、在线交易等,这些应用都必须要求Web服务器提供动态内容生成的功能。

Apache中提供了服务器端脚本的功能用以动态地生成返回给客户端的HTML页面。允许服务器生成动态内容的一个简单方法就是使用服务器端脚本。允许服务器产生动态内容的第一个技术就是使用CGI。为了使用CGI脚本,Web服务器须执行一个外部应用程序,该程序可以解释脚本代码并且返回执行输出后的HTML至Web服务器,而Web服务器接收到HTML会继续将其转发给客户端。

另外,服务器还可以使用额外的模块来支持脚本语言,比如mod_perl。当请求被处理的时候,脚本语言将在服务器的上下文环境中解释它。脚本模块为脚本语言提供了执行环境,另一方面,脚本模块中还必须提供API函数以便允许脚本访问脚本之外的数据,只有这样它才能接受客户端的数据。

在Apache中,每一个脚本语言模块通常都会注册一个单独的MIME类型,同时定义一个文件扩展,然后当该扩展的文件被请求的时候,对应的内容处理器将被触发调用。一般来说,服务器端的脚本分为两类:嵌入在HTML中的脚本和完全独立生成HTML的脚本。

支持嵌入脚本的HTML文件

对于这种情况,脚本是被嵌入在Web服务器上的HTML文档中的,这种情况大家最熟悉的就是 asp、php 等文件。不管哪一种脚本,它们通常都会被包含在一个特定的标签中,这些标签将被脚本引擎模块所识别。脚本被执行后,这些脚本将被输出为 HTML 文本,并将原有的脚本替换。脚本生成的数据可能基于外部数据源,比如数据库,也有可能是从客户端接收的数据。

服务器端包含功能(Server-Side Includes,SSI)是Apache本身所支持的嵌入脚本。SSI中允许一些最基本的指令,比如对变量赋值、访问系统变量、处理基本的运算甚至执行一些系统命令。这些命令的数据将成为Web页面的一部分。

SSI最通常的作用就是将其余的文件包含到当前HTML文档中。因此通常SSI会用来对CGI页面进行装饰,比如增加页面头和页面尾注,这样可以减少编写CGI页面的工作量。

通过在注释标签中使用特殊的指令,SSI 指令可以被直接包含在 HTML 页面中。浏览器总是会忽略这些注释,而服务器会对这些注释进行处理,然后对其进行内容包含,通常类似于:

    <!--#set var="name" value="Rich" -->

完全由程序生成的HTML文档

对于一些复杂的Web应用程序而言,直接在HTML中嵌入脚本并不是一个最终的完美的解决方案。这些应用程序会完全生成所需要的HTML文档。

如果脚本中不包含静态HTML代码,那么无疑会减轻服务器的压力,因为服务器不需要解析整个脚本文档,并将其中的脚本指令提取出来。另外,纯粹的脚本语言执行要比 HTML中嵌入脚本速度快得多。一些脚本语言会进行编译,所以速度也会快于那些解释性的脚本语言。

纯脚本生成HTML的例子最主要的就是CGI程序和Java Servlets两种。CGI程序通常是C、C++或Perl等程序,Java Servlets则是使用Java程序编写的。这两种都是编译型脚本语言。