GET方法
GET方法用来获取URL指定的资源。这个URL指向可以是一个静态文件,也可以是一个数据生成软件产生的动态内容。
如果GET请求在首部区包含了条件获取字段,那么GET 请求就具体化为条件获取(conditional GET)。条件(获取)字段包括: If-Modified-Since、 If-Unmodified-Since、 If-Match、 If-None-Match、 If-Range。条件获取请求下,只有满足了条件的资源才会传递响应主体到客户端。这样的首部字段搭配使用就可以达成缓存的目的。
如果GET请求包括了范围条件,那么GET请求就被具体化为局部获取(partial GET)。使用局部获取,对于大文件可以分块传递从而提高传输效率。要是你在做一个视频播放应用,那么可以只传递用户跳播的视频片段,提供更好的用户体验。
以访问hello.txt获取其局部为例。使用GET方法,并通过Range头字段指定获取文件的开始字节索引和结束字节索引发出如下局部请求:
GET /hello.txt HTTP/1.1
Range: bytes=0-2 (读者补充:看这里“0-2”)
服务器响应:
HTTP/1.1 206 Partial Content
Content-Type: text/html
Content-Range: bytes 0-2/12
Content-Length: 3
hel
本案例中的Content-Range的值是一个内容为 bytes 0-2/12 的字符串,这里需要对它稍作解释:分隔符“/”的前面一组数字表明本次返回位置范围,“/”后的数字指明资源的总大小。
实验 : GET 请求、条件请求、局部请求
环境准备
(读者补充:
先
git clone https://github.com/1000copy/httpbookcode
)
在code代码目录内找到hello.js ,代码如下:
并且通过node执行:
node hello.js
(读者补充:以上代码,我运行时,报错【throw er; // Unhandled ‘error’ event】,搜索Stack Overflow找到原因和解决办法,原来是“还有另外一个实例运行”,关闭这个实例后,node hello.js
没有报错。)
验证GET和HEAD的差别
然后我们使用nc发起get:
GET /hello.htm HTTP/1.1
(读者补充:
$ nc localhost 3000【这里回车】
GET /hello.htm HTTP/1.1【这里回车】
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 22
ETag: "169148068"
Date: Mon, 25 Sep 2017 07:52:25 GMT
Connection: keep-alive
)
可以看到响应如下:
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>
如果使用HEAD方法:
HEAD /hello.htm HTTP/1.1
(读者补充:
$ nc localhost 3000
HEAD /hello.htm HTTP/1.1
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 22
Date: Mon, 25 Sep 2017 07:54:19 GMT
Connection: keep-alive
)
(使用HEAD方法之后)响应就只会发送响应消息的头,而不会发送响应消息的主体了。如下:
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
(读者补充:请注意,这里使用不是之前的GET方法,而是HEAD方法,服务器没有发送响应消息的主体,即<h1>Hello, World!</h1>
。)
验证:条件获取
本实验中的GET /hello.html的响应首部字段内,有了一个ETag的字段,它是文档内容变化的标识。如果文档改变了,这个标识就会改变。服务器发送它的目的是为了支持客户端的条件获取。客户端可以发起一个GET(请求) ,并使用条件获取首部字段,以便指示服务器,如果文档改变就发送,否则就会使用本地的缓冲。这样可以省下一些并非必要的网络流量。
继续这个案例,我们可以搭配ETag和If-None-Match头字段,达到使用缓存的效果。客户端可以通过 If-None-Match 头字段指明,如果文档标识不匹配,就发送新文档来;否则,服务器就会发送 304 Not Modified:
(读者补充:输入
nc localhost 3000
之后,继续输入如下三行。
)
GET /sample.html HTTP/1.1
Host: example.com
If-None-Match: W/"16-FmHX0hamHjYkHeAP/7PfzA"
如果服务器发现指定的标识是匹配的,那么服务器响应:
HTTP/1.1 304 Not Modified
X-Powered-By: Express
ETag: W/"16-FmHX0hamHjYkHeAP/7PfzA" (读者补充:请注意这一行的“ETag”)
Date: Thu, 03 Dec 2015 09:54:01 GMT
Connection: close
(读者的获得的响应:
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: text/html
Date: Mon, 25 Sep 2017 08:13:38 GMT
Connection: keep-alive
Content-Length: 24
Cannot GET /sample.html
)
还可以搭配 Last-Modified(响应首部字段),If-Modified-Since(请求首部字段),If-UnModified-Since(请求首部字段) 字段达到按照修改时间来让服务器决定发送文档或者客户端使用缓存文件。比如:
(读者补充:
输入:
nc localhost 3000
之后,继续输入下面3行)
GET /sample.html HTTP/1.1
Host: example.com
If-Modified-Since: Thu, 03 Dec 2015 09:54:01 GMT
指示服务器如果文件并没有在指定时间前修改过的话,那么服务器响应一个无消息主体的消息即可:
HTTP/1.1 304 Not Modified
X-Powered-By: Express
Last-Modified: Thu, 04 Dec 2015 09:54:01 GMT
Connection: close
(读者补充:读者输入的请求和获取的响应,如下:
$ nc localhost 3000
GET /sample.html HTTP/1.1
Host: example.com
If-Modified-Since: Thu, 03 Dec 2015 09:54:01 GMT
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: text/html
Date: Mon, 25 Sep 2017 08:17:52 GMT
Connection: keep-alive
Content-Length: 24
Cannot GET /sample.html
)
验证:局部获取(partial GET)
录入以下命令,查看结果。
cd code
node partial-get.js
$nc localhost 8000
GET /hello.txt HTTP/1.1
Range: bytes=0-2
(读者发起的请求和获取的响应如下:
$ nc localhost 8000
GET /hello.js HTTP/1.1
Range: bytes=0-2
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-2/224
Content-Length: 3
Content-Type: application/octet-stream
Accept-Ranges: bytes
Cache-Control: no-cache
Date: Mon, 25 Sep 2017 08:22:40 GMT
Connection: keep-alive
var
)
如果你有兴趣不妨阅读代码来了解它的具体实现。
POST 方法
POST方法常常用来提交表单数据。假设有一个表单,其html如下:
<form enctype="application/x-www-form-urlencoded" /> (读者补充:属性enctype在这一行。)
<input type="text" name="user" action="/example"/>
<input type="password" name="password" />
<input type="submit"/>
</form>
当在两个文本框内分别填写1,2,然后点击提交的时候,我们需要传递形如:
username :1
password :2
的内容给服务器。
本案例中,我们需要注意到Form的属性enctype指定了一个看起来有些复杂的值: application/x-www-form-urlencoded,这个值指示Form经由HTTP提交到服务器的消息主体采用的编码方式(读者补充:enctype可能是encode type的简写。)。最后的提交请求数据就是这样的:
POST /example HTTP/1.1
username=1&password=2
此编码方式把提交字段和值用“=”分隔,多个提交项目之间用“&”分隔,空格会被使用“+“替代,其他不是字母和数字的字符会用url encoding 来编码,替换为%HH(的形式。读者补充。),比如%21 表示 “!”。看到案例后,就知道看起来复杂的enctype其实还比较简单的。
只不过使用这个编码方式的话,一个非字母数字的二进制值会需要3个字符来表达。对于较大的二进制数据来说这样的作法实在有些浪费。因此HTTP标准也引入了新的封装格式:multipart/form-data。我们依然可以通过Form 的enctype属性指定此新的打包格式:
<form enctype="multipart/form-data" />
这样指定的封装类型,请求消息会变成:
POST /example HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
Content-Length: 554
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="username"
1
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="password"
Content-Type: text/plain
2
-----------------------------9051914041544843365972754266--
我们要留意的是,在请求消息内的第三行,也就是Content-Type字段的值内,指定请求主体内的格式为multipart/form-data,同时在“;”后还有一个boundary子字段,它的值是一个字符串。此字符串的目的就是指定每个表单字段的开始和结束。在两个boundary之间,就是一个字段的内容。每个字段内可以指定字段的名称和类型,然后加上一个空行后内容开始。直到遇到一个新的boundary结束。
boundary字符串常常由若干个连字符加上一个随机字符串构成,由客户端软件生成,算法可以自己决定,只要不会在内容中出现就可以。如果对冲突感到担心,还可以在生成后由软件在请求消息体内搜索此字符串,如果发现有相同的话,就在此基础上继续添加一个随机字符,再执行此过程直到不再出现即可。
OPTIONS 方法
请求方法OPTIONS用来查询URL指定的资源所支持的方法列表。
请求案例:
OPTIONS /example HTTP/1.1
响应:
HTTP/1.1 OK
Allow:GET,POST,PUT,OPTIONS
本请求案例中,请求的就是/example 指定的资源所支持的方法。响应案例给出的是此资源支持的请求方法,列表为GET、POST、PUT、OPTIONS。
服务器应该返回405 (Method Not Allowed)响应,如果使用的请求方法是为服务器所知的、但是并不被允许的话。 服务器应该返回 501 (Not Implemented) ,如果请求方法没有被实现的话。
(读者的补充如下。
先运行:
$ node partial-get.js
后:
$ nc localhost 8000(输入换行)
OPTIONS /example HTTP/1.1(输入换行)
(输入换行)
HTTP/1.1 405 Method Not Allowed
Allow: GET
Date: Mon, 25 Sep 2017 09:10:22 GMT
Connection: keep-alive
Transfer-Encoding: chunked
0
)
请求方法: PUT,DELETE
PUT方法的意图,是对URL指定的资源(隐含了“如果不存在”的意思。读者补充。)进行创建,如果资源存在就修改它;相应的,DELETE方法的意图是对URL指定的资源进行删除。
可是要做到这两件事,只是POST就够了。那么为何在HTTP标准内还有PUT和DELETE呢?我将会举个业务案例,给出只是使用POST、和全面使用HTTP方法的效果,以此对比来说明问题。
假设我们手里有一个电子商务网站,那么我们必然需要提供订单的维护,包括创建、修改、删除、查询。
对于查询,我们可以采用GET 方法,并提供订单编号为参数:
GET /order/1
使用GET 方法在这里是恰如其分的,因为GET隐含着只是查询,而并不会影响服务器的状态。
接下来,我们更新一个订单,假设编号为2:
POST /order/2
Content-Type:text/json
{
"date":"2015-12-07",
“guest":"frodo",
[{
"item":"The King of Ring",
"count":"2",
"price":"100"
}]
}
这里使用POST也是合适的。因为POST的语义中包括对资源进行更新。
但是对于创建和删除,我们就有不同的做法了。
首先看创建。我们常见的方法依然是使用POST,但是在URL内(或者请求消息主体内的参数)要和更新操作不同,以便区别两者:
POST /order/2/create
Content-Type:text/json
{
"date":"2015-12-07",
“guest":"frodo",
[{
"item":"The King of Ring",
"count":"1",
"price":"100"
}]
}
这样做是可以达成业务需求的。然而,我们可以有更好的选择。这个选择不但能够完成功能的需求,还能够满足Restful App的规范化需求。我们可以在创建资源是选择PUT:
PUT /order/2
Content-Type:text/json
...
删除的时候也是一样。典型的请求消息:
POST /order/2/delete
或者有人这样
POST /order/2/remove
或者还有人这样
POST /order/remove_order/2
而如果我想要满足Restful app规范,选择就是一个样:
DELETE /order/2
稍微做过总结,Restful App 的方案的好处是看得到的:
(同时也是多使用一个DELETE方法的好处。读者补充。)
- 01.把操作意图表达在请求方法内
- 02.把操作意图从URL中分离出来
相对于使用POST做全部的提交数据的做法而言,这样的做法经过一个著名框架(Ruby on Rails)的首倡,目前得到了很多框架的附和,堪称一时风气之先。这样做可以有语义上的一致性,避免不同程序员选择的不同方案导致的不必要的混乱。
尽管Web Form的Action字段只能指定为GET和POST,本身没有提供PUT、DELETE方法,但是可以通过Form隐含字段来细分POST为 PUT、POST、DELETE,比如约定一个叫做_method的字段,其值可以在PUT、POST、DELETE、POST之间选择一个:
<form method="post" ...>
<input type="hidden" name="_method" value="PUT | POST | DELETE " />
...
这样就可以由框架实现完整的对资源操作的不同类型。在使用框架的基础上,应用可以直接享受到完整的GET、PUT 、POST、 DELETE语义。
CONNECT 方法
在当前已经建立HTTP连接的情况下,CONNECT方法用来告知代理服务器,客户端想要和服务器之间建立SSL连接。
要是没有HTTP代理服务器,客户端可以使用Connection头字段来表达客户端要升级到SSL的请求:
GET http://example.bank.com/acct_stat.html?749394889300 HTTP/1.1
Host: example.bank.com
Upgrade: TLS/1.0
Connection: Upgrade
这样服务器接收到此消息即可发送:
HTTP/1.1 101 Switching Protocols
Upgrade: TLS/1.0, HTTP/1.1
Connection: Upgrade
表示确认。一次握手后,双方认可,这个http连接之后就可以发送SSL流量了。
(读者补充:以下一段解释了,为什么引入HTTP方法。)
如果中间有HTTP代理服务器的话,情况就不同了。因为我们使用的Connection头字段是hop-by-hop(逐跳)的,这个头字段会被代理服务器认为是在客户端到代理服务器之间的协议升级。于是,代理升级连接协议,解析并删除此字段(应该是指Connection字段。读者补充。)后继续转到服务器。这样服务器是收不到这个首部的,本来希望客户端和服务器直接达成SSL 升级,实际上却变成了客户端和代理服务器之间的SSL升级,这是违背Connetion字段的本意的。
为了解决此问题,HTTP 引入了 Connect 方法。客户端使用如下消息,通知代理服务器,去做一个连接到指定的服务器地址和端口(读者的理解:去做一个连接,该连接可以连接到指定的服务器地址和端口。):
CONNECT example.com 443 HTTP/1.1
代理服务器随后提取CONNECT 方法指定的地址和端口(这里是 example.com 443 ),建立和此服务器的SSL连接,成功后随后通知客户端,需要的连接建立完毕:
HTTP/1.1 OK
之后,代理服务器简单地转发客户端的消息到服务器,以及转发服务器来的消息给客户端。因为它只是转发,它就变成了一个透明代理。透明代理和一般代理是不同的,一般的http 代理不是仅仅转发,还需要解析头字段、考虑是否缓存、添加Via头字段等工作,而透明代理只管转发,不管内容和格式。
升级到 SSL只能由客户端发起。如果服务器希望升级,可以通过状态码426 upgrade required 告知客户端。
因此,客户端和服务器之间要升级到SSL,就必须区分两种情况,一种是两者之间存在代理服务器,就需要用Connect方法;否则使用第一种方法(使用Connection 头字段的方法)即可。
有很多资料提到 Connect 方法建立的是一种隧道,叫做SSL隧道。我觉得这个说法不妥,因为和隧道的定义是不符的。
隧道被用来在一个协议上承载一个系统本来并不支持的外部协议。一个协议内嵌套另一个协议,就像一个管道嵌入另一个管道,因此取名为隧道。 这就意味着,完全可能在TCP上承载IP、IPV6协议,或者在TCP上承载NetBIOS协议。
HTTP隧道使用普通的请求方法POST、GET、PUT 和DELETE来实现对被承载协议的包装。HTTP隧道服务器运行在被限制的网络外,执行一个特别的HTTP服务器角色。HTTP隧道客户端运行在被限制的网络内。当任何网络流量传递到客户端,它就把流量作为HTTP 实体,然后加上HTTP首部,传递到外部的服务器;后者(这里的“后者”应该是指HTTP隧道服务器。读者补充。)解开包,并执行这个原始的网络请求。对这个请求的响应获得后,也会被加上HTTP响应首部,传递会给客户端(这里的“会给”应该是笔误,应该是“回给”。读者补充)。对此实现感兴趣的话,可以可以参看node-http-tunnel,或者gnu 的htc、hts,他们都是开放源代码的,并且有还不错的文档帮助你。
(以下这一段我没看明白,似乎也不影响理解HTTP,暂时跳过吧。读者补充。)
我们再进一步查看CONNECTION连接和HTTP隧道在防火墙面前的差异。在使用CONNECT 方法时,一旦连接建立成功后,传递的内容如加密流量是在RAW SOCKET上,对于可以识别HTTP包格式的防火墙,可以知道这个流量尽管可能是80端口,但是并非HTTP流量,因为它的格式根本不遵循HTTP标准内的请求和响应包格式。而HTTP隧道的流量到来时,即使可以识别包格式的防火墙也会认为它传递的就是HTTP流量,因为非HTTP流量本来就是包装HTTP消息内的。因此,CONNECTION 方法建立的SSL通道并不是隧道。把Connect建立的通道叫做隧道会导致认知的混乱。
所以理解 Connect 方法,需要对比和区分以下内容:
- 01.HTTP 可以通过Connection:upgrade的方法升级到TLS。仅仅使用(适用?应该是笔误吧。读者补充。)于无代理服务器的情况。
- 02.Connect 方法是 http upgrade tls 的一个替代。针对有代理的情况。
- 03.Connect 方法成功返回后,中间的http代理变成了透明的代理:不再使用HTTP协议解析数据包和修改数据包,而是简单的转发流量。
这样就清晰了。
参考文献:
1.《HTTP小书》;
2.代码:
https://github.com/1000copy/httpbookcode;
3.《HTTP小书》“请求”章、“CONNECT”节提到的参考资料:
http tunneling
https://blog.udemy.com/http-tunneling/
HTTP tunnel
https://en.wikipedia.org/wiki/HTTP_tunnel
Tunneling protocol
https://en.wikipedia.org/wiki/Tunneling_protocol
Networking 101: Understanding Tunneling
windows tcp tunnel
http://www.codeproject.com/Articles/14617/Windows-TCP-Tunnel
L2TP layer 2 tunneling protocols
https://en.wikipedia.org/wiki/Layer_2_Tunneling_Protocol
Tunneling TCP based protocols through Web proxy
https://tools.ietf.org/html/draft-luotonen-web-proxy-tunneling-01