HTTP

MIME type

数据类型表示实体数据的内容是什么,使用的是 MIME type,相关的头字段是 Accept 和 Content-Type;

Accept 字段标记的是客户端可理解的 MIME type

相应的,服务器会在响应报文里用头字段 Content-Type 告诉实体数据的真实类型

有了 MIME type 和 Encoding type,无论是浏览器还是服务器就都可以轻松识别出 body 的类型,也就能够正确处理数据了。

以为为常见的MIME type:

  1. text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。

  2. image:即图像文件,有 image/gif、image/jpeg、image/png 等。

  3. audio/video:音频和视频数据,例如 audio/mpeg、video/mp4 等。

  4. application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json,application/javascript、application/pdf 等,另外,如果实在是不知道数据是什么类型,需要客户端猜的,就会是 application/octet-stream,即不透明的二进制数据。

Encoding type

数据编码表示实体数据的压缩方式,相关的头字段是 Accept-Encoding 和 Content-Encoding;

常用的只有下面三种:

  1. gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;

  2. deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;

  3. br:一种专门为 HTTP 优化的新压缩算法(Brotli)。

分块传输

当传输大内容时使用只使用一次的请求应答模式会导致响应时间过长,这是很影响性能的,那么如何处理这样的问题呢?

那就把它“拆开”,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。

这种“化整为零”的思路在 HTTP 协议里就是“chunked”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块逐个发送。

“Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)。

范围请求

如果使用范围请求,需要服务器在响应头里使用字段“Accept-Ranges: bytes”明确告知客户端:“我是支持范围请求的”。

如果已经确定双端都支持范围请求,我们就可以在请求资源的时候使用它。

HTTP/1.1 中定义了一个 Ranges 的请求头,来指定请求实体的范围。它的范围取值是在 0 - Content-Length 之间,使用 - 分割。。

服务器收到 Range 字段后,需要做以下操作:

  1. 它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码 416,意思是“你的范围请求有误,我无法处理,请再检查一下”。

  2. 如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”,和 200 的意思差不多,但表示 body 只是原数据的一部分。

  3. 服务器要添加一个响应头字段 Content-Range,告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。

Cookie 是服务器委托浏览器存储的一些数据,让服务器有了“记忆能力”,每次的传输内容都会带上 cookie 信息,所以尽量不要让自己服务器以外的人看到。

为了保护 Cookie,还要给它设置有效期、作用域等属性,常用的有 Max-Age、Expires、Domain、HttpOnly 、SameSite 等。

Cookie 最基本的一个用途就是身份识别,保存用户的登录信息,实现会话事务。

比如,你用账号和密码登录某电商,登录成功后网站服务器就会发给浏览器一个 Cookie,内容大概是“name=yourid”,这样就成功地把身份标签贴在了你身上,实现了“状态保持”。

Cookie 的另一个常见用途是广告跟踪。

你上网的时候肯定看过很多的广告图片,这些图片背后都是广告商网站(例如 Google),它会“偷偷地”给你贴上 Cookie 小纸条,这样你上其他的网站,别的广告就能用 Cookie 读出你的身份,然后做行为分析,再推给你广告。

这种 Cookie 不是由访问的主站存储的,所以又叫“第三方 Cookie”。如果广告商势力很大,广告到处都是,那么就比较“恐怖”了,无论你走到哪里它都会通过 Cookie 认出你来,实现广告“精准打击”,但也是最容易泄露隐私。

HTTPS

因为 http 的不安全性,才有 https 的出现。

通信安全必须同时具备机密性、完整性、身份认证和不可否认这四个特性,而 https 为 HTTP 增加了这四大安全特性。

HTTPS 默认端口号是 443,除了协议名“http”和端口号 80 这两点不同,HTTPS 协议在语法、语义上和 HTTP 完全一样,优缺点也“照单全收”(当然要除去“明文”和“不安全”)。

HTTPS 与 HTTP 最大的区别,它能够鉴别危险的网站,并且尽最大可能保证你的上网安全,防御黑客对信息的窃听、篡改或者“钓鱼”、伪造。

而 HTTPS 把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS,让 HTTP 运行在了安全的 SSL/TLS 协议上,其实就是在 TCP/IP 与 HTTP 之间添加一层 SSL/TLS。

HTTP2/3

阐述

当 HTTP 到 HTTPS 后,在安全上已经有保障了,但本来性能一般般的 HTTP 又加上一层处理,变得更加重,在性能上有所损耗,那么成熟后的 HTTPS 已经在性能暴露了问题,那么只能对其性能方面下手,所以就出现后续的 HTTP2/3。

而为什么 HTTP 在后续的版本再也没有出现过小版本?

以前的“1.0”“1.1”造成了很多的混乱和误解,让人在实际的使用中难以区分差异,所以就决定 HTTP 协议不再使用小版本号,只使用大版本号,从今往后 HTTP 协议不会出现 HTTP/2.0、2.1,只会有“HTTP/2”“HTTP/3”。

这样就可以明确无误地辨别出协议版本的“跃进程度”,让协议在一段较长的时期内保持稳定,每当发布新版本的 HTTP 协议都会有本质的不同,绝不会有“零敲碎打”的小改良。

HTTP/2

保持功能上的兼容,完全兼容 HTTP1,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。

特别要说的是,与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用“http”表示明文协议,用“https”表示加密协议。

可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡。

但 HTTP/2 的内容对于 HTTP/1 有着“天翻地覆”的改造

头部压缩

由于 HTTP1 没有针对头部的优化手段,头部还有优化的空间,不过 HTTP/2 并没有使用传统的压缩算法,而是开发了专门的“HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

二进制格式

相对于 HTTP/1 里纯文本形式的报文,HTTP/2 采用二进制格式

二进制里只有“0”和“1”,可以严格规定字段大小、顺序、标志位等格式,“对就是对,错就是错”,解析起来没有歧义,实现简单,而且体积小、速度快,做到“内部提效”。

虚拟的“流”

HTTP/2 为此定义了一个“流”的概念,它是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID。你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。

因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”——多个往返通信都复用一个连接来处理。

在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。

多路复用

多路复用多个请求没有顺序,而长连接多个请求必须排队,就会队头阻塞。

http 协议要求请求-响应必须一来一回,上一个请求没有处理完,下一个请求是不能发出去的。一个 tcp 连接上的 http 请求必然是串行。

管道模式可以顺序发出多个请求,但响应也必须顺序响应。这些都是 http/1.1 里规定的。

再对比 http/2,一个 tcp 连接里有多个流,每个流就是一个请求,所以多个请求可以并发,“复用”在了一个连接里。

http/3

HTTP/2 虽然使用“帧”“流”“多路复用”,没有了“队头阻塞”,但这些手段都是在应用层里,而在下层,也就是 TCP 协议里,还是会发生“队头阻塞”。

由于这种“队头阻塞”是 TCP 协议固有的,所以 HTTP/2 即使设计出再多的“花样”也无法解决。

Google 在推 SPDY 的时候就已经意识到了这个问题,于是就又发明了一个新的“QUIC”协议,让 HTTP 跑在 QUIC 上而不是 TCP 上。

QUIC 选择使用 UDP 为基础,因为 UDP 是无序的,包之间没有依赖关系,所以就从根本上解决了“队头阻塞”。

QUIC 在 UDP 上把 TCP 的那一套连接管理、拥塞窗口、流量控制等“搬”了过来,“去其糟粕,取其精华”,打造出了一个全新的可靠传输协议,可以认为是“新时代的 TCP”。

QUIC 基于 UDP,而 UDP 是“无连接”的,根本就不需要“握手”和“挥手”,所以天生就要比 TCP 快。

webSocket

其实 WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。

“请求 - 应答”是一种“半双工”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

“请求 - 应答”模式,导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域。

为了克服 HTTP“请求 - 应答”模式的缺点,WebSocket 就“应运而生”了。

WebSocket 是一个真正“全双工”的通信协议,与 TCP 一样,客户端和服务器都可以随时向对方发送数据,而不用像 HTTP“你拍一,我拍一”那么“客套”。于是,服务器就可以变得更加“主动”了。一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。

WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443;

总结

http/1 请求-应答都是有序并且每次都需要建立连接,而后 http/1.1 出现长连接与管道模式可以让连接维持不断并且多个请求有序的同时发送等待响应,但响应部分还是需要排队等待一个一个返回,而 HTTP/2 出现就是主要是压缩头部信息与数据格式从文本改为二进制,让数据的体积变小,而且让应用层上连接的有序变为无序,解决了连接层上队头阻塞,而 HTTP/3 的出现主要解决了 TCP 的传输层上队头阻塞问题,把 TCP 换为 UDP 无序的、包之间没有依赖更快传输协议,并改造为可靠的传输协议。

而使用 HTTP2/3 就没有必要使用精灵图、多个请求合并的操作,因为这些技术出现都是为了解决对头阻塞导致的请求响应慢的问题,而且还不能很好的使用 HTTP 的缓存机制,在 HTTP2/3 中这些技术反而会拖慢请求响应的速度。