HTTP必知必会
HTTP协议作为网络传输的基本协议,有着广泛的应用。HTTP协议的完整内容很多,但是其核心知识却又简单精炼。学习者应该掌握其基本结构,并且能够举一反三。这篇文章所列的,就是在实际开发中必须知道必须掌握的HTTP知识。
HTTP协议
HTTP协议:消息的分类
HTTP消息(有的文章称之为报文)分为请求消息和响应消息两种基本分类。其中请求消息是客户端发送给服务器的用于请求服务和资源的消息,响应消息是服务器对请求消息的应答。一般来说,一个响应对应一个请求,不多也不少。
HTTP协议:特点
HTTP协议被人总结为无连接、无状态的特点:
-
无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
-
无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP协议:消息的基本格式
HTTP协议的请求消息和响应消息的格式及其相似。提炼出它们的共性,可以指出,HTTP消息分为三个部分:
-
首行
-
头部(Header)
-
正文(Body)
其中,头部用来指出HTTP消息的一些属性,它们有固定的格式;正文部分是传输的实际内容,它们的格式是任意的,通常用Content-Type头来指定。首行在请求消息和响应消息中具体格式略有区别,它们表示的按理说应该是HTTP消息最基本的部分。不论是HTTP请求还是HTTP响应,首行都是有的,否则会出现不可饶恕的解析错误;然而头部和正文是可选的,不过实际过程中,多多少少都要包含一些基本的头。
HTTP消息主要是基于ASCII编码的消息实体。主要的意思是指首行和头部都是以ASCII编码,而正文部分的编码就显得任意了。在实际的开发中,发送的文本消息时常会碰到乱码的问题。一种解决办法是,对于文本消息,约定以UTF-8格式进行编码和解码。
知道的人也许知道,HTTP消息是基于TCP协议的上层应用协议。TCP协议是网络流协议的一种。抽象地讲,就是从一台主机一个字节一个字节有序地传输到另一台主机。对于HTTP协议来说,自然保持了这种有序性,即按照首行、头部、正文的顺序进行传输。首行和头部都是ASCII文本流,正文部分是字节流。一个特殊的控制结构CRLF用来控制每个部分的结束。
CRLF是回车符和换行符的意思,它们是两个特殊的ASCII字符。CR是回车符(\r),在ASCII中的编码是13;LF是换行符(\n),在ASCII中的编码是10.
下面通过一个例子来解释CRLF在HTTP消息中的控制。
GET /simple.html HTTP/1.1<CRLF> ----- 首行
Accept: text/html<CRLF> --|
Accept-Language: zh-cn<CRLF> |
Accept-Encoding: gzip, deflate<CRLF> |-- 头部
User-Agent: Mozilla/4.0<CRLF> |
Host: localhost:8080<CRLF> |
Connection: Keep-Alive<CRLF> --|
<CRLF> ----- 空白行表示头部的结束
----- 接下来的内容是正文部分
这是一个简单的HTTP请求消息。我在其中做了一些必要的删减,以便每个头足够短都能在一行中显示。记住首行和头部是ASCII流,正文部分是字节流,它们在消息实体中是连续的片段,并不像代码中所示那样有换行的结构。换句话说,原始的消息应该是如下形式:
GET /simple.html HTTP/1.1<CRLF>Accept: text/html<CRLF>Accept-Language: zh-cn<CRLF>Accept-Encoding: gzip, deflate<CRLF>User-Agent: Mozilla/4.0<CRLF>Host: localhost:8080<CRLF>Connection: Keep-Alive<CRLF><CRLF>
回到之前有换行符的代码例子中去。将每个CRLF单独列为一行是便于观察组织。可以清楚地看到,第一行是首行,以CRLF标志其结束;接下来是头部,含有多个消息头,每行定义一个消息头,以CRLF标志其结束;一个单独的CRLF(紧接着上一个CRLF)表示整个头部的结束,接下来是正文部分。在这个示例中,正文部分为空。
另外,可以看到每个消息头的格式都是一致的,即Key:Value
的形式。其中Key表示消息头的键,Value表示消息头的值。
HTTP请求
接下来具体讲讲HTTP的请求消息。诚心而论,光是写上面这么点内容就花费了我好久。每每想到写博客耗费的精力和时间,都会影响到我写博客的动力。
之前已经说过,HTTP请求消息也分为三个部分:
-
请求行
-
请求头部
-
请求正文
其中请求头部的格式我们已经见过。请求行的基本格式为:
方法 路径 版本
例如下面的例子:
GET /simple.html HTTP/1.1
就有对应关系:
-
方法:GET
-
路径:/simple.html
-
版本:HTTP/1.1
请求行是HTTP请求消息的最基本要素。版本是用来声明HTTP消息的解析规则,不同的版本在某些地方的表现是不同的,这里不作过多拆解了。现在实际应用中最新的HTTP协议版本就是HTTP/1.1。路径可以理解成该请求消息发往服务器的入口,一般来讲,同一个路径应该代表同一个资源实体。方法表示对该资源实体进行的操作,例如上述的GET方法,其含义就是请求获取该资源的内容。这些都是通常的解释,但不是必然的要求。实际上,服务器会解析到方法和路径,根据方法和路径做出自己相应的响应。这种响应的规则,可以遵循某些规范,也可以完全不考虑这些规范,是任意的。市面上已经存在一些约定俗成的规范了,比如Restful。Restful是非常优秀的基于HTTP协议的WEB API设计理念,很值得讲,但在这里就不讲了。
HTTP请求:方法
首先列出最常用的HTTP方法:
-
GET
-
POST
-
PUT
-
PATCH
-
DELETE
-
HEAD
-
OPTIONS
之前说过,服务器对于方法的处理,是没有强制的规范的。这句话说得并不全对。其实每个HTTP方法,都是有一些HTTP协议要求的。比如说GET方法请求的资源,浏览器端一般都会有缓存,下次请求的时候可能从缓存中去取就够了,服务器不用再重复发送相同的资源了;但是服务器如果将获取资源的接口的方法定义为POST,那么浏览器端就不会再对资源进行缓存了,即使每次取到的都是同样地内容,都会请求服务器重新发送一遍。所以说,将请求资源的接口的方法定义为POST而不是GET,就是一种不合理的设计。
再比如,GET方法的请求消息是不能定义消息体的,HEAD方法的请求其响应消息是不包含消息体的,这些都是HTTP协议对于HTTP方法的约束。
HTTP请求:路径
方法和路径的组合构成WEB API的入口,路径也是很关键的。路径的基本格式一般是:
basic-path[?query-string]
其中[]
中的内容表示可选的。在上例中,basic-path就是/simple.html,但不包含query-string的内容。basic-path形式很像UNIX中绝对路径的样式,要以/
打头。单独的/
表示一种路径,/a
、/a/b
、/a/b/c
都是合理的路径表示。不推荐使用/a/
、/a/b/
、/a/b/c/
这样/
后面不跟任何其他内容的形式(/
除外)。优秀的API设计者会利用不同的路径层级来合理地组织资源。
问号后面的部分就是query-string。它的格式是任意的,只要客户端和服务器约定好一定的形式即可。这个部分一般是请求参数的附加。之前说过,GET方法是不包含请求体的,所以GET方法的HTTP请求想要附加参数只能使用这种方式。当然其他方法也是可以使用这种方式附加参数,只要服务器同意就可以了。query-string的格式任意,但在客户端和服务器之间也有预先定好的约定,即键值对的形式。query-string可以表示成一系列键值对的集合,用以下方式表示:
k1=v1&k2=v2&k3=&k4
在这里,&
分隔不同的键值对,=
表示键和值得关系。可以看到一共有四个键值对关系,它们是:
-
k1: v1
-
k2: v2
-
k3: 空字符串
-
k4: 起码该键被定义了
一般来说,键值对要写成k=v
的形式,但是k=
和仅仅一个k
都是允许的,前者表示键k
的值是空字符串,后者表示键k
被定义了,但是其值是什么并不关心。
从上面的例子中发现,在query-string中&
和=
被用于特殊的用途了,我们不能再在其中从容地使用这两个符号了。如果我们要在值中包含这两个符号,那咋办呢?方法就是,编码。
在实际的HTTP请求中,对于如下的键值关系
k1: &
k2: =
具体的query-string要写成:
k1=%26&k2=%3D
这是因为在ASCII编码中,&
的16进制表示是26
,=
的16进制表示是3D
。对于需要的编码,就要表示成其实际编码的16进制表示,每个字节都用一个%XX
三个字符进行表示。这样,%
本身也就要进行编码了,它的编码是%25
。除了这些控制字符的编码,还可以进行中文等非英语语言的编码。
对于编码部分,我推荐阮一峰的一篇博文
虽然看了也未必懂了,但是最起码知道编码不是一件简单的事情。
HTTP请求头
HTTP请求头格式与之前所说的消息头格式没什么两样,就是以冒号分隔的键值对。HTTP请求头中,既包含预定义的头(如Content-Type、Content-Length等),也支持自定义头。原本打算多列出几个常见的请求头的,但限于精力,不打算这样做了。我只说说我最常用的Content-Type头吧。
Content-Type头,既可用于请求消息,也可用于响应消息,是规定请求正文内容格式的头部。例如利用这个头部,我们可以规定正文的格式为纯文本格式、表单格式、XML格式、JSON格式、图像格式等。例如Content-Type: application/json
就表示JSON文本格式。
在小节的末尾,我良心地给出一个关于HTTP预定义头的参考网址:
HTTP响应
HTTP响应消息的基本格式也是一样的,包含三个部分:
-
响应行
-
响应头部
-
响应正文
响应头部和响应正文我觉得不需要再多说了。响应行的基本格式是:
版本号 状态码 状态文本
例如下面的响应行:
HTTP/1.1 200 OK
其对应关系为:
-
版本号:HTTP/1.1
-
状态码:200
-
状态文本:OK
HTTP状态码主要表示应答的状态。状态码是由3个数字表示,其中第一个数字表示一个大状态,后面两个数字表示该大状态的一个子状态。200就表示操作成功,还有其他常见的如404表示对象未找到,500表示服务器错误,403表示不能浏览目录等等。
状态码一共分为五个大状态,它们是:
-
1xx
-
2xx:请求成功处理
-
3xx
-
4xx:客户端出错
-
5xx:服务器出错
HTTP协议示例:
接下来的所有示例中,我们将代码都写成前面的一行一行的模式,但略去
GET /simple.html?bg=white HTTP/1.1
Accept: text/html
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0
Host: localhost:8080
Connection: Keep-Alive
GET请求没有请求正文,但可以包含query-string.
POST请求可以包含请求正文,例如下面带JSON格式正文的POST请求:
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
Content-Type: application/json
Content-Length: 38
{"name1": "value1", "name2": "value2"}
一个返回404错误的响应示例:
HTTP/1.1 404 Not Found
Date: Mon, 06 Mar 2006 09:03:14 GMT
Server: Apache/2.0.55 (Unix) PHP/5.0.5
Content-Length: 291
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /notexist was not found on this server.</p>
<hr>
<address>Apache/2.0.55 (Unix) PHP/5.0.5 Server at localhost Port 8080</address>
</body></html>