02-HTTP详解

HTTP 协议基本工作流程,也就是“请求——应答”,“一发一收”的模式。

HTTP 的工作模式是由于 TCP/IP 协议负责底层的具体传输工作,HTTP 协议基本上不用操心。单从这一点上来看,所谓的“超文本传输协议”其实并不怎么管“传输”的事情,有点“名不副实”。

HTTP 协议的核心部分是它传输的报文内容

HTTP 协议在规范文档里详细定义了报文的格式,规定了组成部分,解析规则,还有处理策略,所以可以在 TCP/IP 层之上实现更灵活丰富的功能,例如:

  • 连接控制
  • 缓存管理
  • 数据编码
  • 内容协商等

0.1. 报文结构

TCP 报文在实际要传输的数据之前附加了一个 20 字节的头部数据,存储 TCP 协议必须的额外信息,例如:

  • 发送方的端口号
  • 接收方的端口号
  • 包序号
  • 标志位等

有了这个附加的 TCP 头,数据包才能够正确传输,到了目的地后把头部去掉,就可以拿到真正的数据。

image

  • HTTP 协议也是与 TCP/UDP 类似,同样也需要在实际传输的数据前附加一些头数据,
  • 不过与 TCP/UDP 不同的是,它是一个“纯文本”的协议,所以头数据都是 ASCII 码的文本,可以很容易地用肉眼阅读,不用借助程序解析也能够看懂。

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  • 起始行(start line):描述请求或响应的基本信息; 41 个状态码
  • 头部字段集合(header):使用 key-value 形式更详细地说明报文;
  • 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。

下面是wireshark抓取的HTTP请求的报文:

GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

在这个浏览器发出的请求报文里:

  • 第一行“GET / HTTP/1.1”就是请求行
  • 而后面的“Host”“Connection”等等都属于 header
  • 报文的最后是一个空白行结束,没有 body

在很多时候,特别是浏览器发送 GET 请求的时候都是这样,HTTP 报文经常是只有 header 而没 body。虽然 HTTP 协议对 header 的大小没有做限制,但各个 Web 服务器都不允许过大的请求头,因为头部太大可能会占用大量的服务器资源,影响运行效率

0.1.1. 请求行

请求报文里的起始行也就是请求行(request line),它简要地描述了客户端想要如何操作服务器端的资源。请求行由三部分构成:

  • 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
  • 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
  • 版本号:表示报文使用的 HTTP 协议版本。

这三个部分通常使用空格(space)来分隔,也可以是制表符(tab),最后要用 CRLF 换行表示结束。

GET / HTTP/1.1

在这个请求行里,“GET”是请求方法,“/”是请求目标,“HTTP/1.1”是版本号,把这三部分连起来,意思就是“服务器你好,我想获取网站根目录下的默认文件,我用的协议版本号是 1.1,请不要用 1.0 或者 2.0 回复我。”

0.1.2. 状态行

响应报文里的起始行,叫“状态行”(status line),意思是服务器响应的状态

状态行要简单一些,同样也是由三部分构成:

  • 版本号:表示报文使用的 HTTP 协议版本;
  • 状态码:一个三位数,用代码的形式表示处理的结果;
  • 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。
HTTP/1.1 200 OK

0.1.3. 头部字段

请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头,如下两个示意图。

request

response

请求头和响应头的结构是基本一样的,唯一的区别是起始行。

头部字段是 key-value 的形式,key 和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。

比如在“Host: 127.0.0.1”这一行里 key 就是“Host”,value 就是“127.0.0.1”。

HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以任意添加自定义头,这就给 HTTP 协议带来了无限的扩展可能。不过使用头字段需要注意下面几点:

  • 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;
  • 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;
  • 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
  • 字段的顺序是没有意义的,可以任意排列不影响语义;
  • 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

0.1.4. 常用头字段

详细的常用头字段介绍看这里

HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

  • 通用字段:在请求头和响应头里都可以出现;
  • 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
  • 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
  • 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。

对 HTTP 报文的解析和处理实际上主要就是对头字段的处理,理解了头字段也就理解了 HTTP 报文。

0.1.4.1. Host 字段

属于请求字段,只能出现在请求头里,同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,如果请求头里没有 Host,那这就是一个错误的报文。

Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”。

0.1.4.2. User-Agent 字段

属于请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。

由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致 User-Agent 变得越来越长,最终变得毫无意义。

有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。

0.1.4.3. Date 字段

属于通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。

0.1.4.4. Server 字段

属于响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号。

Server 字段不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在 bug,那么黑客就有可能利用 bug 攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。

0.1.4.5. Content-Length 字段

属于实体字段,表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度。

  • 服务器看到这个字段,就知道了后续有多少数据,可以直接接收。
  • 如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。

0.2. 请求方法

HTTP 协议设计时的定位是要用 HTTP 协议构建一个超链接文档系统,使用 URI 来定位这些文档,也就是资源。那么,该怎么在协议里操作这些资源呢?很显然,需要有某种“动作的指示”,告诉操作这些资源的方式。所以,就这么出现了“请求方法”。

请求方法的实际含义就是客户端发出了一个“动作指令”,要求服务器端对 URI 定位的资源执行这个动作。目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形式:

  • GET:获取资源,可以理解为读取或者下载数据;
  • HEAD:获取资源的元信息;
  • POST:向资源提交数据,相当于写入或上传数据;
  • PUT:类似 POST;
  • DELETE:删除资源;
  • CONNECT:建立特殊的连接隧道;
  • OPTIONS:列出可对资源实行的方法;
  • TRACE:追踪请求和响应的传输路径。

method

这些方法,就像是对文件或数据库的“增删改查”操作,只不过这些动作操作的目标不是本地资源,而是远程服务器上的资源,所以只能由客户端“请求”或者“指示”服务器来完成。

请求方法是一个“指示”,客户端没有决定权,服务器掌控着所有资源,它收到 HTTP 请求报文后,看到请求方法,可以执行也可以拒绝,或者改变动作的含义,毕竟 HTTP 是一个“协议”,两边都要“商量着来”。

比如,发起了一个 GET 请求,想获取“/orders”保密级别比较高的文件,服务器就可以有如下的几种响应方式:

  1. 假装文件不存在,返回 404 Not found
  2. 有这个文件但不允许访问,返回 403 Forbidden
  3. 返回 405 Method Not Allowed,然后用 Allow 头告诉你可以用 HEAD 方法获取文件的元信息。

0.2.1. GET/HEAD

GET 方法是 HTTP 协议里最知名,用的最多的请求方法,自 0.9 版出现并一直被保留至今。它的含义是请求从服务器获取资源,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据。

GET 方法虽然基本动作比较简单,但搭配 URI 和其他头字段就能实现对资源更精细的操作。例如:

  • 在 URI 后使用“#”,就可以在获取页面后直接定位到某个标签所在的位置
  • 使用 If-Modified-Since 字段就变成了“有条件的请求”,仅当资源被修改时才会执行获取动作
  • 使用 Range 字段就是“范围请求”,只获取资源的一部分数据

HEAD 方法也是请求从服务器获取资源,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”。

HEAD 方法可以看做是 GET 方法的一个“简化版”或者“轻量版”。

HEAD 的响应头与 GET 完全相同,所以可以用在很多并不真正需要资源的场合,避免传输 body 数据的浪费。例如:

  • 检查一个文件是否存在,只要发个 HEAD 请求,没有必要用 GET 把整个文件都取下来
  • 检查文件是否有最新版本,用 HEAD 请求,服务器会在响应头里把文件的修改时间传回来

0.2.2. POST/PUT

GET 和 HEAD 方法是从服务器获取数据,而 POST 和 PUT 方法则是向 URI 指定的资源提交数据,数据就放在报文的 body 里。

POST 是一个常用到的请求方法,使用频率仅次于 GET,应用的场景非常多,只要向服务器发送数据,用的大多数都是 POST。例如:

  • 在论坛输入文字后点击“发帖”按钮,浏览器就执行一次 POST 请求,把文字放进报文的 body 里,然后拼好 POST 请求头,通过 TCP 协议发给服务器。
  • 在购物网站点击商品“加入购物车”,浏览器就执行一次 POST 请求,把商品 ID 发给服务器,服务器再把 ID 写入你的购物车相关的数据库记录。

PUT 方法也可以向服务器提交数据,但与 POST 存在微妙的不同,通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修改”“update”的含义。

在实际应用中,PUT 用到的比较少。而且,因为它与 POST 的语义、功能太过近似,有的服务器甚至就直接禁止使用 PUT 方法,只用 POST 方法上传数据。

0.2.3. 非常用方法

  • DELETE 方法指示服务器删除资源,这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。
  • CONNECT 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。
  • OPTIONS 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。
  • TRACE 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求——响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

0.2.4. 扩展方法

虽然 HTTP/1.1 里规定了八种请求方法,但它并没有限制只能用这八种方法,这也体现了 HTTP 协议良好的扩展性,我们可以任意添加请求动作,只要请求方和响应方都能理解就行。

例如著名的愚人节玩笑 RFC2324,它定义了协议 HTCPCP,即“超文本咖啡壶控制协议”,为 HTTP 协议增加了用来煮咖啡的 BREW 方法,要求添牛奶的 WHEN 方法。

还有一些得到了实际应用的请求方法(WebDAV),例如 MKCOLCOPYMOVELOCKUNLOCKPATCH 等。

如果有合适的场景,可以把它们应用到系统里,例如:

  • 用 LOCK 方法锁定资源暂时不允许修改,
  • 用 PATCH 方法给资源打个小补丁,部分更新数据。

这些方法是非标准的,所以需要为客户端和服务器编写额外的代码才能添加支持

也完全可以根据实际需求发明新的方法,例如:

  • “PULL”拉取某些资源到本地,
  • “PURGE”清理某个目录下的所有缓存数据。

0.2.5. 幂等与安全

关于请求方法还有两个比较重要的概念:安全与幂等。

在 HTTP 协议里,所谓的“安全”是指请求方法不会“破坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。

  • 按照这个定义,只有 GET 和 HEAD 方法是“安全”的,因为它们是“只读”操作,只要服务器不故意曲解请求方法的处理方式,无论 GET 和 HEAD 操作多少次,服务器上的数据都是“安全的”。
  • 而 POST/PUT/DELETE 操作会修改服务器上的资源,增加或删除数据,所以是“不安全”的。

所谓的“幂等”意思是多次执行相同的操作,结果也都是相同的,即多次“幂”后结果“相等”。

  • GET 和 HEAD 既是安全的也是幂等的,
  • DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。
  • POST 是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的;
  • PUT 是“替换或更新数据”,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。

把 POST 理解成 INSERT,把 PUT 理解成 UPDATE。多次 INSERT 会添加多条记录,而多次 UPDATE 只操作一条记录,而且效果相同。

0.3. URI

严格地说,URI 不完全等同于网址,它包含有 URL 和 URN 两个部分,在 HTTP 世界里用的网址实际上是 URL,即统一资源定位符(Uniform Resource Locator)。但因为 URL 实在是太普及了,所以常常把这两者简单地视为相等。

URL有绝对URL和相对URL之分,多用在HTML页面标记应用的其他资源,而在HTTP请求行里则不会出现。

URI 本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字

注意:它不仅能够标记万维网的资源,也可以标记其他的,如邮件系统、本地文件系统等任意资源。

“资源”既可以是存在磁盘上的静态文本、页面数据,也可以是由 Java、PHP 提供的动态服务。

下面的这张图显示了 URI 最常用的形式,由 schemehost:portpathquery 四个部分组成,有的部分可以视情况省略。

URI

0.3.1. URI基本组成

URI 第一部分 scheme(“方案名”或者“协议名”),表示资源应该使用哪种协议来访问。

  • 最常见的是“http”,表示使用 HTTP 协议。
  • 还有“https”,表示使用经过加密、安全的 HTTPS 协议。
  • 还有非常见的 scheme,例如 ftp、ldap、file、news 等。

浏览器或应用程序看到 URI 里的 scheme,下一步就会调用相应的 HTTP 或者 HTTPS 下层 API。如果一个 URI 没有提供 scheme,即使后面的地址再完善,也是无法处理的。

在 scheme 之后,必须是三个特定的字符“://”,它把 scheme 和后面的部分分离开。在“://”之后,是被称为“authority”的部分,表示资源所在的主机名,通常的形式是“host:port”,即主机名加端口号。

  • 主机名可以是 IP 地址或者域名的形式,必须要有,否则浏览器就会找不到服务器。
  • 端口号有时可以省略,浏览器等客户端会依据 scheme 使用默认的端口号,例如 HTTP 的默认端口号是 80,HTTPS 的默认端口号是 443。

有了协议名和主机地址、端口号,再加上后面标记资源所在位置的 path,浏览器就可以连接服务器访问资源了。

URI 里 path 采用了类似文件系统“目录”“路径”的表示方式,因为早期互联网上的计算机多是 UNIX 系统,所以采用了 UNIX 的“/”风格,path 部分必须以“/”开始,也就是必须包含“/”。

http://nginx.org    #省略端口号,http默认为80,省略路径,默认为根目录/
http://www.chrono.com:8080/11-1
https://tools.ietf.org/html/rfc7230     #省略端口号,https默认为443
file:///D:/http_study/www/      # file协议,表示本地文件,省略authority部分,默认为localhost,后面直接path部分

0.3.1.1. 注意

客户端和服务器看到的 URI 是不一样的。

  • 客户端看到的必须是完整的 URI,使用特定的协议去连接特定的主机
  • 服务器看到的只是报文请求行里被删除了协议名和主机名的 URI

如 Nginx 作为一个 Web 服务器,它的 location、rewrite 等指令操作的 URI 其实指的是真正 URI 里的 path 和后续的部分。

0.3.2. URI查询参数

使用“协议名 + 主机名 + 路径”的方式,可以精确定位网络上的任何资源,但这还不够,很多时候还想在操作资源的时候附加一些额外的修饰参数,例如:

  • 获取商品图片,但想要一个 32×32 的缩略图版本;
  • 获取商品列表,但要按某种规则做分页和排序;
  • 跳转页面,但想要标记跳转前的原始页面。

仅用“协议名 + 主机名 + 路径”的方式是无法适应这些场景的,所以 URI 后面还有一个“query”部分,它在 path 之后,用一个“?”开始,但不包含“?”,表示对资源附加的额外要求。这是个很形象的符号,比“://”要好的多,很明显地表示了“查询”的含义。

查询参数 query 的格式是多个“key=value”的字符串,这些 KV 值用字符“&”连接,浏览器和服务器都可以按照这个格式把长串的查询参数解析成可理解的字典或关联数组形式。

查询参数 query 也可以不适用 “key=value”的形式,只是单纯的“key”,这样“value”就是空字符串。

如果查询参数 query 太长,也可以使用GET方法,放在body里发送给服务器。

0.3.3. URI完整格式

URI “真正”的完整形态,如下图所示。

URI

这个“真正”形态比基本形态多了两部分。

  • 第一个多出的部分是协议名之后、主机名之前的身份信息“user:passwd@”,表示登录主机时的用户名和密码,但现在已经不推荐使用这种形式了(RFC7230),因为它把敏感信息以明文形式暴露出来,存在严重的安全隐患
  • 第二个多出的部分是查询参数后的片段标识符“#fragment”,它是 URI 所定位的资源内部的一个“锚点”或者说是“标签”,浏览器可以在获取资源后直接跳转到它指示的位置。

片段标识符仅能由浏览器这样的客户端使用,服务器是看不到的。浏览器永远不会把带“#fragment”的 URI 发送给服务器,服务器也永远不会用这种方式去处理资源的片段。

0.3.4. URI编码

  1. 在 URI 里只能使用 ASCII 码,如何在 URI 里使用英语以外的其他语言?
  2. 某些特殊的 URI,会在 path、query 里出现“@&?"等起界定符作用的字符,会导致 URI 解析错误,这时又该怎么办?

URI 引入了编码机制,对于 ASCII 码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式。这在 RFC 规范里称为“escape”和“unescape”,俗称“转义”。

URI 转义的规则“简单粗暴”,直接把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一个“%。例如:

  • 空格被转义成“%20
  • “?”被转义成“%3F
  • 中文、日文等则通常使用 UTF-8 编码后再转义,如“银河”会被转义成“%E9%93%B6%E6%B2%B3

有了编码规则 URI 就可以支持任意的字符集用任何语言来标记资源。在浏览器的地址栏里通常是不会看到这些转义后的“乱码”的,这是浏览器一种“友好”表现,隐藏了 URI 编码后的“丑陋一面”。

注意,URI编码转义与HTML里的编码转义是完全不同的,URI的转义使用的是“%”,而HTML转义使用的是“&#”。

0.4. 状态码

响应报文由响应头加响应体数据组成,响应头又由状态行和头字段构成。状态行的结构,有三部分:

  • 开头的 Version 部分是 HTTP 协议的版本号,通常是 HTTP/1.1,用处不是很大。
  • 后面的 Reason 部分是原因短语,是状态码的简短文字描述,例如“OK”“Not Found”等等,也可以自定义。但它只是为了兼容早期的文本客户端而存在,提供的信息很有限,目前的大多数客户端都会忽略它。
  • 状态行里有用的就只剩下中间的状态码(Status Code)。它是一个十进制数字,以代码的形式表示服务器对请求的处理结果,就像编写程序时函数返回的错误码一样。

它的名字是“状态码”不是“错误码”,它的含义不仅是错误,更重要的意义在于表达 HTTP 数据处理的“状态”,客户端可以依据代码适时转换处理状态,例如:

  • 继续发送请求、
  • 切换协议,
  • 重定向跳转等

目前 RFC 标准里规定的状态码是三位数,所以取值范围就是从 000 到 999。如果把状态码简单地从 000 开始顺序编下去就显得有点太“low”,不灵活、不利于扩展,所以状态码也被设计成有一定的格式。

RFC 标准把状态码分成了五类,用数字的第一位表示分类,而 0~99 不用,这样状态码的实际可用范围就大大缩小了,由 000~999 变成了 100~599。这五类的具体含义是:

  • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
  • 2××:成功,报文已经收到并被正确处理;
  • 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
  • 4××:客户端错误,请求报文有误,服务器无法处理;
  • 5××:服务器错误,服务器在处理请求时内部发生了错误。

在 HTTP 协议中,正确地理解并应用这些状态码不是客户端或服务器单方的责任,而是双方共同的责任

  • 客户端作为请求的发起方,获取响应报文后,需要通过状态码知道请求是否被正确处理,是否要再次发送请求,如果出错了原因又是什么。这样才能进行下一步的动作,要么发送新请求,要么改正错误重发请求。
  • 服务器端作为请求的接收方,应该很好地运用状态码。在处理请求时,选择最恰当的状态码回复客户端,告知客户端处理的结果,指示客户端下一步应该如何行动。特别是在出错的时候,尽量不要简单地返 400、500 这样意思含糊不清的状态码。

目前 RFC 标准里总共有 41 个状态码,但状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。

如果开发 Web 应用,也完全可以在不冲突的前提下定义新的代码。

0.4.1. 1.××

1××类状态码属于提示信息,是协议处理的中间状态,实际能够用到的时候很少。

  • 101 Switching Protocols”:意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了。

0.4.2. 2.××

2××类状态码表示服务器收到并成功处理了客户端的请求,这也是客户端最愿意看到的状态码。

  • 200 OK”:是最常见的成功状态码,表示一切正常,服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD 请求,通常在响应头后都会有 body 数据。
  • 204 No Content”:是很常见的成功状态码,它的含义与“200 OK”基本相同,但响应头后没有 body 数据。所以对于 Web 服务器来说,正确地区分 200 和 204 是很必要的。
  • 206 Partial Content”:是 HTTP 分块下载断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。

状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。

0.4.3. 3.××

3××类状态码表示客户端请求的资源发生了变动,客户端必须用新的 URI 重新发送请求获取资源,也就是通常所说的“重定向”,包括著名的 301、302 跳转。

  • 301 Moved Permanently”:俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用新的 URI 再次访问。
  • 302 Found”:曾经的描述短语是“Moved Temporarily”,俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。
  • 304 Not Modified”:是一个比较有意思的状态码,它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。

301、302 和 304 分别涉及了 HTTP 协议里重要的“重定向跳转”和“缓存控制”。

301 和 302 都会在响应头里使用字段 Location 指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的 URI。两者的根本区别在于语义,一个是“永久”,一个是“临时”,所以在场景、用法上差距很大。比如:

  • 网站升级到了 HTTPS,原来的 HTTP 不打算用了,这就是“永久”的,所以要配置 301 跳转,把所有的 HTTP 流量都切换到 HTTPS。
  • 夜里网站后台要系统维护,服务暂时不可用,这就属于“临时”的,可以配置成 302 跳转,把流量临时切换到一个静态通知页面,浏览器看到这个 302 就知道这只是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。

301和302还有另外两个等价的状态码“308 Permanent Redirect”和“307 Temporary Redirect”,但是这两个状态码不允许后续的请求更改请求方法。

0.4.4. 4.××

4××类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的“错误码”含义了。

  • 400 Bad Request”:是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。
  • 403 Forbidden”:实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。
  • 404 Not Found”:可能是最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比 403 还要令人讨厌。
  • 405 Method Not Allowed”:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
  • 406 Not Acceptable”:资源无法满足客户端请求的条件,例如请求中文但只有英文;
  • 408 Request Timeout”:请求超时,服务器等待了过长的时间;
  • 409 Conflict”:多个请求发生了冲突,可以理解为多线程并发时的竞态;
  • 413 Request Entity Too Large”:请求报文里的 body 太大;
  • 414 Request-URI Too Long”:请求行里的 URI 太大;
  • 429 Too Many Requests”:客户端发送了太多的请求,通常是由于服务器的限连策略;
  • 431 Request Header Fields Too Large”:请求头某个字段或总体太大;

0.4.5. 5.××

5××类状态码表示客户端请求报文正确,但服务器在处理时内部发生了错误,无法返回应有的响应数据,是服务器端的“错误码”。

  • 500 Internal Server Error”:与 400 类似,是一个通用的错误码,服务器究竟发生了什么错误是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
  • 501 Not Implemented”:表示客户端请求的功能还不支持,这个错误码比 500 要“温和”一些,和“即将开业,敬请期待”的意思差不多,不过具体什么时候“开业”就不好说了。
  • 502 Bad Gateway”:通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。
  • 503 Service Unavailable”:表示服务器当前很忙,暂时无法响应服务,上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。
上次修改: 14 April 2020