媒体类型
因特网上有数千种不同的数据类型,HTTP仔细地给各种要通过Web传输的对象都打上了名为MIME类型(MIME type) 的数据格式标签。
Web服务器会为所有HTTP对象数据附加一个MIME类型。当Web浏览器从服务器取回一个对象时,会去查看相关的MIME类型,看看它是否知道应该如何处理这个对象。大多数浏览器都可以处理数百种常见的对象类型:显示图片文件,解析并格式化HTML文件,通过计算机声卡播放音频文件,或者运行外部播放软件来处理特殊格式的数据。
事务
一个HTTP事务由一条(由客户端发往服务器的)请求命令和一个(从服务器发回客户端的)响应结果组成。这种通信是通过名为HTTP报文(HTTP message)的格式化数据块进行的,如下图所示。
报文
HTTP报文是由一行一行的简单字符串组成的。HTTP报文都是纯文本,不是二进制代码,所以可以很方便地对其进行读写。下图显示了一个简单事务所使用的HTTP报文。
HTTP报文包括以下三个部分
- 起始行:报文的第一行就是起始行,在请求报文中用来说明要做些什么,在响应报文中说明出现了什么情况
- 首部字段:起始行后面有零个或多个首部字段,每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)来分隔。首部以一个空行结束,添加一个首部字段和添加新行一样简单。
- 主体:空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了要发送给Web服务器的数据,响应主体中装载了要返回给客户端的数据。起始行和首部都是文本格式且都是格式化的,而主体则不同,主体中可以包含任意的二进制数据(比如图片,视频,音频,软件程序)。当然,主体中也可以包含文本。
连接
下面我们来讨论下报文是如何通过传输控制协议(Transmission Control Protocol TCP)连接从一个地方搬移到另一个地方去的。
HTTP是个应用层协议。HTTP无需操作网络通信的具体细节,它把联网的细节都交给了通用的,可靠的因特网传输协议TCP/IP。
只要建立了TCP连接,客户端和服务器之间的报文交换就不会丢失,不会被破坏,也不会在建立时出现错序了。
具体过程如下:
在HTTP客户端向服务器发送报文之前,需要用网际协议(Internet Protocol)地址和端口号在客户端和服务器之间建立一条TCP/IP连接。
建立一条TCP连接的过程与给公司办公室的某个人打电话的过程类似,首先,要拨打公司的电话号码,这样就能进入正确的机构了,其次,拨打要联系的那个人的分机号。
在TCP中,你需要知道服务器的IP地址,以及与服务器运行的特定软件相关的TCP端口号。
有个IP地址和端口号,客户端就可以很方便地通过TCP/IP进行通信了。下图显示了浏览器是怎样通过HTTP事务显示位于远端服务器中的某个简单HTML资源的。
步骤如下
- a:浏览器从URL中解析出服务器的主机名
- b:浏览器将服务器的主机名转换成服务器的IP地址
- c:浏览器将端口号从URL中解析出来
- d:浏览器建立一条与Web服务器的TCP连接
- e:浏览器向服务器发送一条HTTP请求报文
- f:服务器向浏览器回送一条HTTP响应报文
- g:关闭连接,浏览器显示文档
字符限制
在URL中,有几个字符被保存起来,有着特殊的含义。有些字符不在定义的US-ASCII可打印字符集中,还有些字符会与某些因特网网关和协议产生混淆,因此不赞成使用。
下图列出了一些字符,在将其用于保留用途之外的场合时,要在URL中对齐进行编码
常用的HTTP方法
HEAD
HEAD方法与GET方法的行为很类似,但服务器在响应中只返回首部,不会返回实体的主体部分。这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查,使用HEAD ,可以:
- 在不获取资源的情况下了解资源的情况(比如,判断其类型)
- 通过查看响应中的状态码,查看某个对象是否存在
- 通过查看首部,测试资源是否被修改了
服务器开发者必须确保返回的首部与GET请求所返回的首部完全相同。遵循HTTP/1.1规范,就必须实现HEAD方法。下图显示了实际的HEAD方法。
首部
首部和方法配合工作,共同决定了客户端和服务器能做什么事情。在请求和响应报文中都可以用首部来提供信息,有些首部是某些报文专用的,有些首部则更通用一些,可以将首部分为五个主要的类型。
通用首部:
这些是客户端和服务器都可以使用的通用首部,可以在客户端,服务器和其他应用程序之间提供一些非常有用的通用功能。比如,Date首部就是一个通用首部,每一端都可以用它来说明构建报文的时间和日期。
请求首部:
从名字中就可以看出,请求首部是请求报文特有的,它们为服务器提供了一些额外信息,比如客户端希望接受什么类型的数据。例如,下面的Accept首部就用来告知服务器客户端会接受与其请求相符的任意媒体类型。
Accept:*/*
响应首部:
响应报文有自己的首部集,以便为客户端提供信息(比如,客户端在于哪种类型的服务器进行交互)。例如,下列Server首部就用来告知客户端它在于一个版本为1.0的Tiki-Hut服务器进行交互。
Server:Tiki-Hut/1.0。
实体首部:
实体首部指的是应用实体主体部分的首部。例如,可以用实体首部来说明主体部分的数据类型。例如,可以通过下面的Content-Type首部告知应用程序,数据是以iso-latin-1字符集表示的HTML文档。
Content-Type:text/html; charset=iso-latin-1
扩展首部:
扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的HTTP规范中。即使不知道这些扩展首部的含义,HTTP程序也要接受它们并对其进行转发
TCP连接
世界上几乎所有的HTTP通信都是由TCP/IP承载的,TCP/IP是全球计算机及网络设备都在使用的一种常用的分组交换网络协议集。客户端应用程序可以打开一条TCP/IP连接,连接到可能运行在世界任何地方的服务器应用程序。一旦连接建立起来了,在客户端和服务器端的计算机之间交换的报文就永远不会丢失,受损或失序。
TCP流是分段的,由IP分组传送
TCP的数据是通过名为IP分组(或IP数据报)的小数据块来发送的。这样的话,如下图所示,HTTP就是“HTTP over TCP over IP”这个协议栈的最顶层了。其安全版本HTTPS就是在HTTP和TCP之间插入了一个(TLS或SSL)的密码加密层。
HTTP要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的TCP连接按序传输。TCP收到数据流之后,会将数据流砍成被称作段的小数据块,并将段封装在IP分组中,通过因特网进行传输。所有这些工作都是由TCP/IP软件来处理的。HTTP程序员什么也看不到。
每个TCP段都是由IP分组承载,从一个IP地址发送到另一个IP地址的。每个IP分组中包括:
- 一个IP分组首部(通常为20字节)
- 一个TCP段首部(通常为20字节)
- 一个TCP数据块(0个或多个字节)
IP首部包含了源和目的IP地址,长度和其他一些标记。TCP的首部包含了TCP端口号,TCP控制标志,以及用来数据排序以及完整性检查的一些数字值。
TCP连接是通过4个值来识别的:
<源IP地址,源端口号,目的IP地址,目的端口号>
这4个值一起唯一地定义了一条连接,两条不同的TCP连接不可能拥有4个完全相同的地址组件值(但不同连接的部分组件可以拥有相同的值)。
在下图中,有4条连接:A,B,C,D,以及列出了每个端口的相关信息。
HTTP事务的时延
HTTP紧挨着TCP,位于其上层,所以HTTP事务的性能在很大程序上取决于底层TCP通道的性能。
下图描述了HTTP事务主要的连接,传输以及处理时延。
注意:与建立CTP连接,以及传输请求和响应报文的时间相比,事务处理时间可能是很短的。除非客户端或服务器超载,或正在处理复杂的动态资源,否则HTTP时延就是由TCP网络时延造成的。
HTTP事务的时延有以下几种重要原因。
- 客户端首先根据URI确定Web服务器的IP地址和端口号。如果最近没有对URI中的主机名进行访问,通过DNS解析系统将URI的主机名转换成一个IP地址可能要花费数十秒的时间
- 接下来,客户端会向服务器发送一条TCP连接,并等待服务器回送一个请求接受应答。每条新的TCP连接都会有连接建立时延。这个值通常最多只有一两秒钟,但如果有数百个HTTP事务的话,这个值会快速地叠加上去。
- 一旦连接建立起来了,客户端就会通过新建立的TCP管道来发送HTTP请求。数据到达时,Web服务器会从TCP连接中读取请求报文,并对请求进行处理。因特网传输请求报文,以及服务器处理请求报文都需要时间
- 然后,Web服务器会回送HTTP响应,这也需要花费时间。
这些TCP网络时延的大小取决于硬件速度,网络和服务器的负载,请求和响应报文的尺寸,以及客户端和服务器之间的距离。TCP协议的技术复杂性也会对时延产生巨大的影响。
下面列出了一些会对HTTP程序员产生影响的,最常见的TCPI相关时延,其中包括
- TCP连接建立握手
- TCP慢启动拥塞控制
- 数据聚集的Nagle算法
- 用于捎带确认的TCP延迟确认算法
- TIME_WAIT时延和端口耗尽
TCP/IP连接的握手时延
建立一条新的TCP连接时,甚至是在发送任意数据之前,TCP软件之间会交换一系列的IP分组,对连接的有关参数进行沟通。如果连接只用来传送少量数据,那么这些交换过程就会严重降低HTTP的性能。
在发送数据之前,TCP要传送两个分组来建立连接。
TCP连接握手需要经过以下几个步骤:
(1)请求新的TCP连接时,客户端要向服务器发送一个小的TCP分组(通常是40~60个字节),这个分组中设置了一个特殊的SYN标志,说明这是一个连接请求。
(2)如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,这个分组中的SYN和ACK标志都被置位,说明连接请求已被接受。
(3)最后客户端向服务器回送一条确认信息,通知它连接已成功建立,现代的TCP栈都允许客户端在这个确认分组中发送数据。
HTTP程序员永远看不到这些分组,这些分组都由TCP/IP软件管理,对其是不可见的,HTTP程序员看到的只是创建TCP连接时存在的时延。
TIME_WAIT累积和端口耗尽
当某个TCP断点关闭TCP连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的IP地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段试用期的两倍(称为2MSL,通常为2分钟)左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。实际上,这个算法可以防止在两分钟内创建,关闭并重新创建两个具有相同IP地址和端口号的连接。
在只有一个客户端和一台Web服务器的异常情况下,构建一条TCP连接的4个值:
<source-IP-address,source-port,destination-IP-address,destination-port>
其中3个都是固定的,只有源端口号可以随便改变。
<client-IP,source-port,server-IP,80>
客户端每次连接到服务器上去时,都会获得一个新的源端口,以实现连接的唯一性。但由于可用源端口的数量有限(比如,60000个),而且在2MSL秒(比如,120秒)内连接是无法重用的,连接率就限制在了60000/120=500次/秒,如果再不断进行优化,并且服务器的连接率不高于500次/秒,就可确保不会遇到TIME_WAIT端口耗尽问题。要修正这个问题,可以增加客户端负载生成机器的数量,或者确保客户端和服务器在循环使用几个虚拟IP地址以增加更多的连接组合。
即使没有遇到端口耗尽问题,也要特别小心有大量连接处于打开状态的情况,或为处于等待状态的连接分配了大量控制块的情况。在有大量打开连接或控制块的情况下,有些操作系统的速度会严重减缓。
串行连接
如果只对连接进行简单的管理,TCP的性能时延可能会叠加起来。比如,假设有一个包含了3个嵌入图片的Web页面。浏览器需要发起4个HTTP事务来显示此页面:1个用于顶层的HTML页面,3个用于嵌入的图片。如果每个事务都需要(串行地建立)一条新的连接,那么连接时延和慢启动时延就会叠加起来。
除了串行加载引入的实际时延之外,加载一幅图片时,页面上其他地方都没有动静也会让人感觉速度很慢,用户更希望能够同时加载多幅图片。
并行连接
如下图所示,HTTP允许客户端打开多条连接,并行地执行多个HTTP事务。在这个例子中,并行加载了四幅嵌入式图片,每个事务都有自己的TCP连接。
包含嵌入对象的组合页面如果能(通过并行连接)克服单条连接的空载时间和带宽限制,加载速度也会有所提高,时延可以重叠起来,而且如果单条连接没有充分利用客户端的因特网带宽,可以将未用带宽分配来装载其他对象。
下图显示了并行连接的时间线,相对来说比上面串行连接的要快得多,首先装载的是封闭的HTML页面,然后并行处理其余的3个事务,每个事务都有自己的连接,图片的装载是并行的,连接的时延也是重叠的。
即使并行连接的速度可能会更快,但并不一定总是更快。客户端的网络带宽不足时,大部分的时间可能都是用来传送数据的。在这种情况下,一个连接到速度较快服务器上的HTTP事务就会很容易地耗尽所有可用的带宽。如果并行加载多个对象,每个对象都会去竞争这有限的带宽,每个对象都会以较慢的速度按比例加载,这样带来的性能提升就很小,甚至没有什么提升。
而且,打开大量连接会消耗很多内存资源,从而引发自身的性能问题。复杂的Web页面可能会有数十或数百个内嵌对象,客户端可能打开数百个连接,但Web服务器通常要同时处理很多其他用户的请求,所以很少有Web服务器希望出现这种情况。100个用户同时发出申请,每个用户打开100个连接,服务器就要负责处理10000个连接,这会造成服务器性能的严重下降,对高负荷的代理来说也同样如此。
实际上,浏览器确实使用了并行连接,但它们会将并行连接的总数限制为一个较小的值(通常是4个),服务器可以随便关闭来自特定客户端的超量连接。
持久连接
Web客户端经常打开到同一个站点的连接。比如,一个Web页面上的大部分内嵌图片通常都来自同一个Web站点,而且相当一部分指向其他对象的超链通常都指向同一个站点,因此,初始化了对某服务器HTTP请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求(比如,获取在线图片)。这种性质被称为站点局限性。
在事务处理结束之后仍然保持在打开状态的TCP连接被称为持久连接。非持久连接会在每个事务结束之后关闭。持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。
重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据的传输。
持久连接有两种类型
- 比较老的HTTP/1.0+”keep-alive”连接
- 现代的HTTP/1.1 “persistent”连接
下图展示了keep-alive连接的一些性能优点,下图将在串行连接上实现4个HTTP事务的时间线与在一条持久连接上实现相同事务所需的时间线进行了比较,由于去除了进行连接和关闭连接的开销,所以时间线有所缩减。
HTTP1.0+Keep-Alive操作
keep-alive已经不再使用了,而且在当前的HTTP/1.1规范中也没有对它的说明了,但浏览器和服务器对keep-alive握手的使用仍然相当广泛,因此,HTTP的实现者应该做好与之进行交互操作的准备。
实现HTTP/1.0 keep-alive连接的客户端可以通过包含Connection:Keep-Alive首部请求将一条连接保持在打开状态。
如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部,如果响应中没有Connection:Keep-Alive首部,客户端就认为服务器不支持keep-alive,会在发回响应报文之后关闭连接。
HTTP/1.1持久连接
HTTP/1.1逐渐停止了对keep-alive连接的支持,用一种名为持久连接(persistent connection)的改进型设计取代了它。持久连接的目的与keep-alive连接的目的相同,但工作机制更优一些。
与HTTP/1.0+keep-alive的连接不同,HTTP/1.1持久连接在默认情况下是激活的,除非特别说明,否则HTTP/1.1假定所有连接都是持久的。要在事务处理结束之后将连接关闭,HTTP/1.1应用程序必须向报文中显式地添加一个Connection:Close首部。这是与以前的HTTP协议版本很重要的区别,在以前的版本中,keep-alive连接要么是可选的,要么根本就不支持。
HTTP/1.1客户端假定在收到响应后,除非响应中包含了Connection:close首部,不然HTTP/1.1连接就仍维持在打开状态,但是,客户端和服务器仍然可以随时关闭空闲的连接。不发送Connection:close并不意味着服务器承诺永远将连接保持在打开状态。
持久化连接的限制和规则
- 发送了Connection:close请求首部之后,客户端就无法在那条连接上发送更多的请求了
- 如果客户端不想在连接上发送其他请求了,就应该在最后一条请求中发送一个Connection:close请求首部
- 只有当连接上所有的报文都有正确的,自定义报文长度时——也就是说,实体主体部分的长度都和相应的Content-Length一致,或者是用分块传输编码方式编码的—-连接才能持久连接。
- HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接—–每个持久连接都只适用于一跳传输。
- (由于较老的代理会转发Connection首部,所以)HTTP/1.1的代理服务器不应该与HTTP/1.0客户端建立持久连接,除非它们了解客户端的处理能力。实际上,这一点是很难做到的,很多厂商都违背了这一原则。
- 尽管服务器不应该试图在传输报文的过程中关闭连接,而且在关闭连接之前至少应该响应一条请求,但不管Connection首部取了什么值,HTTP/1.1设备都可以在任意时刻关闭连接。
管道化连接
HTTP/1.1允许在持久连接上可选地使用请求管道,这是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列。当第一条请求通过网络流向地球另一端的服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。
下图显示了持久连接是怎样消除TCP连接时延,以及管道化请求是如何消除传输时延的。
对管道化连接有几条限制。
- 如果HTTP客户端无法确认连接是持久的,就不应该使用管道。
- 必须按照与请求相同的顺序回送HTTP响应。HTTP报文中没有序列号标签,因此如果收到的响应失序了,就没办法将其与请求匹配起来了
- HTTP客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成的管道化请求。如果客户端打开了一条持久连接,并立即发出了10条请求,服务器可能在只处理了,比方说,5条请求之后关闭连接。剩下的5条请求会失败,客户端必须能够应对这些过早关闭连接的情况,重新发出这些请求。
- HTTP客户端不应该用管道化的方式发送会产生副作用的请求(比如POST)。总之,出错的时候,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪一些。由于无法安全地重试POST这样的非幂等请求,所以出错时,就存在某些方法永远不会被执行的风险。
关闭连接
所有HTTP客户端、服务器或代理都可以在任意时刻关闭一条TCP传输连接。通常会在一条报文结束时关闭连接,但出错的时候,也可能在首部行的中间,或其他奇怪的地方关闭连接。
对管道化持久连接来说,这种情形是很常见的。HTTP应用程序可以在经过任意一段时间之后,关闭持久连接。比如,在持久连接空闲一段时间之后,服务器可能会决定将其关闭。
但是,服务器永远都无法确定在它关闭“空闲”连接的那一刻,在线路那一头的客户端有没有数据要发送。如果出现这种情况,客户端就会在写入半截请求报文时发现出现了连接错误。
每条HTTP响应都应该有精确的Content-Length首部,用以描述响应主体的尺寸。一些老的HTTP服务器会省略Content-Length首部,或者包含错误的长度指示,这样就要依赖服务器发出的连接关闭来说明数据的真实末尾。
客户端或代理收到一条随连接关闭而结束的HTTP响应,且实际传输的实体长度与Content-Length并不匹配(或没有Content-Length)时,接收端就应该质疑长度的正确性。
如果接收端是个缓存代理,接收端就不应该缓存这条响应(以降低今后将潜在的错误报文混合起来的可能)。代理应该将有问题的报文原封不动地转发出去,而不应该试图去“校正”Content-Length,以维护语义的透明性。
连接关闭容限、重试以及幂等性
即使在非错误情况下,连接也可以在任意时刻关闭。HTTP应用程序要做好正确处理非预期关闭的准备。如果在客户端执行事务的过程中,传输连接关闭了,那么,除非事务处理会带来一些副作用,否则客户端就应该重新打开连接,并重试一次。对管道化连接来说,这种情况更加严重一些。客户端可以将大量请求放入队列中排队,但源端服务器可以关闭连接,这样就会留下大量未处理的请求,需要重新调度。
副作用是很重要的问题。如果在发送出一些请求数据之后,收到返回结果之前,连接关闭了,客户端就无法百分之百地确定服务器端实际激活了多少事务。有些事务,比如GET一个静态的HTML页面,可以反复执行多次,也不会有什么变化。而其他一些事务,比如向一个在线书店POST一张订单,就不能重复执行,不然会有下多张订单的危险。
如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个事务就是幂等的。实现者们可以认为GET、HEAD、PUT、DELETE、TRACE和OPTIONS方法都共享这一特性。[插图]客户端不应该以管道化方式传送非幂等请求(比如POST)。否则,传输连接的过早终止就会造成一些不确定的后果。要发送一条非幂等请求,就需要等待来自前一条请求的响应状态。
正常关闭连接
如下图所示,TCP连接是双向的。TCP连接的每一端都有一个输入队列和一个输出队列,用于数据的读或写。放入一端输出队列中的数据最终会出现在另一端的输入队列中。
1.完全关闭与半关闭
应用程序可以关闭TCP输入和输出信道中的任意一个,或者将两者都关闭了。套接字调用close()会将TCP连接的输入和输出信道都关闭了。这被称作“完全关闭”,。还可以用套接字调用shutdown()单独关闭输入或输出信道。这被称为“半关闭”。
2.TCP关闭及重置错误
简单的HTTP应用程序可以只使用完全关闭。但当应用程序开始与很多其他类型的HTTP客户端、服务器和代理进行对话且开始使用管道化持久连接时,使用半关闭来防止对等实体收到非预期的写入错误就变得很重要了。
总之,关闭连接的输出信道总是很安全的。连接另一端的对等实体会在从其缓冲区中读出所有数据之后收到一条通知,说明流结束了,这样它就知道你将连接关闭了。
关闭连接的输入信道比较危险,除非你知道另一端不打算再发送其他数据了。如果另一端向你已关闭的输入信道发送数据,操作系统就会向另一端的机器回送一条TCP“连接被对端重置”的报文,如下图所示。大部分操作系统都会将这种情况作为很严重的错误来处理,删除对端还未读取的所有缓存数据。对管道化连接来说,这是非常糟糕的事情。
比如你已经在一条持久连接上发送了10条管道式请求了,响应也已经收到了,正在操作系统的缓冲区中存着呢(但应用程序还未将其读走)。现在,假设你发送了第11条请求,但服务器认为你使用这条连接的时间已经够长了,决定将其关闭。那么你的第11条请求就会被发送到一条已关闭的连接上去,并会向你回送一条重置信息。这个重置信息会清空你的输入缓冲区。
当你最终要去读取数据的时候,会得到一个连接被对端重置的错误,已缓存的未读响应数据都丢失了,尽管其中的大部分都已经成功抵达你的机器了。
3.正常关闭
HTTP规范建议,当客户端或服务器突然要关闭一条连接时,应该“正常地关闭传输连接”,但它并没有说明应该如何去做。
总之,实现正常关闭的应用程序首先应该关闭它们的输出信道,然后等待连接另一端的对等实体关闭它的输出信道。当两端都告诉对方它们不会再发送任何数据(比如关闭输出信道)之后,连接就会被完全关闭,而不会有重置的危险。
但不幸的是,无法确保对等实体会实现半关闭,或对其进行检查。因此,想要正常关闭连接的应用程序应该先半关闭其输出信道,然后周期性地检查其输入信道的状态(查找数据,或流的末尾)。如果在一定的时间区间内对端没有关闭输入信道,应用程序可以强制关闭连接,以节省资源。
Web服务器的实现
Web服务器实现了HTTP和相关的TCP连接处理。负责管理Web服务器提供的资源,以及对Web服务器的配置,控制及扩展方面的管理。
Web服务器逻辑实现了HTTP协议,管理着Web资源,并负责提供Web服务器的管理功能。Web服务器逻辑和操作系统共同管理TCP连接。底层操作系统负责管理底层计算机系统的硬件细节,并提供了TCP/IP网络支持,负责装载Web资源的文件系统以及控制当前计算活动的进程管理功能。
Web服务器会做些什么?
(1) 建立连接—–接受一个客户端连接,或者如果不希望与这个客户端建立连接,就将其关闭。
(2)接收请求—–从网络中读取一条HTTP请求报文
(3)处理请求—–对请求报文进行解析,并采取行动
(4)访问资源—–访问报文中指定的资源
(5)构建响应—–创建带有正确首部的HTTP响应报文
(6)发送响应—–将响应回送给客户端
(7)记录事务处理过程—-将与已完成事务有关的内容记录在一个日志文件中
处理新连接
如果客户端已经打开了一条到服务器的持久连接,可以使用那条连接来发送它的请求。否则,客户端需要打开一条新的到服务器的连接。
客户端请求一条到Web服务器的TCP连接时,Web服务器会建立连接,判断连接的另一端是哪个客户端,从TCP连接中将IP地址解析出来。一旦新连接建立起来并被接受,服务器就会将新连接添加到其现存Web服务器连接列表中,做好监视连接上数据传输的准备。
Web服务器可以随意拒绝或立即关闭一条连接。有些Web服务器会因为客户端IP地址或主机名是未认证的,或者因为它是已知的恶意客户端而关闭连接。Web服务器也可以使用其他识别技术。
Web服务器架构
1:单线程Web服务器
单线程的Web服务器一次只处理一个请求,直到其完成为止。一个事务处理结束之后,才去处理下一个连接。这种结构易于实现,但在处理过程中,所有其他的连接会被忽略,这样会造成严重的性能问题,只适用于低负载的服务器,以及type-o-serve这样的诊断工具
2:多进程及多线程服务器
多进程和多线程Web服务器用多个进程,或更高效的线程同时对请求进行处理。可以根据需要创建,或者预先创建一些线程/进程。有些服务器会为每条连接分配一个线程/进程,但当服务器同时要处理成百上千,甚至数以万计的连接时,需要的进程或线程数量可能会消耗太多的内存或系统资源,因此,很多多线程Web服务器都会对线程/进程的最大数量进行限制。
3:复用I/O的服务器
为了支持大量的连接,很多Web服务器都采用了复用结构。在复用结构中,要同时监视所有连接上的活动。当连接的状态发生变化时(比如,有数据可用,或出现错误时),就对那条连接进行少量的处理,处理结束之后,将连接返回到开放连接列表中,等待下一次状态变化。只有在有事情可做时才会对连接进行处理,在空闲连接上等待的时候并不会绑定线程和进程。
4:复用的多线程Web服务器
有些系统会将多线程和复用功能结合在一起,以利用计算机平台上的多个CPU。多个线程(通常是一个物理处理器)中的每一个都在观察打开的连接(或打开连接中的一个子集),并对每条连接执行少量的任务。
Web缓存
Web缓存是可以自动保存常见文档副本的HTTP设备,当Web请求抵达缓存时,如果本地有”已缓存“的副本,就可以从本地存储设备而不是原始服务器中提取这个文档,使用缓存有以下优点:
- 缓存减少了冗余的数据传输,节省了你的网络带宽
- 缓存缓解了网络瓶颈的问题,不需要更多的带宽就能够更快地加载页面
- 缓存降低了对原始服务器的要求,服务器可以更快地响应,避免过载的出现
- 缓存降低了距离时延,因为从较远的地方加载页面可能会更慢一些
命中,未命中,再验证
可以用已有的副本为某些到达缓存的请求提供服务,这称为缓冲命中,其他一些到达缓存的请求可能会因为没有副本可用,而被转发给原始服务器,这被称为缓存未命中。
原始服务器的内容可能会发生变化,缓存要不时对其进行检测,看看它们保存的副本是否是服务器上最新的副本。这些”新鲜度检测“被称为HTTP再验证。
缓存可以在任意时刻,以任意的频率对副本进行再验证,但由于缓存中通常会包含数百万的文档,而且网络带宽是很珍贵的,所以大部分缓存只有在客户端发起请求,并且副本旧的足以检测的时候,才会对副本进行再验证。
HTTP提供了一个用来对已缓存对象进行再验证的工具,但最常用的是If-Modified-Since首部。将这个首部添加到GET请求中去,就可以告诉服务器,只有在缓存了对象的副本之后,有对其进行了修改的情况下,才发送此对象。
下面列出了服务器在收到GET If-Modified-Since请求时发生的情况
- 再验证命中:如果服务器对象未被修改,服务器会向客户端发送一个小的HTTP 304 Not Modified响应
- 再验证未命中:如果服务器对象与已缓存副本不同,服务器向客户端发送一条普通的,带有完整内容的HTTP 200 OK响应
- 对象被删除:如果服务器对象已经被删除了,服务器就回送一个404 Not Found响应,缓存会将其副本删除
区分命中和未命中的情况
HTTP没有为用户提供一种手段来区分响应是缓存命中的,还是访问原始服务器得到的。在这两种情况下,响应码都是200 OK,说明响应有主体部分。
客户端有一种方法可以判断响应是否来自缓存,就是使用Date首部。将响应中Date首部的值与当前时间进行比较,如果响应中的日期值比较早,客户端通常就可以认为这是一条缓存的响应。客户端也可以通过Age首部来检测缓存的响应,通过这个首部可以分辨出这条响应的使用期。
缓存的处理步骤
Web缓存的基本工作原理大多很简单,对一条HTTP GET报文的基本缓存处理过程包括7个步骤
(1)接收—-缓存从网络中读取抵达的请求报文
(2)解析—-缓存对报文进行解析,提取出URL和各种首部
(3)查询—-缓存查看是否有本地副本可用,如果没有,就获取一份副本(并将其保留在本地)
(4)新鲜度检测—-缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新
(5)创建响应—-缓存会用新的首部和已缓存的主体来构建一条响应报文
(6)发送—-缓存通过网络将响应发回给客户端
(7)日志—-缓存可选地创建一个日志文件条目来描述这个事务
第三步:查找
在第三步中,缓存获取了URL,查找本地副本。本地副本可能存储在内存,本次磁盘,甚至附近的另一台计算机。专业级的缓存会使用快速算法来确定本地缓存中是否有某个对象。如果本地没有这个文档,它可以根据情形和配置,到原始服务器或父代理去取,或者返回一条错误信息。
已缓存对象中包含了服务器响应主体和原始服务器响应首部,这样就会在缓存命中时返回正确的服务器首部。已缓存对象中还包含了一些元数据,用来记录对象在缓存中停留了多长时间,以及它被用过多少次等。
第四步:新鲜度检测
HTTP通过缓存将服务器文档的副本保留一段时间。在这段时间里,都认为文档是”新鲜的“,缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副本停留的时间太长,超过了文档的新鲜度限值,就认为对象”过时“了,在提供该文档之前,缓存要再次与服务器进行确认,以查看文档是否发生了变化。客户端发送给缓存的所有请求首部自身都可以强制缓存进行再验证,或者完全避免验证,这使得事情变得更加复杂了。
HTTP有一组非常复杂的新鲜度检测规则,缓存产品支持的大量配置选项,以及与非HTTP新鲜度标准进行互通的需要则使问题变得更加严重了。
缓存GET请求的流程图
文档过期,
通过特殊的HTTP Cache-Control首部和Expires首部,HTTP让原始服务器向每个文档附加了一个”过期时间“,就像食品包装袋的过期时间一样,这些首部说明了在多长时间内可以将这些内容视为新鲜的。
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系—-当然,除非客户端请求中包含有阻止提供已缓存或未验证资源的首部。但一旦已缓存文档过期,缓存就必须与服务器进行核对,询问文档是否被修改过,如果被修改过,就要获取一份新鲜(带有新的过期日期)的副本。
服务器用HTTP/1.0的Expires首部或HTTP/1.1的Cache-Control: max-age响应首部来指定过期日期,同时还会带有响应主体。Expires首部和Cache-Control:max-age首部所做的事情上本质上是一样的,但由于Cache-Control首部使用的事相对时间而不是绝对日期,所以更倾向于使用比较新的Cache-Control首部。绝对日期依赖于计算机时钟的正确设置。
服务器再验证
仅仅是已缓存文档过期了并不意味着它和原始服务器上目前处于活跃的文档有实际的区别,这只是意味着到了要进行核对的时间了,这种情况被称为”服务器再验证“,说明缓存需要询问原始服务器文档是否发生了变化。
- 如果再验证显示内容发生了变化,缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将文档发送给客户端。
- 如果再验证显示内容没有发生变化,缓存只需要获取新的首部,包括一个新的过期日期,并对缓存中的首部进行更新就行了。
用条件方法进行再验证
HTTP的条件方法可以高效地实现再验证,HTTP允许缓存向原始服务器发送一个”条件GET“,请求服务器只有在文档与缓存中现有的副本不同时,才回送对象主体。通过这种方式,将新鲜度检测和对象获取结合成了单个条件GET。向GET请求报文中添加一些特殊的条件首部,就可以发起条件GET,只有条件为真时,Web服务器才会返回对象。
HTTP定义了5个条件请求首部,对缓存再验证来说最常用的2个首部是If-Modified-Since和If-None-Match。所有的条件首部都以前缀”If-“开头。
If-Modified-Since:Date再验证
If-Modified-Since首部可以与Last-Modified服务器响应首部配合工作。原始服务器会将最后的修改日期附加到所提供的文档上去。当缓存要对已缓存文档进行再验证时,就会包含一个If-Modified-Since首部,其中携带有最后修改已缓存副本的日期。
如果在此期间内容被修改了,最后的修改日期就会有所不同。原始服务器就会回送新的文档。否则,服务器会注意到缓存的最后修改日期与服务器文档当前的最后修改日期相符,会返回一个304 Not Modified响应。
If-None-Match:实体标签再验证
有些情况下仅使用最后修改日期再验证是不够的。
- 有些文档被周期性的重写(比如,从一个后台进程中写入),但实际包含的数据常常是一样的,尽管内容没有变化,但修改日期会发生变化
- 有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如对拼写或注释的修改)
- 有些服务器无法准确地判断其页面的最后修改日期
- 有些服务器提供的文档会在亚秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。
为了解决这些问题,HTTP允许用户对被称为实体标签(ETag)的”版本标识符“进行比较。实体标签是附加到文档上的任意标签(引用字符串)。它们可能包含了文档的序列号或版本号,或者是文档内容的校验和及其他指纹信息。
当发布者对文档进行修改时,可以修改文档的实体标签来说明这个新的版本。这样,如果实体标签被修改了,缓存就可以用If-None-Match条件首部来GET文档的新副本了。
在下图中,缓存中有一个实体标签为v2.6的文档,它会与原始服务器进行再验证。如果标签v2.6不再匹配,就会请求一个新对象。在下图中,标签仍然与之匹配,因此会返回一条304 Modified响应。
如果服务器的实体标签已经发生了变化(可能变成了v3.0),服务器会在一个200 OK响应中返回新的内容以及相应的新ETag。
可以在If-None-Match首部包含几个实体标签,告诉服务器,缓存中已经存在带有这些实体标签的对象副本。
什么时候应该使用实体标签和最近修改日期
如果服务器回送一个实体标签,HTTP/1.1客户端就必须使用实体标签验证器。如果服务器只回送了一个Last-Modified值,客户端就可以使用If-Modified-Since验证。如果实体标签和最后修改日期都提供了,客户端就应该使用这两种再验证方案。这样HTTP/1.0和HTTP/1.1缓存就可以正确响应了。
个性化接触
HTTP最初是一个匿名,无状态的请求/响应协议。服务器处理来自客户端的请求,然后向客户端回送一条响应。Web服务器几乎没有什么信息可以用来判定是哪个用户发送的请求,也无法记录用户的请求序列。
现在的Web站点希望能够提供个性化的接触。它们希望对连接另一端的用户有更多的了解,并且能在用户浏览页面时对其进行跟踪。
HTTP首部
下图中给出了七种最常见的用来承载用户相关信息的HTTP请求首部。
这七种方式,我们比较常用的就是Cookie,其他的我们先不介绍了,大家可以去网上查。
cookie
cookie是当前识别用户,实现持久会话的最好方式。可以笼统地将cookie分为两类:会话cookie和持久cookie。
会话cookie是一种临时cookie,它记录了用户访问站点时的设置和偏好。用户退出浏览器时,会话cookie就被删除了。
持久cookie的生存时间更长一些,它们存储在硬盘上,浏览器退出,计算机重启时它们仍然存在。通常会用持久cookie维护某个用户会周期性访问的站点的配置文件或登录名。
会话cookie和持久cookie之间唯一的区别是它们的过期时间,如果设置了Discard参数,或者没有设置Expires和Max-Age参数来说明扩展的过期时间,这个cookie就是一个会话cookie。
cookie是如何工作
cookie就像服务器给用户贴的“嗨,我叫”的贴纸一样,用户访问一个Web站点时,这个Web站点就可以读取那个服务器贴在用户身上的所有贴纸。
用户首次访问Web站点时,Web服务器对用户一无所知,Web服务器希望这个用户会再次回来,所以想给这个用户“拍上”一个独有的cookie,这样以后它就可以识别出这个用户了。cookie中包含了一个由名字=值(name=value)这样的信息构成的任意列表,并通过set-cookie或Set-Cookie2 HTTP响应(扩展)首部将其贴到用户身上去。
cookie可以包含任意信息,但它们通常都只包含一个服务器为了进行跟踪而产生的独立的标识码。
不同的站点使用不同的cookie
cookie的基本思想就是让浏览器积累一组服务器特有的信息,每次访问浏览器时都将这些信息提供给它,因为浏览器要负责存储cookie信息,所以此系统称为客户端侧状态(client-side state)。
浏览器内部有着成千上百的cookie,但浏览器不会将每个cookie发送给所有的站点。实际上,它们通常只会向每个站点发送2-3个cookie,原因如下:
- 对所有这些cookie字节进行传输会严重降低性能,浏览器实际传输的cookie字节数要比实际的内容字节数多
- cookie中包含的是服务器特有的名值对,所以对大部分站点来说,大多数cookie都只是无法识别的无用数据
- 将所有的cookie发送给所有站点会引发潜在的隐私问题,那么你并不信任的站点也会获取你只想发给其他站点的信息。
总之,浏览器只向服务器发送服务器产生的那些cookie。123.com产生的cookie只会发送给123.com,不会发送给456.com。
产生cookie的服务器可以向set-Cookie响应首部添加一个Domain首部来控制哪些站点可以看到那个cookie。
cookie规范甚至允许用户将cookie与部分Web站点关联起来。可以通过Path属性来实现这一功能,在这个属性列出的URL路径前缀下所有cookie都是有效的。
保护HTTP的安全
人们会用Web事务来处理一些很重要的事情。如果没有强有力的安全保证,人们就无法安心地进行网络购物或使用银行业务。如果无法严重限制访问权限,公司就不能将重要的文档放在Web服务器上。Web需要一种更安全的HTTP形式。
HTTP的安全版本要高效,可移植且易于管理,不但能够适应不断变化的情况而且还应该能满足社会和政府的各项要求。我们需要一种提供如下功能的HTTP安全技术
(1)服务器验证(客户端知道它们是在与真正的而不是伪造的服务器通信)
(2)客户端验证(服务器知道它们是在于真正的而不是伪造的客户端通信)
(3)完整性(客户端和服务器的数据不会被修改)
(4)加密(客户端和服务器的对话是加密的,无需担心被窃听)
(5)效率(一个运行的足够快的算法,以便低端的客户端和服务器使用)
(6)普适性(基本上所有的客户端和服务器都支持这些协议)
(7)管理的可扩展性(在任何地方的任何人都可以立即进行安全通信)
(8)适应性(能够支持当前最知名的安全方法)
(9)在社会上的可行性(满足社会的政治文化需求)
HTTPS
HTTPS是最流行的HTTP安全方式,它是由网景公司首创的,所有的主要的浏览器和服务器都支持此协议。
HTTPS方案的URL以https://,而不是http://开头,据此就可以分辨某个Web页面是通过HTTPS而不是HTTP访问的。
使用HTTPS时,所有的HTTP请求和响应数据在发送到网络之前,都要进行加密。
HTTPS在HTTP下面提供了一个传输级的密码安全层。
大部分困难的编码及解码工作都是在SSL库完成的,所以Web客户端和服务器在使用安全HTTP时无需过多地修改其协议处理逻辑。在大多数的情况下,只需要用SSL的输入/输出调用取代TCP的调用,再增加其他几个调用来配置和管理安全信息就行了。
SSL
SSL是个二进制协议,与HTTP完全不同,其流量是承载在另一个端口上的(SSL通常是由端口443承载的)。如果SSL和HTTP流量都从端口80到达,大部分Web服务器会将二进制SSL流量理解为错误的HTTP并关闭连接。将安全服务进一步整合到HTTP层中去就无需使用多个目的端口了,在实际中这样不会引发严重的问题。
建立安全传输
在未加密HTTP中,客户端会打开一条道Web服务器端口80的TCP连接,发送一条请求报文,接收一条响应报文,关闭连接。
由于SSL安全层的存在,HTTPS这个过程会略微复杂一些。在HTTPS中,客户端首先打开一条到Web服务器端口443(安全HTTP的默认端口)的连接,一旦建立了TCP连接。客户端和服务器就会初始化SSL层,对加密参数进行沟通,并交换秘钥。握手完成之后,SSL初始化就完成了,客户端就可以将请求报文发送给安全层了。在将这些报文发送给TCP之前,要先对其进行加密。
SSL握手
在发送已加密的HTTP报文之前,客户端和服务器要进行一次SSL握手,在这个握手过程中,它们要完成以下工作:
- 交换协议版本号
- 选择一个两端都了解的密码
- 对两端的身份进行验证
- 生成临时的会话秘钥,以便加密信道。
在通过网络传输任何已加密的HTTP数据之前,SSL已经发送了一组握手数据来建立通信连接了。
这是SSL握手的简化版本。根据SSL的使用方式,握手过程可能会复杂一些,但总的思想就是这样。
报文是箱子,实体是货物
每天都有数以亿计的各种媒体对象经由HTTP传送,如图像,文本,影片以及软件程序等。只要你能叫出名字的,HTTP都可以传送。HTTP还确保它所承载的”货物“满足以下条件:
(1)可以被正确地识别(通过Content-Type首部说明媒体格式,Content-Language首部说明语言),以便浏览器和客户端能正确处理内容
(2)可以被正确地解包(通过Content-Length首部和Content-Encoding首部)
(3)是最新的(通过实体验证码和缓存过期控制)
(4)符合用户的需要(基于Accept系列的内容协商首部)
(5)在网络上可以快速有效地传输(通过范围请求,差异编码以及其他数据压缩方法)
(6)完整到达,未被篡改(通过传输编码首部和Content-MD5校验和首部)
如果把HTTP报文想象成因特网货运系统中的箱子,那么HTTP实体就是报文中实际的货物,下图展示了一个简单的实体,装在HTTP响应报文中。
实体首部指出这是一个纯文本文档(Content-type:text/plain),它只有18个字节长(Content-length:18)。和往常一样,一个空白行(CRLF)把首部字段同主体的开始部分分隔开来。
HTTP/1.1定义了以下基本实体首部字段:
- Content-Type:实体中所承载对象的类型
- Content-Length:所传送实体主体的长度或大小
- Content-Language:与所传送对象最相配的人类语言
- Content-Encoding:对象数据所做的任意变化(例如,压缩)
- Content-Location:一个备用位置,请求时可以通过它获得对象
- Content-Range:如果这是部分实体,这个首部说明它是整体的哪个部分
- Content-MD5:实体主体内容的校验和
- Last-Modified:所传输内容在服务器上创建或最后修改的日期时间。
- Expires:实体数据将要失效的日期时间
- Allow:该资源所允许的各种请求方法,例如,GET和HEAD。
- ETag:这份文档特定实例的唯一验证码。ETag首部没有正式定义为实体首部,但它对许多涉及实体的操作来说,都是一个重要的首部。
- Cache-Control:指出应该如何缓存该文档。和ETag首部类似,Cache-Control首部也没有正式定义为实体首部。
实体主体
实体主体中就是原始货物啦。任何其他描述性的信息都包含在首部中。因为货物(也就是实体主体)只是原始数据,所以需要实体首部来描述数据的意义。例如,Content-Type实体首部告诉我们如何去解释数据(是图像还是文本等),而Content-Encoding实体首部告诉我们数据是不是已被压缩或者重编码。
首部字段以一个空白的CRLF行结束,随后就是实体主体的原始内容。不管内容是什么,文本或二进制的、文档或图像、压缩的或未压缩的、英语、法语或日语,都紧随这个CRLF之后。
下图展示了两个实际的HTTP报文的例子。一个携带着文本实体,另一个承载的是图像实体。十六进制的数值中展示的是报文的实际内容。
范围请求
HTTP允许客户端实际上只请求文档的一部分,或者说某个范围。
假设你正在通过慢速的调制调解器连接下载最新的热门软件,已经下了四分之三,忽然因为一个网络故障,连接中断了。你已经为等待下载完成耽误了很久,而现在被迫要全部重头再来,祈祷着别再发生这样的倒霉事了。
有了范围请求,HTTP客户端就可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实体到这次发出范围请求的时段内,该对象没有改变过。
下图中展示了涉及范围请求的一系列HTTP事务的例子。
Range首部在流行的点对点(Peer-to-Peer,P2P)文件共享客户端软件中得到广泛应用,它们从不同的对等实体同时下载多媒体文件的不同部分。