欢迎大家来到IT世界,在知识的湖畔探索吧!
HTTP1 的优缺点
如今的互联网世界是离不开设计的极为成功的 HTTP 协议的,这得益于最初对 HTTP 协议的 5 个精准定位。
- 基于 TCP 连接,所以无需关注如何可靠的传输变长的消息。
- 采用了请求响应模式,Client 与 Server 开发都简单。
- 低门槛的 ASCll 编码开发运维都容易上手。
- 无状态设计带来了优秀的 scalability,使得服务端可以轻松的实现高可用。
- 将信息安全交由低一层的TLS协议来解决,即可变成安全的HTTPS,应用无需改动就能实现加密和认证。
其实这五个优点也是 REST 架构能够成功的关键。
然而随着网速的提升需求场景也发生了很大的变化,一条消息的大小从最初几K增长为几兆,每个页面从小于 10 个资源到现在几百个资源,对页面内容的实时性要求也变得越来越高,进而这五个特点带来了新的问题。
- TCP 协议不关注消息的边界,因此需要应用成自己封装每一条消息再传输,HTTP 1.1 采用了 \r\n 的方式来切分消息,这使得接收端必须使用计算量更大的状态机来解析消息。
GET / HTTP/1.1 \r\n Host: www.taohui.publ\r\n \r\n
欢迎大家来到IT世界,在知识的湖畔探索吧!
- 请求响应模式,对 TCP 连接上消息的传输利用率是极低的,即便使用 KeepAlive 长连接也改变不了连接的每一端总有一段时间在等待对方发送完消息,并没有充分利用 TCP 协议全双工双向传输的特性。
- 编码方式虽然可读性很高,但编码效率与传输效率都是非常低的;就像时间、数字原本可以用四个字节表示,但是要用几倍的空间来表达。在 HTTP1 协议中,所有的头部字段(Header)值,包括可能包含的数字,都是作为字符串来处理和传输的。这是因为HTTP协议本身的设计规定了报文头的字段名和字段值都必须是可打印的 ASCII 字符序列。例如,如果你在HTTP请求或响应中设置了一个包含数字的Header: Content-Length: 1024
这里的”1024″虽然是一个数值,但在HTTP协议层面,它被当作一个文本字符串进行处理和解析。
- 无状态特性使得每次请求响应都需要重复发送不变的 User-Agent、Server 等头部。
- 允许直接运行在 TCP 协议之上的 HTTP 工作,HTTP1 协议本身并不提供任何形式的数据加密和身份验证机制,因此在数据传输安全方面存在风险。通过 HTTP1 协议传输的数据是明文的,容易被第三方监听、篡改或伪造。
与此同时虽然网络带宽越来越大,但由于“最后一公里”问题网络时延下降缓慢,所以我们传递消息的速度仍然有上限;同时浏览器访问一个页面最多只能同时使用六个并发连接这与速度上限一起限制了我们的传输效率。那么前面讲到的这五个问题该怎么解决呢?
HTTP2 是如何解决的
多路复用
首先是多路复用技术,在长肥网络中带宽越来越大而时延也居高不下,此时理想中的消息传递方式应该像下图中这样,只要在 TCP 滑动窗口和拥塞窗口的处理范围内,发送端就应当源源不断的发送,接收端则源源不断的接收。
那 HTTP2 是怎么做到多路复用的,但是上层业务不需要改代码呢?一切都是因为引入了一个二进制分帧层,并且HTTP2 的新特性也都是建立在这个基础之上的。这又印证了那句话,“计算机中任何问题都可以通过增加一个中间层来解决“。
如上图,HTTP2 在应用层(HTTP)和传输层(TCP或者TLS)之间增加一个二进制分帧层,HTTP2 的通信都在一个 TCP 连接上完成,这个连接可以承载任意数量的双向数据流,相应的每个数据流以消息的形式发送。而消息由一或多个帧组成,这些帧可以乱序发送,然后根据每个帧首部的流标识符重新组装。
这是一张描述连接(connection)、流(stream)、消息(message)、帧(frame)关系的图,这里解释下这几个概念。
- 帧(Frame):
- 帧是HTTP2 通信的最小单位
- 请求和响应都分为首部帧和消息帧单独传输,除此之外还有一些其他的类型的帧,下面会介绍帧结构的时候会说。
- 消息(Message):
- 指逻辑上的HTTP消息,比如一次请求、一次响应等。
- 由一个或多个帧组成,以二进制压缩格式存放 HTTP1 中的头部。
- 流(Stream):
- 是一条逻辑上的连接,是TCP连接中的一个虚拟通道(每个TCP 连接会建立多个 Stream)。
- 可以承载双向的消息,每个流都有一个唯一的整数标识符,帧会记录 Stream 的ID。
- 不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息。同一 Stream 内部的帧必须是严格有序的。
- 客户端和服务器双方都可以建立 Stream, Stream ID 也是有区别的,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。
- 同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接
- 连接(Connection(连接)):
- 即TCP连接
这种在一个TCP连接中划分多个逻辑连接,把所有两端的流量都集中在一个 TCP 连接的做法,减少了服务端的压力,虽然流量还是这么多流量,但是创建的 socket 会明显减少,内存占用更少,每个连接吞吐量更大。并且 HTTP 性能优化的关键并不在于高带宽,而是低延迟,这种做法避免了TCP的慢启动,能更有效的利用到TCP的流控算法。
有了多路复用技术后传输更多的对象时传输效率会得到很大的提升,当访问 /index.html 首页时,从 XML 树中解析出超链接资源后可以在同一个 TCP 连接下并行发起多个请求;这样就减少了 TCP 建立连接时的三次握手的消耗,也降低了 TCP 的拥塞控制中的慢启动消耗。
多条 Stream 流可以在一条 TCP 连接上并行传输 HTTP 消息,同一条 Stream 流上可以由一个或者多个 HTTP 消息构成,只是这些消息间必须串行传输。
二进制传输数据
在上面的讲解中我们已经知道了一个 HTTP 消息由多个 Frame 帧构成 HTTP1 的消息格式是靠 \r\n 来分割消息的,它的传输效率和装卸效率都很低,HTTP2 中直接改为有二进制的 Frame 帧来承载消息,这样通过帧头的 length 字段就可以轻松的分割 TCP 流承载的消息了。实际上 HTTP 消息分为 Headers 帧和 Data 帧,其中 Headers 帧承载着 start line 和 http header 两个部分,而 Data 帧则承载着 HTTP 的 Body。
让我们再来看一下帧的结构,帧由帧头(Frame Header)和帧负载(Frame Payload)构成,如上是帧头的结构,帧头总共9个字节,包括帧长度、帧类型、标识位、保留位和流标识符。
其中帧长度为3个字节,表示帧负载(Frame Playload)的长度(不包括帧头9字节),这个长度的最大值通过 SETTING 类型的帧中携带的 SETTINGS_MAX_FRAME_SIZE 参数来设置。假如一个 HEADERS 帧不够传输所有的 HEADER 字段,则会把剩余的部分放到后续的 CONTINUATION 类型的帧来传输。
帧类型,1个字节,表示帧的类型,目前HTTP2 总共定义了 10 种类型的帧。
帧类型类型编码用途DATA0x0传递HTTP包体HEADERS0x1传递HTTP头部PRIORITY0x2指定STREAM流的优先级RST_STREAM0x3终止STREAM流SETTINGS0x4初始化或修改连接或者STREAM流的配置PUSH_PROMISE0x5服务端推送资源时描述请求的帧PING0x6探活、兼具计算RTT往返时间GOAWAY0x7优雅终止连接并通知错误WINDOW_UPDATE0x8用于流控,指定窗口大小的包CONTINUATION0x9传递较大HTTP头部的持续帧
TCP 本身有探活的机制,为什么 HTTP2 里又要重新定义一个 PING 的帧呢?因为 TCP 协议栈其实是内核里实现的,收到包后会自动 ack 把数据缓存到内核缓存区里,然后再复制到应用程序的进程空间,所以其实 TCP 的探活不能反映应用程序是否正常,可能应用程序就是因为某些原因卡住了,但是进程又没挂,ack 还是正常的。所以在应用层实现一种探活机制是很有必要的。
头部压缩
Headers 帧在 HTTP 1.1 中广受诟病,它的消息传输效率很低,HTTP2 通过 HPACK 头部压缩技术解决了这一问题,它包括静态表、动态表和 Huffman 编码三种技术,下面我作为一一跟你讲解。
静态表
由于许多 HTTP 头部的名字甚至值都是固定的,因此用固定的单字节整数来表示多字节的字符串就是非常划算的事了,HPACK 静态表就是这样的设计,目前它包括 61 个表项表项其中 14 个表项同时含有有名称与值,其余 47 项仅含有名称;他们的值将直接使用 ASCll 编码,或者使用静态 Huffman 编码进行传输。
动态表
其他 HTTP 头部虽然没有静态表中的像那么常见,但由于完成 web 站点的展示需要先后成百上千个请求的协作,而许多请求都在重复传输相同的头部名称甚至是值,这些头部可以在第一次传输完成后给一个数字编号,后续传输时仅传输该数字编号就可以了。
HPACK 动态表就是基于前面时间轴上的增量变化实现了高效的信息压缩,使用一个动态表来存储不常见的头部信息,这些信息会在通信过程中动态添加到表中,并且可以通过索引值进行引用,从而减少重复传输。
以常用的 User-Agent 为例,它在静态表中的索引值是 58,它的值是不存在表中的,因为它的值是多变的。第一次请求的时候它的 key 用 58 表示,表示这是一个 User-Agent ,它的值部分会进行霍夫曼编码(如果编码后的字符串变更长了,则不采用霍夫曼编码)。服务端收到请求后,会将这个 User-Agent 添加到 动态表缓存起来,分配一个新的索引值。客户端下一次请求时,假设上次请求 User-Agent 的在表中的索引位置是 62, 此时只需要发送 0xBE,便可以代表:User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36。
为什么是 0xBE,而不是 0x3E?这是因为高位设置为 1 表示这个字节是一个完全索引值(key 和 value 都在索引表中)。类似的,通过高位的标志位可以区分出这个字节是属于一个完全索引值,还是仅索引了 key,还是 key 和 value 都没有索引。
霍夫曼编码
HPACK 不仅仅通过索引键值对来降低数据量,同时还会将字符串进行霍夫曼编码来压缩字符串大小。它的原理是出现概率较大的符号采用较短的编码,出现概率较小的符号采用较长的编码;通过这样做来减少编码后的占用空间。
比如 FORGET 这 6 个字符假设它在这里出现的概率分别是 2、3、4、4、5、7,你可以根据这个构造 Huffman 树。构造方法就是将出现频率最小的两个字母相加构成指数,左节点的频率小,右节点的频率大,重复这样一个步骤就可以构造出完整的 Huffman 树
比如第一步将出现概率最小的 F 与 O 构成指数,概率较小的 F 在左,其父节点概率为 5;第二步再将剩余概率最小的 R 与 G 构成指数,其父节点概率为 8;第三步将概率为 5 的 E 与第一步生成的指数合并;第四步将概率为 7 的 T,与第二步生成的指数合并;第五步将第三步生成的概率为 10 的指数与第四步生成的概率为 15 的指数合并;接下来我们把数中的左链接编码为 0,右链接编码为 1,这样 Huffman 树就构造完成了。
从根节点到所在叶子节点所有链接的编码就是每个字母的编码,比如出现概率最大的 T 它的编码是较短的 11,而出现概率最小的 O 编码是较长的 000.
当我们统计大量 HTTP 保文头部得到各种字母、数字、符号出现的概率,并基于前面讲到的 Huffman 算法对他们进行编码就得到了静态 Huffman 编码表,从这里可以看到最常出现了小写字母 a c e i o 以及数字中的 1 2 3 等只用了五个二进制编码,而不常见的字符则使用了十多个二进制进行编码,每个 ASCll 字符占用八个二进制,因此这里的静态 Huffman 编码最高压缩比是 5/8。
我们来看一个具体的例子 ,if-none-match 头部对应的值是 5cb816f5-19d8,它编码好的二进制如下图显示的,原本 16 字节 128 位被压缩为只有 96 位,你可以根据静态 Huffman 编码从二进制反推出原先的 ASCll 码。
安全问题
我们再来看网络安全性,由于 HTTP1 并没有要求实现方必须基于 TLS 协议传输消息,因此许多未使用 HTTPS 的网站存在着安全隐患。HTTP2 的 RFC 规范明确定义了两种从 HTTP1 升级到 HTTP2 的方式,既允许 HTTP2 运行在 TLS 协议之上缩写为 H2,也允许直接运行在 TCP 协议之上缩写为 H2C。
其实这两种方式的握手并不相同,H2C 类似于 WebSocket 协议的升级,H2 却是把 HTTP2 的握手内置到了 TLS 的握手中主要集成在 ALPN 扩展里,目前所有实现 HTTP2 的浏览器都只支持更安全的 H2C,所以公网上的 HTTP2 浏览几乎都是运行在 TLS 之上的。
从最新的 TLS1.3 后包含了 HTTP2 的握手也只需要一个 RTT,它提供了一次性的对称密钥这给我们的服务带来很高的安全性。
HTTP2 的其它优点
请求优先级
将 HTTP 消息分解为很多独立的帧之后,我们就可以复用一个连接,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。而对于浏览器来说,这是一种用于提升浏览性能的关键功能,网络中拥有多种资源类型,它们的依赖关系和权重各不相同,比如可以定义传输 HTML 的流的优先级比传输图片文件的流的优先级更高,来让服务端优先传输 HTML。
如上图,HTTP2 定义了每个流的依赖关系,依赖于另一个流的流是依赖流,被依赖的是父流。兄弟节点则会定一个一个权重值,这个权重是个介于 1 至 256 之间的整数,兄弟节点依靠权重来分配资源。这个模型构建了一个虚拟的流,作为所有流的祖先节点,是这个依赖树的根节点(其实就是 id 为 0 的流,当然这个流是不存在的)。流节点不能依赖自身。
请求先级这个特性主要还是客户端用来调整服务端发送资源的顺序,比如浏览器建议服务端返回资源的优先级。但是,请求优先级只是一个建议,而不是要求,更不是保证,流优先级不保证流相对于任何其他流的任何特定处理或传输顺序。也就是说,客户端定义了流优先级,并把这个信息通过 HEADERS 或者 PRIORITY 帧传输给服务端,但是服务端会不会按照这个定义的优先级顺序来进行处理和返回,则完全没有保证,可能服务端没有实现任何和流优先级相关的功能,也可能实现了,一旦服务端实现了,就可以利用客户端给予的信息来做一些优化。
服务端推送
服务器推送这个机制允许服务端主动向客户端在新开的流上推送一些资源,典型的场景是浏览器请求一个 html 页面,html 里会引用 css 和相关图片,当服务端收到 html 页面的请求时,除了返回 html 文件,还会直接把相关的 css 和图片文件都返回,这样浏览器就不用请求多次再来获取 html 页面里引用的资源了。服务器推送在语义上等同于服务器响应一个请求,为了不改变 HTTP 的语义怎么来做呢,服务端会先返回一个帧作为请求,然后在一个新打开的流上,返回一个帧作为响应,大概过程是:
- Push Requests:当收到一个客户端请求时,服务端决定推送一些资源,此时服务端会发送一个 PUSH_PROMISE 帧给客户端,这个 PUSH_PROMISE 帧包含了后面返回的响应帧所在的流的 id,还包含了响应的 header 的信息。客户端在接收到 PUSH_PROMISE 帧后,它可以根据自身情况选择拒绝响应流(发送 RST_STREAM 帧),例如,如果资源已经位于缓存中,便可能会发生这种情况。同时这个 PUSH_PROMISE 在语义上视作客户端对服务端的一个请求。
- Push Responses:发送 PUSH_PROMISE 帧后,服务端会直接发送数据帧到之前 PUSH_PROMISE 承诺的流上,作为响应,(因为 header 已经在 PUSH_PROMISE 传输了,这里只要传输 body 即可)也就是这里的数据帧在语义上相当于对 PUSH_PROMISE 帧的一个响应。
服务端推送只能由服务端进行,客户端(发起连接的一方)无法推送,也就是只能是单向的。所以在第一眼看到服务端推送这个功能时,还以为和我们常规理解的服务端推送技术一样,还想着是否在 HTTP/2 时代就不需要 websocket 了,现在看来还是不太一样,正是因为这个推送是单向的,所以还是受到了很大的限制,场景也比较有限。
流量控制
由于 HTTP2 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。为了解决这一问题,HTTP2 提供了一组简单的机制,允许客户端和服务器实现其自己的数据流和连接级流控制。
流量控制基于 WINDOW_UPDATE 帧,发送者通告他们准备在 Stream 流,或者整个连接上接收多少个八位字节,流量控制是定向的,发送者提供信息,接收者控制自己的速度。对于一个新的 Stream 流和整个连接,流量控制窗口的初始值为 65535 个八位字节。并且只有 DATA 帧才会受到流控的限制,因为其他帧都不大,而且也比较重要,这确保了重要的控制帧不会被流量控制阻挡。无法禁用流量控制。建立 HTTP2 连接后,客户端将与服务器交换 SETTINGS 帧,这会在两个方向上设置流控制窗口
HTTP2 仅定义 WINDOW_UPDATE 帧的格式和语义。未规定发送方何时发送此帧或其发送的值,也未规定接受方如何选择发送数据包。实现方能够选择任何适合其需求的算法,也就是 HTTP2 的流量控制只是定义了流控信息的交换格式,至于怎么利用这些信息来进行流控,客户端和服务端可以有不同的实现
看到 HTTP2 的快
这里我们用 Chrome 浏览器打开 http://www.http2demo.io/ 这个网站,这个时候会使用 HTTP1.1 加载图片,这张图片其实是 178 小图片拼接成的,当使用 HTTP1.1 协议串行访问时总共消耗了 28.03 秒,这时再点击 Run HTTP/2 test 按钮;然后看到使用 HTTP2 协议访问则只消耗了 4.4 秒。加载速度相差 7 倍!!!
我们再使用 Chrome 开发者工具的 Network 面板抓包,再次重复上述步骤访问这个页面,可以看到对于 HTTP1 协议浏览器一共打开了 6 个并发的 TCP 连接,每个连接上有几十个请求串行的执行,而 HTTP2 则在一个 TCP 连接上采用多路复用快速访问,在稳定的 TCP 连接上这一过程其实是非常高效的。
HTTP2 的不足
HTTP2 虽然有了很大的性能提升,但是由于它基于 TCP 协议,TCP 是一个面向字节流传输的协议,它强制每一个字节都得按序到达;比如图中有红绿蓝三条消息在并发传输,在 TCP 层认为字节必须串行到达,所以如果排在最前方的红色的消息如果丢失了,那么内核的 TCP 模块将会阻塞应用进程,无法接收处理后面的绿色和蓝色的消息,而原本蓝色和绿色的消息其实是可以提前处理的。
HTTP3 就解决了这个问题,HTTP3 不再基于 TCP 协议运行,而是直接在 UDP 协议中运行,这样它需要把 TLS 拥塞控制等都实现在应用程序中,这也摆脱了依托内核的 TCP 模块总是难以升级的问题,当然,HTTP3 由于强制使用更先进的 TLS 1.3 协议因此它的性能和安全性也会更好。
总结
HTTP/1.1协议虽然在互联网早期表现出色,但随着网络速度和交互复杂度的提升,出现了连接利用率低、头部冗余、并发性能受限等问题。HTTP2 通过引入多路复用技术,在单一 TCP 连接上实现多个数据流并行处理,并采用二进制分帧机制提高传输效率,同时利用 HPACK 头部压缩算法减少重复传输,包括静态表和动态表对常见及非固定头部进行索引引用,结合霍夫曼编码进一步压缩数据量。此外,HTTP2 要求与更安全的 TLS 协议结合使用,简化握手过程,并支持请求优先级管理,显著提升了网络性能和用户体验。
尽管HTTP2带来了显著的性能提升,但它仍然基于 TCP 协议,这意味着在数据传输中存在一定的局限性。文章最后指出,HTTP3 协议通过在 UDP 协议上运行并集成 TLS 1.3 ,解决了HTTP2的这些问题,提供了更高的性能和安全性。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/98624.html