透视 HTTP 协议(一)

报文结构

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

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

其中前两部分经常被合称为 header,消息正文被称为 body

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个 空行,也就是 CRLF(十六进制 0x0D0A)。所以,一个完整的 HTTP 报文就是下图的这个样子:

请求行

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

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

这三个部分通常使用空格(space)分隔,最后用 CRLF 换行表示结束。

状态行

状态行(status line)描述了 服务器响应的状态,它由三部分构成:

  1. 版本号:表示报文使用的 HTTP 协议版本;
  2. 状态码:一个三位数,用代码的形式表示服务器处理的结果;
  3. 原因:作为数字状态码补充,是更详细的解释文字,帮助理解原因。

头部字段

请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的 请求头响应头,除了起始行不同,请求头和响应头的结构是基本一样的。

头部字段是 key-value 的形式,key 和 value 之间用 : 分隔,最后用 CRLF 换行表示字段结束。比如在 Host: 127.0.0.1 这一行里,key 就是 Host,value 就是 127.0.0.1。

使用头字段需要注意下面几点:

  1. 字段名 不区分大小写,例如 Host 也可以写成 host,但首字母大写的可读性更好;
  2. 字段名 不允许出现空格,可以使用连字符 -,但不能使用下划线 _
  3. 字段名后面必须紧接着 : 不能有空格,字段值前可以有多个空格;
  4. 字段的顺序不影响语义,可以任意排列;
  5. 字段原则上 不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

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

虽然 HTTP 协议对 header 的大小没有做限制,但各个 Web 服务器都不允许过大的请求头,因为头部太大可能会占用大量的服务器资源,影响运行效率。

常用头字段

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

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

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

通用字段

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

请求字段

  • Host:是 HTTP/1.1 规范里唯一要求 必须出现 的字段,用于告诉服务器这个请求应该由哪个主机来处理。当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,类似简单的“路由重定向”。
  • User-Agent:使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。
  • Referer:表示当前请求页面的来源页面的地址(注:referer 实际上是 referrer 的误拼写),服务端一般使用 Referer 请求头识别访问来源,可用于统计分析、日志记录以及缓存优化、防盗链等。与之相关的还有一个 Referrer Policy,用于控制 Referer 请求头的内容。

响应字段

  • Server:告诉客户端当前正在提供 Web 服务的软件名称和版本号,这个字段不是必须出现的。因为会暴露服务器的信息,有可能被黑客利用,所以有的网站响应头中要么没有这个字段,要么就给出一个完全无关的描述信息。
  • Location:表示该网页的跳转地址,一般在 3xx 的重定向响应中使用。
  • X-Powered-By:非标准字段,用于告知服务器端使用的编程语言。

请求方法

标准方法

目前 HTTP/1.1 规定了八种方法,方法名称必须是 大写形式

  1. GET:从服务器获取资源;
  2. HEAD:从服务器获取资源的元信息,即“响应头”,是轻量化的 GET;
  3. POST:向服务器提交数据,通常表示“新建”;
  4. PUT:向服务器提交数据,通常表示“修改”,功能类似 POST;
  5. DELETE:要求服务器删除资源;
  6. CONNECT:要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色;
  7. OPTIONS:要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回;它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持;
  8. TRACE:多用于对 HTTP 链路的测试或诊断,可以追踪请求 - 响应的传输路径;它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

扩展方法

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

例如在 WebDAV 中就对 HTTP/1.1 进行了扩展,添加了 MKCOL、COPY、MOVE、LOCK、UNLOCK、PATCH 等方法。如果有合适的场景,你也可以把它们应用到自己的系统里,比如用 LOCK 方法锁定资源暂时不允许修改,或者使用 PATCH 方法给资源打个小补丁,部分更新数据。但因为这些方法是非标准的,所以需要为客户端和服务器编写额外的代码才能支持。

状态码

状态码(Status Code) 在响应报文里表示了服务器对请求的处理结果。目前 RFC 标准里规定的状态码由三位十进制数字组成,并分为了五类,用数字的第一位表示类别,0~99 不用。因此,状态码的实际可用范围在 100~599

这五类状态码的具体含义是:

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

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

目前 RFC 标准里总共有 41 个 状态码,但状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。如果你自己开发 Web 应用,也完全可以在不冲突的前提下定义新的代码。

1××

  • 101 Switching Protocols:表示客户端使用 Connection: upgrade 字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket、HTTP/2,如果服务器也同意变更协议,就会发送状态码 101 切换协议。

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 个字节。

3××

  • 301 Moved Permanently:俗称 永久重定向,表示请求的资源已经不存在了,以后的请求都必须改用新的 URI。

  • 302 Found / Moved Temporarily:俗称 临时重定向,表示请求的资源还在,但是暂时不可用,需要用另一个 URI 来访问。

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

    比如,你的网站升级到了 HTTPS,原来的 HTTP 不打算用了,这就是 “永久” 的,所以要配置 301 跳转,把所有的 HTTP 流量都切换到 HTTPS,搜索引擎的爬虫看到 301 就会更新索引库,不再使用老的 URI;再比如,今晚网站后台要系统维护,服务暂时不可用,这就属于 “临时” 的,可以配置成 302 跳转,把流量临时切换到一个静态通知页面,浏览器看到这个 302 就知道这只是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。

  • 304 Not Modified:用于在 If-Modified-Since 等条件请求中,进行 缓存控制,表示资源未修改。它不具有通常的跳转含义,但可以理解为 “重定向到已缓存的文件”(即缓存重定向)。

  • 307 Temporary Redirect:与 302 状态码意义相同,唯一区别在于,当发送重定向请求时,307 状态码可以确保请求方法和消息主体不会发生变化。

4××

  • 400 Bad Request:通用错误码,表示客户端请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长都没有明确说,只是一个笼统的错误,客户端看到只会是一头雾水。在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。
  • 401 Authorization Required:未授权,当前请求需要用户验证。
  • 403 Forbidden:实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因。
  • 404 Not Found:原意是资源在请求的服务器上未找到,所以无法提供给客户端。但现在已经被用滥了,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因。
  • 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 太大;
  • 416 Requested Range Not Satisfiable:所请求的范围无法满足;
  • 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
  • 431 Request Header Fields Too Large:请求头某个字段或总体太大;

5××

  • 500 Internal Server Error:通用错误码,表示服务器内部发生错误,但究竟发生了什么错误我们不得而知。不过对于服务器来说这应该是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界,防止黑客的窥探或者分析。

  • 501 Not Implemented:表示客户端请求的功能还不支持,这个错误码比 500 要更温和一些。

  • 502 Bad Gateway:通常是服务器 作为网关或者代理 时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

  • 503 Service Unavailable:表示服务器当前繁忙,暂时无法响应服务。

    503 是一个 临时 的状态,很可能过几秒后服务器就可以继续提供服务了,所以 503 响应头里通常还会有一个 Retry-After 字段,指示客户端可以在多久之后再次尝试发送请求。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!