实验环境
为什么选择Node.js?
因为Node.js小巧,跨平台,并且内置有我们需要的HTTP标准库。
Node.js的框架Express.js的安装
在Node.js之内,我们还需要它的一个框架Express.js,以便编写HTTP应用时,可以更加简易。安装完 Node.js后,可以通过Node.js 内置的包管理工具(npm)来安装和初始化express,以便运行我给出的代码案例。只要:
$npm i express
就可以构建和准备好环境了。
注意:
作者使用的是苹果的电脑,这里我补上乌班图的安装方法:
sudo apt-get install nodejs
sudo apt-get install npm
sudo apt-get install node-express
sudo apt install nodejs-legacy
netcat
(1)为什么使用命令行工具?
因为可以降低眼球识别负担——看命令输入和执行结果常常是最小化高相关性的信息输出。而使用图形化的浏览器的话,就不可避免的需要大量的截图,以及操作过程的说明(点击、点击、下拉、拖放),并且这些截图几乎必然会包含了很多和问题无关的信息。
(2)netcat命令就是一个命令行工具。它专用于HTTP协议测试验证。 它可以在命令行中发送请求消息,接受并打印响应消息。关于netcat命令,可以在这里找到更多的信息: http://nc110.sourceforge.net。netcat命令行文件名为nc(Windows 操作系统上的文件名为nc.exe),可以输入nc执行此命令。
代码
本书涉及的代码其中有些简单的会直接在书内贴出。有些复杂点的,我把它放到一个包内,这个文件包可以在此处下载:http://badrobot.sinaapp.com/code.zip。或者在github上clone :https://github.com/1000copy/httpbookcode (注意:作者的github中,没有后续第一个实验需要的app.js等代码,最好使用我自己的:【git clone https://github.com/henry199101/http_little_book.git】)
HTTP引入
假设这样的一个场景:一个站点example.com上有一个hello.htm 的页面,位于站点的根目录。那么我可以打开一个浏览器,在地址栏内输入URL(http://example.com/hello.htm),确认回车后等待一些时间,就可以看到一个html页面呈现在浏览器内。
HTTP协议规定了如何把客户端的请求打包为HTTP请求消息并发送给服务器,也是它规定了把一个响应打包成HTTP响应消息,然后送回客户端。
以hello.htm资源的获取过程为例,具体过程是这样的:
客户端软件打开到服务器的连接,发送文本如下:
GET /hello.htm HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: example.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
服务器软件接到这个请求消息,通过解析首行 GET /hello.htm HTTP/1.1可以知道客户端发了一个GET请求,想要根目录下的hello.htm 资源,HTTP协议版本为1.1。服务器还可以根据第二行到空行之间的被称为首部字段区内得到更多客户端信息。比如看到Accept-Language: en-us,表明客户端接受美国英语的内容。
服务器软件根据(统一)资源定位符*在服务器上定位并找到此资源,打包给出如下响应到客户端*:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache/2.2.14 (Win32)
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
Content-Length: 88
Content-Type: text/html
Connection: Closed
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
客户端的浏览器接收到此响应消息完毕后开始解析。首先解析首行HTTP/1.1 200 OK,从此可以知道协议版本为1.1,状态码为200,由状态码可以知道这个请求在服务器已经成功处理。然后从第二行解析到空行,得到更多的消息首部信息。比如Content-Type: text/html 指示在本消息的主体内承载的是一个html文件;比如Content-Length: 88只是消息主体内承载的内容长度。然后在这些首部头字段的帮助下,就解析出实体内的html文件内容,并呈现html给用户。
最重要的一个概念是消息。无论是请求消息还是响应消息都有4个部分构成:首行+首部字段行区+空行+消息主体。在HTTP协议中,总是由客户端发送请求消息给服务器,服务器则返回响应消息给客户端。接下来我们分别详细的考察请求消息和响应消息。
请求消息
通用的目的下,一个请求消息是由一个请求行、0到多个首部字段行、一个空行、随后的消息主体(?此处我存在疑问,消息主体是什么?是【html/body】这样的网页代码吗?)构成的。
请求行
请求消息的第一行就是请求行。它指明使用的请求方法、资源标示符、和HTTP 版本。
请求方法可以是 GET、POST、HEAD、PUT、DELETE、CONNECT、OPTIONS、TRACE的一个。本案例中使用的是 GET 。
- GET方法 表示我要请求一个指定名称的资源。
- PUT方法 表示如果指定URL不存在就创建它,否则就修改它。资源数据由消息主体提供。
- POST方法 表示要创建一个新的子资源,或者更新一个存在的资源。资源数据由消息主体提供。
- DELETE方法 表示我要删除一个指定名称的资源。
- CONNECT方法、OPTIONS方法、TRACE方法会在后面单独讲解。
请求方法是最关键的请求消息字段。
PUT 和 POST 都可以创建和更新资源,如何选择?假设我们正在设计电子订单系统,那么:
- PUT /orders/1 创建订单号1的资源;如果此订单已经存在,那么就更新它。订单号是由客户端指定的。
- POST /orders 创建一个订单,新订单号由服务器指定。
- POST /orders/1 如果订单1存在就更新它。如果1不存在,应该抛出“资源未找到”错误。
特别请留意,重复执行 PUT 请求是不会影响服务器状态的。在HTTP 协议中,这个特性被称为幂等性。所以,如果可能,优先使用PUT创建资源。在我们的订单案例中,如果重复一次提交POST创建子资源的请求,会导致创建两个订单。因此POST是不具备幂等性的。
首部字段
零行或者多行头字段行。可以用来传递客户端的更多信息,以及传递解析消息主体的必要信息。案例中的:
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: example.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
都被称为首部字段(或者称为头字段)。首部字段行由冒号分隔为两部分,左边的被称为头字段名,右边的是*头字段值。比如Host: example.com,说明头字段Host的值为example.com。头字段的可选值是一个超长的列表,对应它们的值也各有不同。暂时先不关注这些细节。
空行(CRLF)
指示头字段区完成,消息主体开始(如果有消息主体的话)。
消息主体
消息主体是请求消息的承载数据。比如在提交POST表单,并且表单方法不是GET时,表单数据就是打包在消息主体内的。消息主体是可选的,本案例是没有主体的。
响应消息
响应消息由一个状态行、一个或者多个首部字段行、一个空行、消息主体构成。
状态行
由http版本、状态码、状态描述文字构成。如HTTP/1.1 200 OK。状态码200表示成功。
状态码共有5组,分别是 100-199,200-299,300-399,400-499,500-599的范围。
- 200-299 成功。 指明客户端请求是正确的,并被成功执行。
- 300-399 重定向。指明客户端请求是正确的,不过当前请求资源的位置在别处,请再次定向你的资源位置,发起新的请求。
- 400-499 客户端错误。 指明客户端的请求是不正确的,可能是格式无法识别,或者URL太长等等。
- 500-599 服务器端错误。 指明客户端的请求正确,但是服务器因为自身原因无法完成请求。
- 100-199 信息提示。 这个系列的状态码只有2个,但是比较费解,会专门单独的做出解释。
状态码是最关键的响应消息字段,选择不同的状态码常常意味着不同的首部字段和主体。
首部字段(也称为头字段)
和请求消息类似,首部字段会包括服务器本身的一些信息指示、以及响应消息本身的元数据。本案例中这些行都是头字段:
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache/2.2.14 (Win32)
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
Content-Length: 88
Content-Type: text/html
Connection: Closed
比如Server: Apache/2.2.14 (Win32)指示服务器使用的是Apache Server。而Content-Type: text/html 指明消息主体是html格式的资源。
一个空行(CRLF)
指示头字段完成。
可选的消息主体
案例中就是一个hello.htm文件的内容。
实验:表象下的细节
搭建一个简单的HTTP 服务器,并使用nc命令行发送请求,接受响应消息。
1. 建立一个http 服务器。
服务器代码(文件名为app.js):
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('<a href="/test204">204</a> <a href="/test205">205</a> <a href="/test300">300</a>');
});
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
2. 执行服务器
执行如下命令:
$node app.js
要是报错的话,可能因为你还没有搭建好环境。请移步到实验环境一章,首先搭建好环境,然后来运行此案例。
如果看到打印消息:
Example app listening at http//localhost:3000
就说明服务器准备完毕并且在3000端口等待客户端的连接。
3. 发送请求,查看响应
执行 nc(netcat),随后在控制台输入GET / ,两次回车,即可发出请求。随后nc会打印响应消息如下:
$ nc localhost 3000
GET /
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 80
ETag: W/"50-41mmSLl6PW+Zt5VLKLE2/Q"
Date: Thu, 03 Dec 2015 08:54:23 GMT
Connection: close
4. 降低版本试试
我们有提到了HTTP的版本。目前的HTTP版本有0.9、1.0 、1.1、2.0 之分。目前的主流使用版本是1.1版。使用nc和Node.js,不但可以查看1.1 的HTTP响应,还可以查看协议版本为 1.0 的http响应。可以对比查看两者的差别。
$ nc localhost 3000
GET / HTTP/1.0
HTTP/1.0 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 80
ETag: W/"50-41mmSLl6PW+Zt5VLKLE2/Q"
Date: Thu, 03 Dec 2015 08:52:45 GMT
<a href="/test204">204</a> <a href="/test205">205</a> <a href="/test300">300</a>
术语
为了更好的讨论问题,我们引入一系列的相关概念:
资源(resource)
Web资源是使用URL指向的Web内容。
内容可以是静态的,如:文本文件、HTML文件、JPEG文件。可以查看RFC 2045文档,了解这个很长的清单。
或者是动态的内容。如:摄像头的实时采集软件生成的动态影像,用户填写的电子网站订单。
资源类型
Web服务器会为所有HTTP资源赋予一个类型,以便于HTTP软件处理消息主体。如,用text/html标记html。可以再看两个案例:
text/plain :ASCII文本文档
image/jpeg :JPEG版本的图片
每个条目对应的文本格式由表示一种主要的对象类型和一个特定的子类型组成,中间由一条斜杠来分隔。
非常多的资源类型和文本标记的对应关系,一起构成了一个超长的清单,并且由RFC 2045标准化。此标准被称为MIME。MIME是Multipurpose Internet Mail Extension(多用途互联网邮件扩展)的缩写。虽然名称很长,但是含义简单,就是用来指定消息内的实体类型的。之所以有Mail字样,是因为最初设计是为了Mail的异构系统交换文档的。
资源标示符
URL是一种资源位置标示方法。URL描述了一个资源在服务器上的位置。这就是一个合法的URL:http://example.com/part/index.htm
01.第一部分:方案(scheme)。指明了访问资源所使用的协议类型。这部分通常是HTTP协议(http://)。
02.第二部分:服务器地址(比如,example.com)。
03.其余部分指定了Web服务器上的某个资源(比如,/part/index.htm)。
URL 是 Uniform Resource Locator (统一资源定位符),用来指向互联网的一个资源。 一个典型的URL :http://www.example.com/index.html, 指示了协议 (http), 一个主机名(www.example.com), 和一个资源名 (index.html)。
当在地址栏输入此资源名并回车后,用户代理会把URL解析,把必要的信息以HTTP协议的要求,打入请求消息内。以http://www.example.com/index.html,变成
GET index.html HTTP/1.1
host:www.example.com
CRLF
打开到www.example.com的tcp连接,并发送此请求消息给服务器,然后等待服务器响应并解析显示给用户。
HTTP事务
一个HTTP事务由一条请求消息和一个响应消息构成。
HTTP方法
HTTP支持几种不同的请求命令,这些命令被称为HTTP方法(HTTP method)。每条HTTP**请求报文都包含一个方法**。
状态码
每条HTTP**响应消息**返回时都会携带一个状态码。状态码是一个三位数字的代码,(1)告知客户端请求是否成功,(2)或者是需要采取其他行动。
消息
从Web客户端发往Web服务器的HTTP报文称为请求消息。从服务器发往客户端的消息称为响应消息。HTTP报文包括三部分:
- 起始行
- 首部字段
- 主体
如发送一个hello.htm 的资源给客户端,请求消息是:
GET /hello.html HTTP/1.1
请求消息只有起始行,指明使用的HTTP方法、资源的URL,以及协议的版本。没有首部字段和主体。
响应消息为:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 22
ETag: W/"16-FmHX0hamHjYkHeAP/7PfzA"
Date: Thu, 03 Dec 2015 09:54:01 GMT
Connection: close
<h1>Hello, World!</h1>
这个消息第一行为起始行,指明协议版本、状态码(200表示成功)和状态说明(OK)。接下来一直到空行之间都是首部字段,用来说明服务器、资源类型、内容长度、生成文档时间等。空行后就是主体,这里就是一个html文件的内容。实际上,主体可以承载任何内容,而不限于文本。
一点历史
一个物理学家想要做一个可以在不同地点的科学家之间分享知识的超文本文档网络。他称之为 World Wide Web (WWW就是世界范围的网络——多么宏大的名字)。随后他编写了让世界网络成为现实的服务器和客户端。
超文本本身被称为 HTML。它的”超“字,来源于一个最显著的特点:在一个 HTML 文档内,除了本文档的内容外,还可以通过URL指向其他文档、图片、视频等不同的媒体格式。
为了传递 HTML 文档,Berners-Lee设计了一套应用层协议,随后被标准化,正式协议文本的第一版为HTTP/1.0。之前的协议,虽然没有被正式规范,为了沿袭兼容,被称为 HTTP/0.9。
当年Berners-Lee采用NeXTStep环境,HTTP则因为互联网的潮流快速成为主流的应用层协议。
WWW技术包括了HTTP协议、HTML标准,以及作为实现的众多的服务器、客户端、代理软件。WWW位于应用层,和它处于同一层面的还有很多包括Email、FTP、Telnet等。
在这里,我们关心HTTP(超文本传输协议)。
为何搞一个新的协议
在Berners-Lee的年代,已经存在了一些应用层协议,包括Email、FTP、News,但是他提出了现有协议的局限和新协议的更多需求。
- Email 用来由作者发起,发送一些暂态(暂态:一段短暂时间。读者补充。出处:暂态-维基百科。读者认为,这个术语,似乎是作者自己提出来的,并且对理解本书也没有帮助,还是跳过吧。)的消息给少量的接收者。
- FTP 可以传递文档,但是在响应者一端只能做很少的处理。
- News 可以广播暂态消息给大量听众。
基于这样的认识,HTTP 应该能够:
01.只要一部分的文件传输功能。(即)HTTP 只要客户端单向发起,服务端单向响应。
02.自动在客户端与服务器之间协商格式。(这里的格式是什么意思,读者尚未明白。)
03.引导客户端到别的服务器的能力(这一条,读者还没明白。)。
04.可以由作者放置长效文档,访问者可以输入URL,访问得到这个文档。
这些看来极为基础的思想成为当前HTTP的种子。
请求
HTTP协议只允许客户端发起请求,也只允许服务器针对请求返回响应。我们已经看到了请求的样例,现在我们具体了解HTTP请求消息的构成。*请求消息包括一个首行、首部、请求主体。
Request = Request-Line CRLF
*Header CRLF
CRLF
[ message-body ]
首行
请求行由请求方法、请求URL、一个HTTP版本构成。(读者补充:以下应该是请求行的格式。)
Request-Line = Method SPACE Request-URI SPACE HTTP-Version CRLF
请求方法
HTTP/1.1支持的请求方法包括:
Method = "OPTIONS"
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "DELETE"
| "TRACE"
| "CONNECT"
| extension-method
extension-method = token
HTTP 0.9协议版本中,GET方法是唯一被支持的HTTP请求方法,用来向服务器请求一个指定URL关联的资源。随后,为了优化的目的,HEAD方法被加入进来;为了支持向服务器提交数据,引入了POST、PUT、DELETE方法。分别在语义上表达更新、创建和删除指定的资源。对于任何一个通用目的的HTTP服务器,GET、HEAD方法是必须实现的,其他的方法都是可选实现的。可以采用OPTIONS方法去查询一个服务器资源所支持的请求方法清单。
首部
首部包括零到多个首部行,每个首部行由“:”分隔,左边的文本被称为首部字段,右边的文本被称为首部值。采用文本的首部让HTTP变得容易扩展。只要客户端和服务器做好约定,就可添加自己需要的新的请求方法。
消息主体 message-body
消息主体就是消息的承载内容。单独分章节随同请求方法来做说明。
稍作总结:
- GET 表示我要请求一个由URI指定的在服务器上的资源。
- PUT方法 表示如果指定URI资源:
不存在就创建它;
存在就修改它。 - POST方法 表示要创建一个新的子资源,或者更新一个存在的资源。
- DELETE表示我要删除一个由URI指定的资源。
- HEAD 和GET一样,但是仅仅返回指定资源响应的头部分,而不必返回响应主体。
- OPTIONS查询目标资源支持method的清单。
- TRACE查询到目标资源经过的中间节点。用于测试。
- CONNECT建立一个到URI指定的服务器的隧道。
请求之请求方法(更详细的介绍)
响应
服务器执行了客户端请求后,需要给客户端以响应,告知它请求的执行状态、以及放置在消息主体内的执行结果。
响应消息构成:
Response = Status-Line (请求的执行状态,读者补充。)
*(header CRLF) (CRLF的意思是回车换行,读者补充。)
CRLF
[ message-body ]
状态行( Status-Line)
由HTTP版本、状态码、状态说明共三个字段构成:
Status-Line = HTTP-Version SPACE Status-Code SPACE Reason-Phrase CRLF
状态说明(Reason-Phrase)就是一个给人类阅读的内容,对于处理逻辑并不重要。 HTTP版本、状态码则直接影响客户端如何处理此响应消息。状态码是一个三位的数字,用来告知客户端请求的处理结果。三位数字的第一位是一个分类,指明状态码的类型,以此数字把状态码分为5类:
- 1xx: 信息类 。
- 2xx: 成功。请求被成功地接受或者理解,或者执行。我们常见的200 OK就是这个分类内的。
- 3xx: 重定向 - 为完成请求,需要进一步的行动。我们常见的301 Redirect就是这个分类内的。
- 4xx: 客户端错误。客户端提交的数据错误,不能被理解或者接受等等。我们常见的404 Not Found 就是这个分类内的。
- 5xx: 服务器错误。错误发生了,是服务器的问题,和客户端无关。
本书内,我们把响应消息按照状态码类别的不同分为6节来阐释。2xx-5xx 的各有一节。而其中1xx内的状态码目前只有两个,但是因为它们俩都相对比较复杂,因此拆分为两节,以便说明和理解。
首部字段
和请求消息的首部字段类似,只是因为请求和响应的差异,可以选择的首部字段各有不同。这些字段会在随后的章节,随同状态码分类来做介绍。
消息主体 (message-body)
消息主体承载响应请求消息的具体内容。会在随后的章节,随同状态码分类来做介绍。
响应之状态码(更详细的介绍)
消息主体
无论是请求消息还是响应消息,都有一个可选的消息主体(message-body)。如果客户端在提交表单,那么请求消息主体内就可以放置表单的数据;如果客户端请求下载一个gif,那么响应消息主体内就可以放置gif文件的二进制字节集合。所以,消息主体内可以放置任何内容。它的定义也是如此:
message-body = *OCTET (*OCTET表示多个字节)
OCTET 就是字节。而 *OCTET
则标示为多个字节。 仅仅看*OCTET是无法知道其中到底是什么内容。这就需要在消息头字段内用一组头字段来标示它。比如Content-Type就会指示内容的类型。下图列出可作为消息主体限定的首部字段清单。
entity-header = Content-Encoding
| Content-Language
| Content-Length
| Content-Location
| Content-MD5
| Content-Range
| Content-Type
| Allow
| Expires
| Last-Modified
| extension-header
extension-header = message-header#response(OK)
我们看看每个字段的含义:
- Content-Type 实体中所承载对象的类型。
- Content-Length 所传送实体主体的长度或大小。
- Content-Language 与所传送对象最相配的人类语言。
- Content-Encoding 对象数据所做的压缩格式。
- Content-Location 一个备用位置,请求时可通过它获得对象。
- Content-Range 说明它是整体的哪个部分。
- Content-MD5 实体主体内容的校验和。
- Last-Modified 所传输内容在服务器上创建或最后修改的日期时间。
- Expires 实体数据将要失效的日期时间。
- Allow 该资源所允许的各种请求方法,例如,GET 和 HEAD。
- ETag 这份文档的唯一验证码。
消息主体可以放置静态文件内容、动态生成内容、还可以放置压缩后的动态和静态内容。整个内容可以一次传输完毕,或者分成多块传输。如果有必要,在内容后,还可以放置拖挂——一些只有内容传递完毕才能够知道的首部字段值。
我们重新查看下 HTTP引入一章已经提到的响应消息案例:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: express
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
Content-Length: 88
Content-Type: text/html
Connection: Closed
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
它正是在主体内放置静态Html文件,然后通过Content-Type指明承载内容类型、通过Content-Length 指明内容长度的。
内容类型(Content-Type)
Content-Type可以是丰富多彩的静态文件,也可以是一些在文件系统内看不到的但是非常实用的格式,比如表达表单数据的multipart/form-data。此格式可以在 POST请求 一节内看到案例。再比如 multipart/x-byterangs ,用来传递文件局部。此格式可以在GET请求 一节内查看案例。
内容编码(Content-Encoding,内容的压缩编码格式。)
消息主体可以压缩后再传递从而节省网络传递流量。Content-Encoding就是内容的压缩编码格式。目前支持的压缩方式有:
- gzip : GNU zip 编码
- compress : Unix 的文件压缩程序
- deflate : zlib 的格式压缩
- identity: 没有进行压缩。
传输编码 Transfer-Encoding
传输编码可以把消息主体分为若干块大小已知的块来传输。Transfer-Encoding 字段目前的取值只能是chunked,表示分块传输。有了分块传输,可以边生成边传输给客户端,从而提升良好的客户体验。
假设我们要分块传递一个 hello world 的字符串,先传5个字节再传后面的7个字节。那么响应消息如下:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Server: express
5
hello
7
world
0
分块主体结构比较简单,首选发送一个数字(16进制)指明本块大小,随后回车标示本块开始。接下来第二个块、以及更多的块也是以块大小的数字开始,随后回车标示本块开始。如此等等。直到块结束就跟着一个数字0。整个内容传递完毕。
拖挂(Trailer)
可以在内容传递完毕后,接着加入一些首部值作为拖挂。之所以这些首部值不放到真正的首部区域,是因为这些首部值只有传递内容都完成了才知道。比如,我希望为分块传输添加一个内容校验首部的话,那么此值必须全部内容都传递了才能计算完成。
如果要放置拖挂,需要首先在首部使用Trailer头字段声明拖挂字段名。在传递内容完毕后,填写字段和字段值,以“:”分隔。依然以hello world字符串为例:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Server: express
Trailer: checksum
5
hello
7
world
0
checksum:5eb63bbbe01eeed093cb22bb8f5acdc3
连接的利用方法
HTTP是一个应用层协议,基于TCP/IP。HTTP的连接其实就是TCP连接,从头到尾都是。只不过在其中传递的数据包不是任意的二进制,而是HTTP规定好的数据包。数据的发送也不是任意的,而是一个请求一个响应(请求和响应一对构成一个事务)。
我们关心连接的使用方式,是因为它事关多事务情况下的性能。而在http中,处理多事务的连接有4种方式:
- 并行连接
- Keep-Alive
- 持久连接
- 管线(pipeline)。
我们拿一个简单的文件作为多事务的案例(文件名为1.html):
<html>
<body>
<h1>image/<h1>
<img src="2.png"/>
<img src="3.png"/>
</body>
</html>
即使如此简单的HTML文件,完全获取资源并呈现给客户也需要三个事务。这三个事务分别用来获取1.html、2.png、3.png文件。我们来看不同的事务处理模型应对此组请求有何不同。
并行连接
我们首先访问1.HTML,那么客户端会发送1个请求给服务器。首先获取1.html,是“打开连接、发送请求、获取响应、关闭”。
GET /1.HTML
然后获取2.png,依然是“打开连接、发送请求、获取响应、关闭”。
GET /2.png
然后获取3.png,依然是“打开连接、发送请求、获取响应、关闭”。
GET /3.png
这样,为了获取3个文件,我们做了三次重复的连接打开和关闭过程。因为获取了1.html ,解析知道1.png和 2.png文件,之后可以不必分次序,而是同时打开两个连接,分别同时获取2个png资源,然后关闭。
然而,既然三个资源都在同一个服务器,请求也都来自一个客户端,是否可以重用既有的已经打开的连接呢?这样做是允许的,接下来的两种连接模型都是出于这样的优化目的而设计出来的。
keep-alive连接
keep-alive方法允许客户端和服务器暂时不关闭连接而继续用于接下来的事务。HTTP协议引入了Connection:keep-alive的首部字段,让双方都可以表达保持连接打开的意图。这样,上面的3个事务就变成了:
打开连接、发送请求、获取响应、但是不关闭:
GET /1.HTML
Connection:keep-alive
---------
HTTP/1.1 200 OK
Connection:keep-alive
内容...
然后获取2.png,“使用现有连接、发送请求、获取响应、也不关闭”。
GET /2.png
Connection:keep-alive
---------
HTTP/1.1 200 OK
Connection:keep-alive
内容...
然后获取3.png,依然是“使用现有连接、发送请求、获取响应、关闭”。
GET /3.png
---------
HTTP/1.1 200 OK
内容...
最后一条事务没有发送Connection:keep-alive,因此连接关闭。
使用keep-alive之后,效果就是后面的两个事务可以重用第一个事务建立的连接,从而省下两次打开和关闭连接的开销。
持久连接
持久连接是对 keep-alive 的改进。持久连接通过头字段值Connection:close 来通知连接关闭,如果没有发送,就表示保持打开。和 keep-alive 的差别在于默认值的不同,持久连接默认保持,而Keep-Alive默认关闭。这就是两者的不同。再看同样的案例,在持久连接下的不同表现:
打开连接、发送请求、获取响应、但是不关闭:
GET /1.HTML
---------
HTTP/1.1 200 OK
内容...
然后获取2.png,“使用现有连接、发送请求、获取响应、也不关闭”:
GET /2.png
---------
HTTP/1.1 200 OK
内容...
然后获取3.png,依然是“使用现有连接、发送请求、获取响应、关闭”:
GET /3.png
Connection:close
---------
HTTP/1.1 200 OK
Connection:close
内容...
持久连接和Keep-alive在重用连接方面是一致的。但是使用持久连接可以少发送两次Connection头字段。
管线
管线是在持久连接的基础上的又一次优化。持久连接内的事务还是逐个方式的。就是说,客户端发起一个请求,然后等待响应,响应收完了再发新的请求。而管线的做法是不同的,在这个模型下,客户端可以一次发出全部请求,然后按照发出的次序,逐个的收对应的响应。依然看案例:
首先获取1.html,打开连接、发送请求、获取响应、但是不关闭:
GET /1.HTML
---------
HTTP/1.1 200 OK
内容...
然后解析完成1.html,浏览器发现要完整呈现还需要两个资源:2.png和3.png。这时就和前面的事务模型有体现出差别了:客户端会同时在同一连接内发出两个GET请求,而服务器会按照请求的次序,发送两个响应回来。
GET /2.png
GET /3.png
Connection:close
---------
HTTP/1.1 200 OK
内容...
HTTP/1.1 200 OK
Connection:close
内容...
这就是管线模式和持久连接的不同。在高时延网络条件下,这样做可以降低网络时间。
嗯,这就是4种处理多事务的连接模型的差别。
实验:管线连接能力的验证
使用nc和echo的命令组合,可以同时发送两个资源请求到Node Http服务器,如果服务器返回我们请求的两个资源,就说明服务器是支持管线(pipeline)能力的。
cd code
node pipeline.js
然后执行命令:
$(echo -en "GET /1 HTTP/1.1\n\nGET /2 HTTP/1.1\n\n"; sleep 10) | nc localhost 3000
客户识别
HTTP要用到客户识别。
在软件开发过程中,即使只是实现购物车功能,服务器也需要区别客户的。HTTP协议以Cookie技术来应对此需求。
Cookie就是用于标识的一个字符串。它会对每个首次来访的客户发一个不同的字符串,客户端会存下此字符串,以便在接下来的同一会话访问过程中提交此字符串给服务器来亮明身份。这样服务器就知道此次访问时之前的是哪个客户端了。
我们还可以再具体点,假设两个客户端来访,它们分别是 A 和 B 。
A首次访问,服务器响应:
HTTP/1.1 200 OK
Set-Cookie:id=25
这是你的首访
B首次访问,服务器响应:
HTTP/1.1 200 OK
Set-Cookie:id=26
这是你的首访
A再次访问,发送之前的Cookie给服务器:
GET /A HTTP/1.1
Cookie: id=25
服务器解析Cookie头字段,知道这个客户是A,可以做出个性响应:
HTTP/1.1 200 OK
欢迎你第二次访问,你的号牌为25
B再次访问,带上Cookie:
GET /A HTTP/1.1
Cookie: id=26
服务器响应:
HTTP/1.1 200 OK
Set-Cookie:id=26
欢迎你第二次访问,你的号牌为26
有了这个客户端识别ID,我们就可以完成客户识别的目标了。
Cookie 格式定义
Cookie并不是只能存储ID,你可以使用键值对,像这样:
Cookie: name="1000copy"; charactor="strong";height="175cm"
还可以指定特殊的键值对,以便指示Cookie的使用范围(DOMAIN)、失效日期(Expires)、安全(SECURE):
Cookie: name="1000copy";Expires=Wed, 09 Jun 2021 10:18:14 GMT
它的格式设计是面向通用目的的:
Set-Cookie:
name=value
[;EXPIRES=dateValue]
[;DOMAIN=domainName]
[;PATH=pathName]
[;SECURE]
此 Cookie 格式定义是网景公司首次提出和采用的,其他的浏览器也支持此格式。之后,Cookie 定义格式也有经过RFC的规范过程,并由此基础上,提出了Set-Cookie2 和 Cookies 的格式。但是这组格式定义显得稍微复杂,且用户代理实现不一,并没有广泛且完整的被业界采用(那还说个蛋啊?读者补充)。对它们有兴趣的读者,可以参看rfc6265。
参考文献:
1.《HTTP小书》。