【网络】计算机网络笔记——HTTP 协议笔记

HTTP 协议是一种基于 TCP 协议的应用层协议,它的特点如下:

  • 用于客户端与服务端之间的通信,例如我们的 Android 应用访问接口,我们访问网页请求资源等。
  • 通过请求 Request 和响应 Respoinse 完成通信,由客户端发出 Request,服务端收到后进行处理并根据客户端的需要返回对应的 Response
  • 是一种无连接,无状态的协议,HTTP 协议的请求随着一次 Request 发起,随着 Response 的返回结束。无连接意味着并不与对方建立 『HTTP 连接』(注意不是指 TCP 连接,仍然需要 TCP 连接,只是 HTTP 层面不需要建立连接),也不会对之前的通信状态进行保存。
  • 可以通过如 http://blog.n0texpecterr0r.cn/index.php 这样的 URI 定位网络上的资源。

报文结构

HTTP 的报文主要分为两类,请求报文以及响应报文。它们的基本结构都比较相似,只是细节有部分不同。它们都由起始行、请求头、数据三部分组成,对于起始行,对于请求报文叫做『请求行』(Request Line),而对于响应报文则叫做『状态行』(Status Line)。

请求报文

请求报文的格式如下:

首先,请求行基本表示了这个 HTTP 请求的基本信息:请求方法URL协议的版本

之后,在请求头部分,不同的 HTTP 版本下有不同的请求头,它们都是以键值对的方式放在 Header 中的,请求的调用方可以根据自己的需求配置对应的 Header,从而实现不同的需求。

而在请求数据部分,则包含了这次发起请求所要携带的数据。

并且可以发现,每一行的结尾都会以一个回车+换行 \r\n 结尾,在数据与 Header 区域之间,也会用一个 \r\n 来进行区分。

响应报文

可以发现,响应报文与请求报文除了状态行,其余部分基本一致,它的状态行由 协议版本状态码信息 三部分构成。

之后,仍然是以键值对方式存储的 Header 部分,以及响应的数据部分。

请求方法

HTTP 协议中提供了一系列的请求方法,用于向服务器告知自己的意图:

  • GET:获取资源。可以通过它访问被 URI 识别的资源。
  • POST:传输数据。虽然说 GET 也可以用来传递数据,但一般来说不采用 GET 进行数据传输,而是使用 POST。虽然 POST 可以用来获取响应的数据,但它的目的实际上不是获取响应数据。
  • PUT:传输文件。一般用于文件上传等待,但由于 PUT 并没有验证机制,因此任何人都可以上传文件,这导致大部分的网站都不会用它来实现文件的上传。
  • HEAD:获取报文首部。它与 GET 一样用于获取资源,只是不会请求整个报文,而是只请求报文的首部,不请求报文的主体部分。(之前写的多线程下载库就是采用 HEAD 来请求首部获取下载资源的 Content-Length 字段)
  • DELETE:删除文件。对指定的文件进行删除,与 PUT 相反。但同样由于它也没有验证机制,因此大部分网站都不会通过它来实现删除文件。
  • OPTIONS:询问支持的方法。可以用来查询对指定资源所支持的方法。
  • TRACE:追踪路径。用于让 Web 服务器将请求的通信还回馈给客户端。发送请求的时候,在 Max-Forwards 中填入一个值,每当经过一个服务端,这个值减 1,当到达 0 时,就停止传输,此时收到的服务器返回响应给客户端。(有点 TraceRoute 的感觉)
  • CONNECT:用隧道协议连接代理。主要是使用 SSL 和 TLS 协议把通信内容加密后通过隧道进行传输。

GET 与 POST 的区别

网上的博文总是会提到 GETPOST 请求的区别,其实从本质上来说,它们只是请求方式的不同,HTTP 协议并没有对它们的如 URL、参数位置之类的规则进行规定,这实际上都是大家使用的时候慢慢产生的规范。(就像 TCP 粘包问题一样存在争议)

不过由于这种规范我们已经应用已久,因此从这些规定的角度来说,GETPOST 的区别主要体现在以下方面:

GET 的参数往往是被放在 URL 中,因此是以明文的方式直接传递的,并且由于 URL 有大小的限制(2048 字节),因此它携带的参数长度会被限制。

POST 请求的参数往往是放在请求体中,它不会受到 URL 的大小限制,也不会直接暴露在浏览器的 URL 栏中。(显然 HTTP 请求报文中并没有参数的概念,因此这些实际上还是我们使用的时候所习惯的规范)

状态码

HTTP 状态码可以用于告知客户端 HTTP 请求的过程中服务器端的处理是否正常,出现了哪些错误。一般可以将状态码按最高位分为下表中的五种:

状态码 类别 描述
1XX 信息性状态码 接收的请求正在处理
2XX 成功状态码 请求正常处理完毕
3XX 重定向状态码 需要进行附加操作以完成请求
4XX 客户端错误状态码 服务器无法处理请求
5XX 服务端错误状态码 服务器处理请求出错

2XX 成功

2XX 表示请求成功被处理。常见的如下:

  • 200 OK:表示客户端发来的请求服务端正常处理了。
  • 204 No Content:表示服务器接收的请求已处理,但响应中不包含主体部分。一般在只需要从客户端往服务器发送信息,而对客户端不需要发送新信息的情况下使用。
  • 206 Partial Content:表示客户端进行了范围请求,服务器成功进行了执行,响应中包含了 Content-Length 指定的实体内容。

3XX 重定向

表明客户端需要执行一些特殊处理以正确处理请求,常常配合 Location 这个 Header 使用,常见的有:

  • 301 Moved Permantenly:表示永久性重定向,也就是这个资源被分配了新的 URI,以后需要使用这个资源所指的 URI,此时需要通过 Location 中拿到的 URI 重新保存。
  • 302 Found:表示临时性重定向,希望用户在本次中使用 Location 指定的 URI 使用。
  • 303 See Other:表示请求的资源存在另一个 URI,应该使用 GET 方法获取 Location 位置指定的资源。与 302 类似但明确表示了需要用 GET 进行访问。
  • 304 Not Modified:表示资源没有被修改(与重定向无关)。需要配合 If-MatchIf-ModifiedSinceIf-None-MatchIf-RangeIf-Unmodified-Since 等 Header 使用。
  • 307 Temporary Redirect:临时重定向,与 302 含义相同,需要通过 Location 获取新的 URI。

4XX 客户端错误

表示发生错误,客户端是发生错误的原因,主要有:

  • 400 Bad Request:说明请求报文存在语法错误,需修改请求的内容后再次发送。
  • 401 Unauthorized:表示发送的请求需要有通过 HTTP 认证的信息,响应的 Header 中会包含一个 WWW-Authenticate Header 用来质询用户信息。(例如浏览器有时候弹出的认证对话框)
  • 403 Forbidden:表示服务器拒绝了客户端对该资源的访问
  • 404 Not Found :我们最常见的一个 4XX 了,表示服务器上找不到对应的资源。

5XX 服务器错误

  • 500 Internal Server Error:表示服务端执行请求时出现了错误。
  • 503 Service Unavailable:表示服务器暂时处于超负载或维护状态,可能会包含 RetryAfter 这个 Header 来告诉客户端何时能够进行访问。

TCP 连接管理

虽然说 HTTP 是一个无连接、无状态的协议,但由于它基于 TCP,因此也存在着管理 TCP 连接的管理问题。

HTTP/1.0 及之前

由于前面的 TCP 协议的文章所提到的粘包问题:由于 TCP 连接的数据之间没有数据边界,从而导致 HTTP 数据包粘连,无法处理,因此 HTTP/1.0 及之前,采用了一种非常暴力的方式进行解决:

每进行一次 HTTP 通信,就要先建立一条 TCP 连接,在通信结束后,就需要断开一次 TCP 连接,这显然非常浪费资源。

其实 HTTP/1.0 中还引入了 Connection:Keep-Alive 这个 Header,它就是用来支持 TCP 连接的维持的,但在 大部分基于 HTTP/1.0 的服务器中并没有实现。

HTTP/1.1

而在 HTTP/1.1 时,则真正引入了 Keep-Alive 机制。它默认开启,可以通过 Connection:close 进行关闭。在 HTTP 通信结束时,若启动了 Keep-Alive 机制,则该连接并不会立即关闭,此时如果有新的请求到来,且 HOST 相同,则会复用这条 TCP 连接进行请求,减少了 TCP 连接的频繁建立与关闭的资源消耗。

可以看到,这样就减少了每个 HTTP 通信需要进行的连接建立和连接关闭的过程(三次握手和四次挥手),从而使得 HTTP 请求的效率大大加快:

粘包问题

那么现在既然复用了同一条 TCP 连接,而 HTTP 数据包也没有通过分隔符确定头和尾,它是如何解决粘包的问题的呢?

实际上它是通过 Content-Length 这个 Header 解决的,它标明了数据部分所占用的大小,从而可以通过它来确定这个数据包的边界,避免粘包。这也是 HTTP/1.1 引入 Content-Length 的原因。

同时,HTTP/1.1 中还有一个 Keep-Alive 请求头,可以对 timeoutmax 进行设置,用来指定空闲连接保持打开的时间以及连接关闭前这条连接可以发送请求数的最大值。

分块编码

但在某些动态的场景,发送方是无法预知数据的大小的,因此无法返回一个确切的 Content-Length Header,此时该怎么处理呢?

此时可以采用 Transfer-Encoding:chunked 这个 Header,它是一种分块编码的概念,只在 HTTP/1.1 中提供,允许服务端将数据分为多个部分。

如果使用了分块编码,则请求及响应有以下的特点:

  1. 在 Header 中加入 Transfer-Encoding:chunked,表示使用分块编码。
  2. 每一个分块有两行,每一行都以 \r\n 结尾,第一行表示这个分块的数据长度,是一个十六进制的数(不包括数据结尾的 \r\n,第二行则是这个分块的具体数据。
  3. 最后一个分块长度为0,且数据没有内容,表示整个数据的结束。

例如:

25
This is the data in the first chunk

1C
and this is the second one

3
con

8
sequence

0

对应了

This is the data in the first chunk
and this is the second one
consequence

HTTP/2.0

HTTP1.1 的问题

HTTP/1.1 中,虽然实现了 TCP 连接的复用,但仍有如下几个缺陷:

  1. 如果客户端想要发起并行的请求,则必须建立多个 TCP 连接,这对网络资源的消耗也是十分严重的。
  2. 不会读对请求及响应的 Header 进行压缩,造成了网络流量的浪费。
  3. 不支持资源优先级导致 TCP 连接利用率低下。

举个例子,如果我们打开一个网页,必须等待图片一张一张的加载,直到加载结束,如果要全部一起并行加载,则必须要使用多个不同的 TCP 连接。

多路复用

HTTP/2.0 引入了一种多路复用机制,引入了如下的概念:

  • 数据流:基于 TCP 连接上的一个双向的字节流,每发起一个请求,就会建立一个数据流,后续的请求过程的数据传递都通过该流进行
  • 数据帧:HTTP/2 中的数据最小切片单位,其中又分为了 Header FrameData Frame 等等。
  • 消息:一个请求或响应对应的一系列数据帧。

引入了这些概念之后,在 HTTP 请求的过程中,服务端/客户端首先会将我们的请求/响应切分为不同的数据帧,当另一方接收到后再将其组装从而形成完整的请求/响应:

通过这样将一个请求/响应分为数据帧的方式,可以实现对 TCP 连接的多路复用,让多个请求可以同时进行,大大提高了数据传输的效率:

Cookie 状态管理

由于 HTTP 协议本身并不维护状态信息,但有些业务场景下我们仍然需要之前的状态,例如登陆等场景,我们希望的往往是只需要一次登陆,之后便不再需要重新登陆即可获取数据。

此时可以通过 Cookie 实现状态管理,服务端会通过在响应报文中携带一个 Set-Cookie Header,它的值就是服务端交给客户端的一个 Token,客户端对这个 Token 进行保存后,在下一次请求时在请求头将这个 Token 放入 Cookie 这个 Header,即可实现维持上次的状态。

Range 部分请求

HTTP 协议还对指定资源的某个部分进行请求进行了支持,通过 Range 这个 Header,可以指定从 start-end 的一段,这样服务器收到请求后,就只会返回这个资源的对应范围的数据,而不会全部返回。

例如通过:Range: bytes=5000-10000 可以指定第 5000 个字节到第 10000 个字节的全部数据。

数据转发

代理

HTTP 代理实际上是通过代理服务器实现的。代理服务器通过接收客户端发送的请求,之后转发给其他服务器,它不会改变请求的 URI,直接将数据发送给前方持有资源的目标服务器。持有资源的服务器返回的响应,会经过代理服务器的转发返还给客户端。

在 HTTP 通信的过程中,可能会经过多台代理服务器,代理服务器转发时,需要在 Via 这个 Header 中标记经过的主机的信息。

(这就像外卖一样,我们在家没办法直接买到食物,就通过将钱交给外卖小哥,由外卖小哥将钱交给店家。店家收到钱后将食物交给外卖小哥,外卖小哥再把这个食物转发给我们。)

代理注意基于使用方法有两种分类:是否使用缓存和是否对报文进行修改。

缓存代理

缓存代理会预先将资源的副本存在代理服务器,再次接到同样的资源的请求时,就会用自己缓存的副本而不是源服务器的资源进行响应,从而提高请求的效率。

透明代理

在转发请求和响应时不对报文进行任何加工的代理称为透明代理。否则为非透明代理

网关

网关的主要作用是使得通信线路上的服务器提供非 HTTP 协议的服务,它在收到 HTTP 请求后用非 HTTP 协议的服务与非 HTTP 服务器进行通信,将结果转换为 HTTP 响应后返回给客户端。通过网关可以提高通信的安全性。

(前面网络层的文章中提到的 IPv6 切换的策略中的『双栈』策略的双栈节点感觉就有点像一种双向的网关)

隧道

隧道在前面网络层 IPv6 切换策略中提到过,提供隧道可以通过 HTTP 协议对其他协议的请求进行传输,例如可以通过 SSL 隧道等加密手段实现安全的通信。它本身不会去解析 HTTP 请求,会将请求以原样的形式中转给服务器。

资源缓存

缓存主要指代理服务器或客户端的磁盘中保存的资源副本,通过缓存可以减少向源服务器的访问,从而提高效率。

Expires

Expires 是 HTTP/1.0 所提供的对缓存的支持,通过这个 Header ,服务端可以告诉客户端缓存的过期时间,表示在过期时间内该资源都不会被更改,可以不用再向自己请求了。

例如:Expires: Wed, 21 Oct 2017 07:28:00 GMT 就标明了缓存的过期时间。

可以发现,它记录的是一个具体的时间,浏览器之类的客户端应用会根据本地的时间与该具体时间对比。那么如果我们对本地的时间进行了修改,则 Expires 的功能显然会受到影响。

Cache-Control

由于 Expires 存在上述的问题,因此在 HTTP/1.1 协议中引入了 Cache-Control 机制,通过这个 Header 可以在服务端与客户端之间沟通缓存信息。对于请求头,存在一种请求缓存指令,而对于响应头,则存在一种响应缓存指令

常见的缓存指令如下:

请求缓存指令

  • max-age=:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。
  • max-stale[=]:表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。
  • min-fresh=:表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。
  • no-cache :在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。
  • no-store:缓存不应存储有关客户端请求的任何内容。
  • no-transform:不得对资源进行转换或转变,Content-EncodingContent-RangeContent-Type等 Header 不能由代理修改。
  • only-if-cached:表明客户端只接受已缓存的响应,并且不向原始服务器检查是否有更新的数据。

响应缓存指令

  • must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
  • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证
  • no-store:缓存不应存储有关服务器响应的任何内容。
  • no-transform:不得对资源进行转换或转变,Content-EncodingContent-RangeContent-Type等 Header 不能由代理修改。
  • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有 max-age 指令或 Expires 消息头)。
  • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容。
  • proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(如代理),并被私有缓存忽略。
  • max-age=:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。
  • s-maxage=:覆盖 max-age 或者 Expires 头,但它仅适用于共享缓存(如代理),并被私有缓存忽略。

其中我们常用的就是加粗的几个字段(max-agemax-staleno-cache)。

那么如果我们同时出现了 Cache-Control:max-age= 以及 Expires,如何抉择呢?实际上 HTTP 协议规定了此时会以 max-age 为准。

Last-Modified / If-Modified-Since

这两个字段需要配合 Cache-Control 进行使用,Last-Modified 位于响应头,If-Modified-Since 位于请求头。它们的含义分别是:

  • Last-Modified:该响应资源最后的修改时间,服务器在响应请求的时候可以填入该字段。
  • If-Modified-Since:客户端缓存过期时(max-age 到达),发现该资源具有 Last-Modified 字段,可以在 Header 中填入 If-Modified-Since 字段,并填入当前时间。服务端收到该时间后会与该资源的最后修改时间进行比较。
    • 若该资源已经被修改 ,则会对整个资源响应。
    • 否则说明该资源在访问时未被修改,则会响应状态码 304,告知客户端可以使用缓存的资源。

Etag / If-None-Match

同样需要配合 Cache-Control 使用,Etag 位于响应头,If-None-Match 位于请求头。并且这它们的优先级高于 If-ModifiedIf-Modified-Since

它们的含义分别是:

  • Etag:请求的资源在服务器中的唯一标识,规则由服务器决定。
  • If-None-Match:若客户端在缓存过期时(max-age 到达),发现该资源具有 Etag 字段,就可以添加 If-None-Match Header,并传入 Etag 中的值,之后服务器就会根据这个唯一标识来寻找对应的资源,根据其更新与否情况返回给客户端 200 或 304。

HTTP/2.0 服务器推送

HTTP/2.0 中还引入了服务器推送(Server Push)功能。它的核心思想就是提前推送响应。例如在请求一个网页时,由于在 HTTP/1.x 中无法完全进行连接复用,因此很多站点为了减少请求数会将 css、脚本等资源内嵌到 HTML 中。而在 HTTP/2.0 中由于支持了服务器推送,因此当用户请求了 HTML 时,服务器可以将这个 HTML 文件及可能会用到的静态资源一并推送给客户端。

服务器可以通过 PUSH_PROMISE 帧来传输主动推送的资源,客户端根据其中的 Promise Stream Id 可以读取响应。为了避免竞争PUSH_PROMISE 帧往往先被发送,再发送 HTML 的内容(为了避免客户端先收到 HTML,之后再发起对静态资源的请求)。

HTTP/2.0 流量控制

HTTP/2.0 中引入了流量控制机制,它基于 WINDOW_UPDATE 帧,发送方维护了一个流量控制窗口,大小默认为 65535。发送方每发送一个 DATA 帧,就将窗口大小递减这个帧的大小,若窗口大小比帧的大小还小,则会将帧拆分。接收方通过 WINDOW_UPDATE 帧通知发送方对窗口大小的增加值,整个流量控制完全由接收方进行控制,它可以为流和连接设定任意的窗口大小,发送方会遵循接收方的流量控制限制。并且需要注意的是:只有 DATA 帧遵循流量控制的限制

HTTP 协议的缺点

HTTP 协议看似完美,但实际上在安全性上存在一定风险:

  • 通信通过明文进行传输,不进行加密,容易被窃听
  • 并不会对双方的身份进行验证,可能被恶意主机伪装
  • 无法验证报文完整性,可能遭到篡改

以上的问题可以通过在 HTTP 与 TCP 之间引入 SSL 层,通过 SSL 层来实现对安全性的保证,这就是 HTTPS 协议。

参考资料

Keep-Alive

【HTTP】keep-alive

HTTP Keep-Alive模式

都 2019 年了,还问 GET 和 POST 的区别

HTTP|GET 和 POST 区别?网上多数答案都是错的!

循序漸進理解 HTTP Cache 機制

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注

%d 博主赞过: