HTTP 详解

(一)HTTP 简介

HTTP(Hyper Text Transfer Protocol,超文本传输协议)是用于从万维网(WWW,World Wide Web)服务器传输超文本到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据(HTML 文件、图片文件、查询结果等)。

HTTP 是一个属于应用层的面向对象的协议,由于其简洁快速的方式,适用于分布式超媒体信息系统。HTTP 协议工作于客户端 - 服务端架构上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求,Web 服务器根据接收到的请求向客户端发送响应信息。

image-20210331163106019

主要特点

  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因此通信速度很快。
  • 灵活:HTTP 允许传输任意类型的数据对象。传输的类型由 Content-Type 加以标记。
  • 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则必须重传,这可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

(二)HTTP 报文

作为信息载体参与前后端的交互,HTTP 报文一般包括:通用头部(请求行/响应行)、请求/响应头部、空行、请求/响应体。

1. 请求报文

image-20210410232654377
  • 请求行:由请求方法、URL、HTTP 协议版本构成。它们用空格分隔。如 GET /index.html HTTP/1.1
  • 请求头部:由关键字/值对组成,每行⼀对,关键字和值用英文冒号分隔
  • CRLF:Carriage-Return Line-Feed,意思是回车换行,一般作为分隔符存在
  • 请求体:post 和 put 等请求携带的数据。请求实体中会将一些需要的参数都放入消息实体(用于 post 请求)。譬如实体中可以放参数的序列化形式(a=1&b=2这种),或者直接放表单对象(FormData对象,上传时可以夹杂参数以及文件)等。
image-20210403223400738

2. 响应报文

image-20210410233735305
  • 响应行:由协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK
  • 响应头部:由关键字/值对组成,每行⼀对,关键字和值用英文冒号分隔
  • CRLF:Carriage-Return Line-Feed,意思是回车换行,一般作为分隔符存在
  • 响应体:响应实体用来存放服务端返回给客户端的内容。接口请求时实体一般是对应信息的 json 格式,而页面请求时实体则是一个 html 字符串,然后浏览器自行解析并渲染

下图是对某请求的 http 报文结构的简要分析

image-20210324154546155

3. 重要组成

① HTTP 请求方法

HTTP1.0 定义了三种请求方法:GET、POST 和 HEAD;

HTTP1.1 新增了五种请求方法:OPTIONS、PUT、DELETE、TRACE 和 CONNECT。

  • GET:通常用于请求服务器发送某些资源
  • HEAD:请求资源的头部信息,并且这些头部与 HTTP GET 方法请求时返回的⼀致。该请求方法的⼀个使用场景是在下载⼀个大文件前先获取其大小再决定是否要下,因此可以节约带宽资源
  • OPTIONS:用于获取目的资源所支持的通信选项。在跨域拒绝时,method 可能为 options,状态码为404/405
  • POST:发送数据给服务器
  • PUT:用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE:用于删除指定的资源
  • PATCH:用于对资源进行部分修改
  • CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器
  • TRACE:回溯服务器收到的请求,主要用于测试或诊断

GET VS POST

  • 数据传输方式不同:GET 请求通过 URL 传输数据,而 POST 的数据通过请求体传输。
  • 安全性不同:POST 的数据因为在请求体内,所以有⼀定的安全性保证,而 GET 的数据在 URL 中,通过历史记录,缓存很容易查到数据信息。
  • 数据类型不同:GET 只允许 ASCII 字符,而 POST 无限制
  • GET 无害:刷新、后退等浏览器操作 GET 请求是无害的,POST 可能重复提交表单
  • 特性不同:GET 是安全(这里安全是指只读特性,即这个方法不会引起服务器状态变化)且幂等(指同⼀个请求方法执行多次和仅执行⼀次的效果完全相同),而 POST 是非安全非幂等

PUT VS POST

PUT 和 POST 都是给服务器发送新增资源,但 PUT 方法是幂等的:连续调用⼀次或者多次的效果相同(无副作用),而 POST 方法是非幂等的。除此之外,通常情况下,PUT 的 URI 指向是具体单⼀资源,而 POST 指向资源集合。

以开发⼀个博客系统为例,当我们要创建⼀篇文章时,往往使用 POST https://www.jianshu.com/articles,这个请求的语义是:在 articles 的资源集合下创建⼀篇新的文章,如果多次提交该请求则会创建多篇文章,这是非幂等的。

PUT https://www.jianshu.com/articles/820357430 的语义是更新对应文章下的资源(比如修改作者名称),这个 URI 指向的就是单⼀资源,而且是幂等的。比如你把『刘德华』修改成『张学友』,提交多少次都是修改成『张学友』 。

注:POST 表示创建资源,PUT 表示更新资源这种说法是错误的,两个都能创建资源,根本区别就在于幂等性。

PUT VS PATCH

PUT 和 PATCH 都是更新资源,而 PATCH 是用来对已知资源进行局部更新。例如一篇文章的地址 https://www.jianshu.com/articles/820357430,这篇文章可表示为:

1
2
3
4
5
6
article = { 
author: 'dxy',
creationDate: '2019-6-12',
content: '我写⽂章像张学友',
id: 820357430
}

当要修改文章作者时,可以直接发送 PUT https://www.jianshu.com/articles/820357430 ,这个时候的数据应该是:

1
2
3
4
5
6
{ 
author:'张学友',
creationDate: '2019-6-12',
content: '我写⽂章像张学友',
id: 820357430
}

用 put 修改会直接覆盖原来的资源,但为了避免每次都要附带大量无用信息,那么可以发送 PATCH https://www.jianshu.com/articles/820357430 ,这时只需要:

1
2
3
{ 
author:'张学友',
}

② HTTP 响应状态码

当请求出错时,状态码能帮助快速定位问题。列举下大致不同范围状态的意义

image-20210324153409226

2XX 成功

  • 200 OK:表示从客户端发来的请求在服务器端被正确处理
  • 201 Created:请求已经被实现,而且有⼀个新的资源已经依据请求的需要而建立
  • 202 Accepted:请求已接受,但是还没执行,不保证完成请求
  • 204 No content:表示请求成功,但响应报文不含实体的主体部分
  • 206 Partial Content:进行范围请求

3XX 重定向

  • 301 moved permanently:永久性重定向,表示资源已被分配了新的 URL
  • 302 found:临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other:表示资源存在着另⼀个 URL,应使用 GET 方法定向获取资源
  • 304 not modified:表示服务器允许访问资源,但发生了请求未满足条件的情况
  • 307 temporary redirect:临时重定向,和 302 含义相同

4XX 客户端错误

  • 400 bad request:请求报文存在语法错误
  • 401 unauthorized:表示发送的请求需要有通过 HTTP 认证的认证信息
  • 403 forbidden:表示对请求资源的访问被服务器拒绝
  • 404 not found:表示在服务器上没有找到请求的资源
  • 408 Request timeout:客户端请求超时
  • 409 Conflict:请求的资源可能引起冲突

5XX 服务器错误

  • 500 internal sever error:表示服务器端在执行请求时发生了错误
  • 501 Not Implemented:请求超出服务器能力范围,例如服务器不支持当前请求所需要的某个功能,或者请求是服务器不支持的某个方法
  • 503 service unavailable:表明服务器暂时处于超负载或正在停机维护,无法处理请求
  • 505 http version not supported:服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本

同样是重定向,307、303 和 302 的区别

302 是 http1.0 的协议状态码,在 http1.1 版本时为了细化 302 状态码又出来了 303 和 307。303 明确表示客户端应当采用 get 方法获取资源,它会把 POST 请求变为 GET 请求进行重定向。307 会遵照浏览器标准,不会从 post 变为 get。

③ HTTP 首部

一般来说,请求头部和响应头部是匹配分析的。譬如,请求头部的Accept要和响应头部的Content-Type匹配,否则会报错;跨域请求时,请求头部的Origin要匹配响应头部的Access-Control-Allow-Origin,否则会报跨域错误;在使用缓存时,请求头部的 If-Modified-SinceIf-None-Match分别和响应头部的 Last-ModifiedETag对应。

通用首部字段(General Header Fields):请求报文和响应报文两方都会使用的首部

  • Cache-Control:控制缓存,告诉浏览器或其他客户什么环境可以安全的缓存文档
  • Connection:当浏览器与服务器通信时对于长连接如何进行处理,如 keep-alive
  • Upgrade:升级为其他协议
  • via:代理服务器的相关信息
  • Warning:错误和警告通知
  • Transfer-Encoding:报文主体的传输编码格式
  • Trailer:报文末端的首部⼀览
  • Pragma:报文指令
  • Date:创建报文的日期

请求首部字段(Reauest Header Fields):客户端向服务器发送请求报文时使用的首部

  • Accept:客户端或者代理能够处理的媒体类型
  • Accept-Encoding:优先处理的编码格式
  • Accept-Language:优先处理的自然语言
  • Accept-Charset:优先处理的字符集
  • Cookie:有 cookie 并且同域访问时会自动带上
  • If-Match:比较实体标记(ETag)
  • If-None-Match:比较实体标记(ETag),与 If-Match 相反
  • If-Modified-Since:比较资源更新时间(Last-Modified)
  • If-Unmodified-Since:比较资源更新时间(Last-Modified),与 If-Modified-Since 相反
  • If-Ranges:资源未更新时发送实体 byte 的范围请求
  • Range:实体的字节范围请求
  • Authorization:web 的认证信息,放 token 的地方
  • Proxy-Authorization:代理服务器要求 web 认证信息
  • Host:请求资源所在服务器
  • From:用户的邮箱地址
  • User-Agent:客户端程序信息
  • Max-Forwrads:最大的逐跳次数
  • TE:传输编码的优先级
  • Referer:请求原始放的 url
  • Expect:期待服务器的特定行为

响应首部字段(Response Header Fields):从服务器向客户端响应时使用的字段

  • Accept-Ranges:能接受的字节范围
  • Access-Control-Allow-Headers:服务器端允许的请求 Headers
  • Access-Control-Allow-Methods:服务器端允许的请求方法
  • Access-Control-Allow-Origin:服务器端允许的请求 Origin 头部(譬如为*)
  • Age:推算资源创建经过时间
  • Location:令客户端重定向的 URI
  • vary:代理服务器的缓存信息
  • ETag:能够表示资源唯⼀资源的字符串
  • WWW-Authenticate:服务器要求客户端的验证信息
  • Proxy-Authenticate:代理服务器要求客户端的验证信息
  • Set-Cookie:设置和页面关联的 cookie,服务器通过这个头部把 cookie 传给客户端
  • Keep-Alive:如果客户端有 keep-alive,服务端也会有响应(如 timeout=38)
  • Server:服务器的一些相关信息
  • Retry-After:和状态码 503 ⼀起使用的首部字段,表示下次请求服务器的时间
  • Content-Type:服务端返回的实体内容的类型
  • Date:数据从服务器发送的时间
  • Max-age:客户端的本地资源应该缓存多少秒,开启了 Cache-Control 后有效

实体首部字段(Entiy Header Fields):针对请求报文和响应报文的实体部分使用的首部

  • Allow:资源可支持 http 请求的方法

  • Content-Language:实体的资源语言

  • Content-Encoding:实体的编码格式

  • Content-Length:实体的大小(字节)

  • Content-Type:实体媒体类型 MIME。不允许空格,大小写不敏感

    没有特定 subtype 的文本文件使用 text/plain。没有特定或已知 subtype 的二进制文件使用 application/octet-stream

    image-20210504154751602
  • Content-MD5:实体报文的摘要

  • Content-Location:代替资源的位置

  • Content-Ranges:实体主体的位置返回

  • Last-Modified:资源最后的修改资源

  • Expires:实体主体的过期资源

④ URI & URL

HTTP 使用统一资源标识符(Uniform Resource Identifiers, URI)来建立连接和传输数据。而统一资源定位符(Uniform Resource Locator, URL)是一种特殊类型的 URI,用来标识互联网上某一处资源的地址,包含了用于查找该资源的必要信息。

image-20210331163633346

从上面的 URL 可以看出,一个完整的 URL 包括以下几部分:

  1. 协议:该 URL 的协议部分为 http:,这代表网页使用的是 HTTP 协议。在 Internet 中可以使用多种协议,如 HTTP、FTP 等。http:后面的//为分隔符
  2. 域名:该 URL 的域名部分为 www.aspxfans.com。也可以把 IP 地址作为域名使用
  3. 端口:域名后是端口,域名和端口之间使用:作为分隔符。端口是非必需的,如果省略端口部分,则采用默认端口
  4. 虚拟目录:从域名后的第一个/到最后一个/为止,是虚拟目录部分。虚拟目录也非 URL 必要部分。本例中的虚拟目录是/news
  5. 文件名:从域名后的最后一个/开始到?为止,是文件名部分;如果没有?,则从最后一个/开始到#为止,是文件部分;如果没有?#,那么从域名后的最后一个/开始到结束,都是文件名部分。本例中的文件名是 /index.asp。文件名部分也不是 URL 必需部分,如果省略该部分,则使用默认的文件名
  6. 参数:从?开始到#之间的部分为参数部分(又称搜索部分、查询部分)。本例中的参数部分为?boardID=5&ID=24618&page=1。参数可以允许有多个参数,参数与参数之间用&作为分隔符。
  7. :从#开始到最后,都是锚部分。本例中的锚是name。锚也不是 URL 必需部分

URL 编码

URL 只能使用 ASCII 字符集来通过因特网进行发送。由于 URL 常常会包含 ASCII 集合之外的字符,所以必须将其转换为有效的 ASCII 格式。URL 编码使用%跟随两位的十六进制数来替换非 ASCII 字符。URL 也不能包含空格,所以编码通常使用 + 来替换空格。

拿前端安全篇的反射型 XSS 的例子来说,浏览器的搜索框会显示如下 URL:

https://www.kkkk1000.com/xss/Reflected/searchResult.html?kw=斗罗大陆

但复制该 URL 黏贴在这里,则得到如下结果:

https://www.kkkk1000.com/xss/Reflected/searchResult.html?kw=%E6%96%97%E7%BD%97%E5%A4%A7%E9%99%86

(三)HTTP 数据传输

在 HTTP 数据传输中,内容编码和传输编码一般都是配合使用的。先使用内容编码将内容实体进行压缩,然后再通过传输编码分块发送出去。客户端接收到分块的数据,再将数据进行重新整合,还原成最初的数据。

image-20210504230426905

1. HTTP 内容编码

编码是为了压缩报文实体内容的大小,通过压缩服务器响应报文传输的内容实体,在一定程度上就可以加快响应的速度。而相应的压缩算法分为如下两类。

  • 无损压缩算法:可以还原,通常应用在文本;
  • 有损压缩算法:以加大压缩率为目的,对文件进行有损失的压缩。这是一种不可逆的操作,通常用于对质量要求不高的图片或视频。

在 HTTP 协议中,通常只会对文本内容进行压缩编码。因为进行压缩会消耗服务器资源,而文本文件比多媒体文件轻便很多。并且一般多媒体文件本身已经是高度压缩的二进制格式,再次进行压缩的意义也不大。

HTTP 协议是一种松散的协商协议,需要客户端和服务端相互配合才可以生效。而压缩算法有很多种,选择哪一种也是需要双方协商的:客户端需要把自己支持的压缩算法通知给服务器,服务器再择优选择压缩算法对资源进行压缩,并在响应中告知客户端其所选择的压缩算法。这种双方参与的压缩过程基于 HTTP 报文中的报文头来实现。

① 编码头部

请求头中的 Accept-Encoding

客户端为了告知服务端当前支持的压缩编码,会在请求头中设置 Accept-Encoding ,用来指定当前客户端支持的压缩编码,如果有多个则使用逗号 , 进行分割。优先级还可以通过分割的顺序来指定。

响应头中的 Content-Encoding

服务端为了在响应报文里体现当前对内容压缩使用的编码格式,会在响应头中使用 Content-Encoding 标记,它是一个明确值,所以只可能有一个。

响应头中的 Content-Length

编码的目的就是压缩,所以当服务端选择压缩内容实体时,同时还会修改 Content-Length 来明确表示当前实体被编码压缩后的长度。

压缩前:

image-20210412230705535

压缩后:

image-20210412230728343

② 编码类型

HTTP 定义了一些标准的内容编码类型,并且可以扩展更多的编码类型。由互联网号码分配机构(IANA)对各种编码进行标准化,它给每个内容编码算法分配一个唯一的代号。Content-Encoding 就是用这些标准化的代号来说明编码使用的算法。比较常用的算法有:

  • gzip:表明实体采用 GNU zip 编码;
  • compress:表明实体采用 Unix 的文件压缩程序;
  • deflate:表明使用是用 zlib 的格式压缩的;
  • br:表明实体使用 Brotli 算法的压缩格式;
  • identity:表明没有对实体进行编码,为默认值。

在这些算法中,除了 identity 之外,都是无损压缩,它们都能还原成原始的文本内容。gzip 通常是效率最高且使用最广泛的。但是 gzip 对媒体文件的压缩效果相对较差,本身 JPG/PNG 这类文件已经是一种高度压缩的二进制文件,开启 gzip 效果甚微还会浪费大量 CPU 资源。浏览器的默认实现中,这些压缩编码通常只会作用在文本内容上(即 Content-Type 为 text/Xxx 的请求),而对于一些媒体文件,则不会使用这种方式对其进行压缩。

gzip 编码是采用的 GNU Zip 编码,是一种无损的压缩算法,用于减少传输报文实体的大小,它是可逆的压缩算法,不会导致信息损失。gzip 的压缩效率相对较高,并且使用也是最为广泛的,在工作中如果不特殊说明,说到的 HTTP 压缩,通常就是指的 gzip。

gzip 的原理,简单来说,就是会去扫描整个文本的字符串,找到一样的字符串,就只保留一个并分配一个标识,然后将其他相同的字符串使用这个标识替换,使整个文件变小。在还原的时候,只需要将每个标识代表的字符串,替换还原,就可以还原成最初的内容实体。这种压缩算法,非常适用于现在的互联网产品,HTML、CSS、JavaScript 以及 Json 中,都包含了大量重复的字符串,所以在这里使用 gzip 是非常合适的。gzip 具体能压缩多少,完全取决于压缩的实体内容,内容文本中,包含越多相同的字符串,压缩率就越高,相反则越低。在理想状态下,gzip 的压缩率能高达 70%。

② 编码过程

HTTP 对内容编码的大致流程如下图。

image-20210412231309391

2. HTTP 传输编码

在 HTTP 的报文头中,Transfer-Encoding 首部用来指明当前使用的传输编码。Transfer-Encoding 会改变报文格式和传输方式,使用它并不能减少传输内容的大小,甚至还可能会使内容变大。但其实这是为了解决一些特殊问题:传输编码必须配合长连接使用,它就是为了在长连接中将数据分块传输,并标记传输结束而设计的。

在早年间的设计里,和内容编码使用 Accept-Encoding 来标记客户端接收的压缩编码类型一样,传输编码还需要配合 TE 这个请求报文头来使用,用于指定支持的传输编码。但在最新的 HTTP/1.1 协议规范中,只定义了一种传输编码:分块编码(chunked),所以不再需要依赖 TE 这个头部。

① 长连接

在早期 HTTP 版本中,传输数据大致经历发起请求、建立连接、传输数据、关闭连接等步骤,而长连接(Persistent Connection)去掉了关闭连接的步骤,让客户端和服务端可以继续通过此次连接传输内容。这样做是为了提高传输效率,因为 HTTP 协议建立在 TCP 协议之上,自然有 TCP 的三次握手、慢启动等特性,而每次连接其实都是一次宝贵的资源。

在 HTTP/1.0 协议的早期阶段中并没有长连接的概念,它是在后期才引入的:通过 Connection:Keep-Alive 这个头部来标记实现,用于通知客户端或服务端相对的另一端,在发送完数据后不要断开 TCP 连接,之后还需要再次使用。

长连接的优点:

  • 较少的 CPU 和内存的使用(由于同时打开的连接减少了)
  • 允许请求和应答的 HTTP 管线化
  • 降低拥塞控制(TCP 连接减少了)
  • 减少了后续请求的延迟(无需再进行握手)
  • 报告错误无需关闭 TCP 连接

为了尽可能提高 HTTP 的性能,之后的 HTTP/1.1 协议规定所有的连接必须都是长的,除非显式地在报文头里通过 Connection:close 这个首部,指定在传输结束后会关闭此连接。实际上在 HTTP/1.1 中 Connection 这个头部已经没有 Keep-Alive 这个取值了,只是由于历史原因,很多客户端和服务端依然保留了这个头部。

短连接可以直接通过服务器关闭连接来获取消息的传输长度,而长连接则需要依靠 Content-Length 和 Transfer-Encoding 首部来判定数据有无发送成功。在长连接中,Transfer-Encoding 优先级高于 Content-Length。

② Content-Length

在早期不支持长连接时,可以依靠连接断开来判定当前传输已经结束。但真正规范的操作是使用 Content-Length 头部来指定当前传输的实体内容长度,在长连接的情况下更应如此。对于定长的数据包而言,发送端在发送数据时,需要设置Content-Length来指明发送数据的长度。如果采用了 Gzip 压缩的话,Content-Length 设置的就是压缩后的传输长度。

image-20210504215035856
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置 Content-Length
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("你好,使用的是 Content-Length 设置传输数据形式");
}
})

server.listen(3000, () => {
console.log("成功启动--TinaTian");
})

Content-Length 在这里起到了一个响应实体已经发送结束的判断依据,它必须和内容实体的长度一致。如果 Content-Length 小于内容实体的长度,则会截断,反之则无法判定当前响应已经结束,会将请求持续挂起造成 Padding 超时状态。

③ 分块传输

理想情况下,服务端在响应一个请求时,需要知道其内容实体的大小。但在实际应用中有些内容实体的长度难以知晓,比如内容实体来自网络文件、或者是动态生成的。这时如果依然想提前获取到内容实体的长度,只能开一个足够大的 Buffer,等内容全部缓存好了再计算。但这并非良策:全部缓存到 Buffer 里,不仅消耗更多内存,而且更耗时。

对于不定长的数据,我们不能依赖 Content-Length 的值来判定当前内容实体是否传输完成,而是根据 Transfer-Encoding 这个头部判定。在最新的 HTTP/1.1 协议里,Transfer-Encoding 只有 chunked 参数,标识当前为分块编码传输,后续就能将内容实体包装一个个块进行传输。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 引入 nodejs 中 http 模块。
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');

res.write("你好,使用的是 Transfer-Encoding 设置传输数据形式");
setTimeout(() => {
res.write("第一次传输数据给您<br/>");
}, 1000);
res.write("稍等一下");
setTimeout(() => {
res.write("第一次传输数据给您");
res.end()
}, 3000);
}
})

server.listen(3000, () => {
console.log("成功启动--TinaTian");
})

分块传输的规则:

  1. 每个分块包含一个 16 进制的数据长度值和真实数据。

  2. 数据长度值独占一行,和真实数据通过 CRLF(\r\n) 分割。

  3. 数据长度值,不计算真实数据末尾的 CRLF,只计算当前传输块的数据长度。

  4. 最后通过一个数据长度值为 0 的分块,来标记当前内容实体传输结束。

image-20210504220820851

在上例中,首先在响应头部里标记 Transfer-Encoding: chunked,后续先传递了第一个分块“0123456780”(长度为 b,11 的十六进制),之后分别传输了“Hello CxmyDev”和“123”(长度分别为 d 和 3),最后以一个长度为 0 的分块标记当前响应结束。

当使用 chunked 进行分块编码传输时,在传输结束后,还能在分块报文的末尾再追加一段数据,此数据称为拖挂(Trailer)。拖挂的数据,可以是服务端在末尾需要传递的数据,客户端其实可以忽略并丢弃拖挂的内容,这就需要双方协商好传输的内容了。在拖挂中可以包含附带的首部字段,除了 Transfer-Encoding、Trailer 以及 Content-Length 首部之外,其他 HTTP 首部都可以作为拖挂发送。

拖挂一般用来传递一些在响应开始时无法确定的某些值。以下图为例,Content-MD5 首部是一个常见的在拖挂中追加发送的首部。和长度一样,对于需要分块编码传输的内容实体,在开始响应时也很难算出它的 MD5 值。注意这里在头部增加了 Trailer,用以指定末尾还会传递一个 Content-MD5 的拖挂首部,如果有多个拖挂的数据,可以使用逗号进行分割。

image-20210504223235575

(四)HTTP 2.0

先回顾 HTTP 的发展史。

img

  • HTTP/0.9 - 单行协议:只支持 GET 方法;没有首部;只能获取纯文本。
  • HTTP/1.0 - 搭建协议的框架:增加了首部、状态码、权限、缓存、长连接(默认短连接)等规范,搭建了协议的基本框架。协议规定,对于同一个 tcp 连接,所有的 http1.0 请求放入队列中,只有前一个请求的响应收到了,才能发送下一个请求,这个时候就发生了阻塞,并且这个阻塞主要发生在客户端。在 HTTP/1.0 中每一次请求都需要建立一个 TCP 连接,请求结束后立即断开连接。
  • HTTP/1.1 - 完善:默认长连接;强制客户端提供 Host 首部;管线化;Cache-Control、ETag 等缓存的相关扩展。

HTTP 发展到 1.1 时,还存在下列问题:

  • 队头阻塞:TCP 连接上只能发送一个请求,前面的请求未完成前,后续的请求都在排队等待。在 HTTP/1.1 中,每一个连接都默认是长连接 (persistent connection)。对于同一个 tcp 连接,允许一次发送多个 http1.1 请求,也就是说,不必等前一个响应收到,就可以发送下一个请求。这样就解决了 http1.0 的客户端的队头阻塞,而这也就是 HTTP/1.1 中管道 (Pipeline) 的概念了。但是,http1.1 规定,服务器端的响应的发送要根据请求被接收的顺序排队,也就是说,先接收到的请求的响应也要先发送。这样造成的问题是,如果最先收到的请求的处理时间长的话,响应生成也慢,就会阻塞已经生成了的响应的发送,这也会造成队头阻塞。可见,http1.1 的队首阻塞是发生在服务器端。
  • 多个 TCP 连接:虽然 HTTP/1.1 管线化可以支持请求并发,但是浏览器很难实现,chrome、firefox 等都禁用了管线化。所以 1.1 版本请求并发依赖于多个 TCP 连接,建立 TCP 连接成本很高,还会存在慢启动的问题。
  • 头部冗余:HTTP/1.X 版本是采用文本格式,首部未压缩,而且每一个请求都会带上 cookie、user-agent 等完全相同的首部。
  • 客户端需要主动请求。

接下来正式开始聊聊 2.0。看看 2.0 相比 1.1 的一些重大改进。

1. 二进制分帧层

HTTP2 性能提升的核心就在于二进制分帧层。HTTP2 是二进制协议,他采用二进制格式传输数据而不是 1.x 的文本格式。

img

1.1 响应是文本格式,而 2.0 把响应划分成了两个帧,图中的 HEADERS(首部)和 DATA(消息负载)是帧的类型。了解更多帧的类型。也就是说一条 HTTP 响应,划分成了两个帧来传输,并且采用二进制来编码。

这里来提三个概念。

  • 流(Stream):已建立的 TCP 连接上的双向字节流,可以承载一个或多个消息。一个 TCP 连接上可以有任意数量的流。
  • 消息(Message):一个完整的 HTTP 请求或响应,由一个或多个帧组成。特定消息的帧在同一个流上发送,这意味着一个 HTTP 请求或响应只能在一个流上发送。
  • 帧(Frame):通信的基本单位。

2. 多路复用

上面提到 HTTP/1.1 的队头阻塞和多个 TCP 连接的问题,HTTP2 的多路复用完美解决。HTTP2 让所有的通信都在一个 TCP 连接上完成,真正实现了请求的并发。我们来看一下 HTTP2 具体是怎么实现的:

img

HTTP2 建立一个 TCP 连接,一个连接上面可以有任意多个流(stream),消息分割成一个或多个帧在流里面传输。帧传输过去以后,再进行重组,形成一个完整的请求或响应。这使得所有的请求或响应都无法阻塞。

img

打开控制台可以看到,HTTP/1.1 的方式,后面的图片的加载时间主要耗时在 stalled,stalled 的意思是从 TCP 连接建立完成,到真正可以传输数据之间的时间差。这就是队头阻塞,前面的请求没有处理,后面的请求都在排队等待。

这里能很直观的看到就是多路复用起到的优化作用。因为 HTTP2 实现了请求并发,后面的请求不用再等待,加载时长当然少了很多。截一张 HTTP2 的图片加载耗时详情来看看(要看比较靠后的请求):

img

我们发现后面的很多请求依旧有在排队哎,只是排队的时间相对 1.1 少了很多。一个 TCP 连接可以有任意数量的流,也就是同时可以并发任意数量的请求啊,为啥还会排队呢?原因就是请求太多时,浏览器或服务器会受不了,这超出了它的处理能力。流控制帮我们解决了这个问题,流控制会管理数据的传输,允许接收者停止或减少发送的数据量,免得接收方不堪重负。所以请求太多时,还是会存在排队等待的问题,因为不管是客户端或服务器端,能同时处理请求或响应都是有限的。

3. 头部压缩

头部压缩也是 HTTP2 的一大亮点。在 1.X 版本中,首部用文本格式传输,通常会给每个传输增加 500 - 800 字节的开销。现在打开一个网页上百个请求已是常态,而每个请求带的一些首部字段都是相同的,例如 cookie、user-agent 等。HTTP2 为此采用 HPACK 压缩格式来压缩首部。头部压缩需要在浏览器和服务器端之间:

  • 维护一份相同的静态字典,包含常见的头部名称,以及常见的头部名称和值的组合
  • 维护一份相同的动态字典,可以动态的添加内容
  • 通过静态 Huffman 编码对传输的首部字段进行编码

HTTP2 的静态字典是长这个样子的(只截取了部分,完整表格在这里):

img

所以在传输首部字段的时候,例如要传输 method:GET,那我们只需要传输静态字典里面 method:GET 对应的索引值就可以了,一个字节搞定。像 user-agent、cookie 这种静态字典里面只有首部名称而没有值的首部,第一次传输需要 user-agent 在静态字典中的索引以及他的值,值会采用静态 Huffman 编码来减小体积。

第一次传输过 user-agent 之后呢,浏览器和服务器端就会把它添加到自己的动态字典中。后续传输就可以传输索引了,一个字节搞定。我们用 WireShark 来抓包验证一下:HTTP2 目前都是 HTTPS 的请求,WireShark 对 HTTPS 网站抓包解密请参考这里

  • 首次传输 user-agent 和第二次传输 user-agent

    img

img

由于第一次传输的时候,字典里面并没有 user-agent 的值,这时候 user-agent 是 63 字节,第二次传输时,他已经在动态字典里面了,只传索引,一个字节搞定。

  • HPACK 的首部压缩力度

img

Header 解码后的长度有 471 个字节,而 HEADERS 流只有 246 个字节。这只是第一个请求,后续的请求压缩力度会更大,因为前面请求用到的首部(静态字典中没有的)会添加到动态字典中,使得后续请求只需要传输字典里面的索引。

img

4. 服务器端推送

服务器端推送使得服务器可以预测客户端需要的资源,主动推送到客户端。

例如:客户端请求 index.html,服务器端能够额外推送 script.js 和 style.css。实现原理就是客户端发出页面请求时,服务器端能够分析这个页面所依赖的其他资源,主动推送到客户端的缓存,当客户端收到原始网页的请求时,它需要的资源已经位于缓存。

针对每一个希望发送的资源,服务器会发送一个 PUSH_PROMISE 帧,客户端可以通过发送 RST_STREAM 帧来拒绝推送(当资源已经位于缓存)。这一步的操作先于父响应(index.html),客户端了解到服务器端打算推送哪些资源,就不会再为这些资源创建重复请求。当客户端收到 index.html 的响应时,script.js 和 style.css 已经位于缓存。

(五)HTTPS

HTTPS 是安全版的 HTTP,因为 HTTP 协议的数据都是明文进行传输的,所以对于⼀些敏感信息的传输就很不安全,HTTPS 就是为了解决这种问题而生的。HTTPS 要比 HTTP 多了 secure 安全性这个概念,实际上,HTTPS 并不是一个新的应用层协议,它其实就是 HTTP + TLS/SSL 协议组合而成,而安全性的保证正是 SSL/TLS 所做的工作。HTTPS 能够对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;同时能够对网站服务器进行真实身份认证。

1. 与 HTTP 的区别

  • HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议;
  • HTTPS 标准端口 443,HTTP 标准端口 80;
  • HTTPS 需要用到 SSL 证书,而 HTTP 不用。
image-20210412205704819

2. 保障安全原理

  • 对称加密:即通信的双方都使用同⼀个秘钥进行加解密,比如特务接头的暗号,就属于对称加密。对称加密虽然简单高效,但无法解决首次把秘钥发给对方时被黑客拦截的问题。
  • 非对称加密:私钥和公钥组成密钥对,用私钥加密的数据,只有对应的公钥才能解密;用公钥加密的数据,只有对应的私钥才能解密。因为通信双方都有⼀套自己的密钥对,通信前双方会先把自己的公钥先发给对方,然后对方再拿着这个公钥来加密数据响应给对方,这样对方就能用自己的私钥进行解密。非对称加密虽然安全性更高,但其速度很慢,影响性能。

HTTPS 结合两种加密方式,将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方便能使用对称加密来进行沟通。但如果此时在客户端和服务器之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的所有数据。所以我们需要⼀个安全的第三方颁发证书(CA)证明对方的身份,防止被中间人攻击。

证书中应包括:签发者、证书用途、使用者公钥、使用者私钥、使用者的 HASH 算法、证书到期时间等。为了避免中间⼈篡改证书,我们需要使用数字签名的技术。数字签名就是用 CA 自带的 HASH 算法对证书的内容进行 HASH 得到⼀个摘要,再用 CA 的私钥加密,最终组成数字签名。当别人把他的证书发过来时,再用同样的 Hash 算法重新生成消息摘要,然后用 CA 的公钥对数字签名解密得到 CA 创建的消息摘要,两者⼀比较便知道中间有没有被人篡改了。

image-20210411103304246

3. TLS/1.2 握手流程

非对称加密采用的算法是 RSA,所以在一些文章中会看见 「传统 RSA 握手」,基于现在 TLS 主流版本是 1.2,所以接下来梳理的是 「TLS/1.2 握手过程」

image-20210412211336720

① Client 发起一个 HTTPS 请求,连接 443 端口。这是请求公钥的过程

② Server 端收到请求后,通过第三方机构私钥加密,会把数字证书(也可以认为是公钥证书)发送给 Client。

③ 浏览器会自动带上权威第三方机构公钥,使用匹配的公钥对数字签名进行解密。根据签名生成的规则对网站信息进行本地签名生成,然后比对两者签名。如果匹配则说明认证通过,不匹配则获取证书失败。

④ 在安全拿到「服务器公钥」后,客户端 Client 随机生成一个「对称密钥」,使用「服务器公钥」(证书的公钥)加密这个「对称密钥」,发送给服务器。

⑤ 服务器通过自己的私钥对信息解密,至此得到了「对称密钥」,此时两者都拥有了相同的「对称密钥」。

4. 恢复断开的 SSL 连接

有如下两种方法来恢复断开的 SSL 连接:

session ID

服务端在每次的会话都会生成一个编号 session ID 返回给客户端。当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成一把。目前所有的浏览器都支持这种方法。但是这种方法有一个缺点是,session ID 只能够存在一台服务器上,如果我们的请求通过负载平衡被转移到了其他的服务器上,那么就无法恢复对话。

session ticket

服务器在上一次对话中会发送给客户 session ticket,这个 ticket 是加密的,只有服务器能够解密,里面包含了本次会话的信息,比如对话秘钥和加密方法等。这样不管我们的请求是否转移到其他的服务器上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。

(五)HTTP 杂项

1. HTTP2 相较 1.X 的优势

二进制分帧

  • 帧:HTTP/2 数据通信的最小单位
  • 消息:HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由⼀个或多个帧组成
  • 流:存在于连接中的⼀个虚拟通道。流可以承载双向消息,每个流都有⼀个唯⼀的整数 ID

HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。

头部压缩

HTTP/1.x 会在请求和响应中重复携带很少改变的冗长头部数据,给网络带来额外的负担。而 HTTP/2 在客户端和服务器端使用首部表来跟踪和存储之前发送的键值对,只发送差异数据而非全部发送,从而减少头部的信息量。首部表在 HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新。每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。

image-20210411105545823

服务器推送

服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 时再发送这些请求。

服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。

多路复用

HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 连接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 连接请求限制。而 HTTP2 中,同域名下所有通信都在单个连接上完成。单个连接可以承载任意数量的双向数据流。数据流以消息的形式发送,而消息又由⼀个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

2. Web 即时通讯技术

借助即时通讯技术,服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能。但在 Web 中,由于浏览器的限制,实现即时通讯需要借助一些方法。这种限制出现的主要原因是,一般的 Web 通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的更新。

实现 Web 端即时通讯的方法:实现即时通讯主要有四种方式,它们大体可以分为两类,一种是在 HTTP 基础上实现的,如短轮询、长轮询(comet)和长连接(SSE);另一种不是在 HTTP 基础上实现的 WebSocket。

短轮询

浏览器每隔一段时间向服务器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。

短轮询比较简单,易于理解,但这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。

长轮询

首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。

长轮询和短轮询比起来,它明显减少了很多不必要的 http 请求次数,因而节约了资源。但连接挂起也会导致资源的浪费。

WebSocket

WebSocket 是 Html5 定义的一个新协议,与传统的 http 协议不同,该协议允许由服务器主动向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,即通信双方是平等的,可以相互发送消息。

3. 正/反向代理

正向代理

我们常说的代理也就是指正向代理。正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。

反向代理

这种代理模式下,它隐藏了真实的服务端,当我们向一个网站发起请求的时候,背后可能有成千上万台服务器为我们服务,具体是哪一台我们不清楚,只需要知道反向代理服务器是谁就行,而且反向代理服务器会帮我们把请求转发到真实的服务器那里去,一般而言反向代理服务器一般用来实现负载平衡。

4. 负载平衡的实现

  • 使用反向代理:用户的请求都发送到反向代理服务器上,然后由反向代理服务器来转发请求到真实的服务器上,以此来实现集群的负载平衡。
  • DNS:DNS 可以用于在冗余的服务器上实现负载平衡。现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户向网站域名发送请求时,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。但由于 DNS 服务器中存在缓存,所以有可能一个服务器出现故障后,域名解析仍然返回的是那个 IP 地址,就会造成访问的问题。

5. 队头阻塞问题

所有 HTTP 请求任务都会被放入一个任务队列中串行执行,一旦队首任务请求太慢,就会阻塞后面的请求处理,这就是 HTTP 队头阻塞问题。解决方案如下:

并发连接

我们知道对于一个域名而言,是允许分配多个长连接的,可以理解成增加了任务队列,这样就不会导致一个任务阻塞了该任务队列的其他任务,在 RFC 规范中规定客户端最多并发 2 个连接,不过实际情况要更多,例如 Chrome 中是 6 个。

域名分片

我们可以在一个域名下分出多个二级域名出来,而它们最终指向的还是同一个服务器,由此可以并发处理的任务队列更多,也更好地解决了队头阻塞的问题。

比如 TianTian.com,可以分出很多二级域名,比如 Day1.TianTian.comDay2.TianTian.comDay3.TianTian.com

6. HTTP 跟 TCP 的关系

网络由下往上分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层(OSI)。IP 协议对应网络层,TCP 协议对应传输层,而 HTTP 协议对应于应用层,三者从本质上来说没有可比性,socket 则是对 TCP/IP 协议的封装和应用 (程序员层面上)。TCP/IP 协议是传输层协议,主要解决数据如何在网络中传输,而 HTTP 是应用层协议,主要解决如何包装数据。

至于 TCP/IP 和 HTTP 协议的关系,在传输数据时可以只使用(传输层)TCP/IP 协议,但如果没有应用层,便无法识别数据内容。如果想要使传输的数据有意义,则必须使用到应用层协议。应用层协议有很多,比如 HTTP、FTP、TELNET 等,也可以自定义应用层协议。WEB 使用 HTTP 协议作应用层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议将它发到网络上。

http 的本质就是 tcp/ip 请求,tcp 将 http 长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输。