- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在前面的章节,我们把HTTP/1.1的大部分核心内容都过了一遍,并且给出了基于Node环境的一部分示例代码,想必大家对HTTP/1.1已经不再陌生,那么HTTP/1.1的学习基本上就结束了。这两篇文章,我会和大家一起,学习一下HTTP/2和HTTP/3.
还记得我们在之前的时间回溯那篇文章里,简单的聊过HTTP/2和HTTP/3,是为了提升HTTP/1.1所存在的性能问题的,这篇文章我们先来看看HTTP/2带来了哪些性能上的改进和提升。下一篇我们再来学习HTTP/3的性能优化.
不知道大家在第一次接触HTTP/2、HTTP/3这样的名字的时候会不会有些诧异?怎么不是HTTP/2.0、HTTP/3.0呢?针对这个问题,HTTP/2的工作组给出了官方的回答。他们认为以前的“1.0”“1.1”造成了很多的混乱和误解,让人在实际的使用中难以区分差异,所以就决定 HTTP 协议不再使用小版本号(minor version),只使用大版本号(major version),从今往后 HTTP 协议不会出现 HTTP/2.0、2.1,只会有“HTTP/2”“HTTP/3”……这样就可以明确无误地辨别出协议版本的“跃进程度”,让协议在一段较长的时期内保持稳定,每当发布新版本的 HTTP 协议都会有本质的不同,绝不会有“零敲碎打”的小改良.
当我们在实际工作中想要开发基于之前版本的新版本代码时,第一个想到的问题就是兼容,我要如何兼容以前的代码,使得使用旧版本的用户也可以尽可能无感的切换到新版本,享受新版本带来的丝滑感受。HTTP/2也是如此,它在背负众多期待的同时,也背负了HTTP/1庞大的历史包袱,所以协议的修改就必须要考虑如何兼容HTTP/1,否则就会破坏互联网上无数现有的资产,这肯定不是大家想要看到的。那HTTP/2是怎么做的呢?
HTTP/2把HTTP分解成了“语法”和“语义”两部分,语法层面不做改动,与HTTP/1也就是RFC7231完全一致。比如请求方法、URI、状态码、头字段等都保持不变,这样就消除了再学习的成本,基于HTTP的上层不需要任何的改动,可以无缝转换到HTTP/2.
特别要说的是,HTTP/2没有再URI里引入新的协议名,仍然用“http”表示明文协议,用“https”表示加密协议。这是一个非常了不起的决定,可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡.
在“语义”保持稳定之后,HTTP/2 在“语法”层做了“天翻地覆”的改造,完全变更了 HTTP 报文的传输格式.
首先,为啥要对头部进行压缩呢?假设这样一种场景,一个GET请求,返回的body十分简单啊,可能就是个简单的文本,几十个字节。但是头字段却又几百个,限制的十分严谨细腻,而这样的请求在整个系统项目中又应用的十分频繁,成了不折不扣的“大头儿子”。更要命的是,这些报文的传输中,大部分的头字段都是一样的。再者,HTTP针对body有很多优化的手段,却对Header一点优化都没有.
基于以上的这些原因,为了优化“长尾效应”导致大量的带宽消耗在这了这些冗余度极高的数据上的情况,HTTP/2就把头部压缩作为性能改进的一个重点,优化的方式,就是压缩。但是HTTP/2的头部压缩并不是想body那样的压缩手段,而是专门开发了“HPACK”算法,在客户端和服务器端建立“字典”,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率.
由于HTTP/2在语义上与HTTP/1兼容,所以报文还是Header+Body的形式,但是在请求发送前,必须要用“HPACK”算法来压缩头部数据.
“HPACK”算法是专门为压缩HTTP头部定制的算法,与gzip、zlib等压缩算法不同,它是一个“有状态”的算法,需要客户端和服务器都维护一份“索引表”,也可以说是字典,压缩和解压缩就是查表和更新表的操作.
为了方便管理和压缩,HTTP/2废除了原有的起始行的概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,并且给这些“不是头字段的头字段”起了个特别的名字——“伪头字段”。而起始行里面的版本号和错误短语因为没啥大用,就给废弃了.
为了与“真头字段”区分开,这些伪头字段会在名字前面加上一个“:”,比如“:authority”、“:method”、“:status”,分别表示的是域名、请求方法和状态码。现在 HTTP 报文头就简单了,全都是“Key-Value”形式的字段,于是 HTTP/2 就为一些最常用的头字段定义了一个只读的“ 静态表 ”(Static Table).
下面的这个表格列出了“静态表”的一部分,这样只要查表就可以知道字段名和对应的值,比如数字“2”代表“GET”,数字“8”代表状态码 200.
但如果表里只有 Key 没有 Value,或者是自定义字段根本找不到该怎么办呢?这就要用到“ 动态表 ”(Dynamic Table),它添加在静态表后面,结构相同,但会在编码解码的时候随时更新.
比如说,第一次发送请求时的“user-agent”字段长是一百多个字节,用哈夫曼压缩编码发送之后,客户端和服务器都更新自己的动态表,添加一个新的索引号“65”。那么下一次发送的时候就不用再重复发那么多字节了,只要用一个字节发送编号就好.
你可以想象得出来,随着在 HTTP/2 连接上发送的报文越来越多,两边的“字典”也会越来越丰富,最终每次的头部字段都会变成一两个字节的代码,原来上千字节的头用几十个字节就可以表示了,压缩效果比 gzip 要好得多.
大家知道HTTP/1是纯文本形式的报文,它的优点就是对人友好,一目了然,用最简单的工具,甚至不用工具就可以开发调试,非常方便.
但是HTTP/2改变了延续十多年的现状,不再使用肉眼可见的ASCII码,而是向下层的TCP/IP协议“靠拢”,全面采用二进制格式。这样虽然对人不友好,但却大大方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在使用时必须用复杂的状态机,效率低,还很麻烦.
二进制里只有0和1,可以严格规定字段大小、顺序、标志位等格式,对错分明,解析起来没有歧义,实现简单,而且体积小、速度快,做到“内部提效”.
基于二进制的基础,HTTP/2进行了大刀阔斧的改革.
它把TCP协议的部分特性挪到了应用层,把原来“Header+Body”的消息“打散”为数个小片的 二进制“帧”(Frame) ,用"HEADERS"帧存放头数据,“DATA”帧存放实体数据.
这种做法有点像是“Chunked”分块编码的方式(参见第 16 讲),也是“化整为零”的思路,但 HTTP/2 数据分帧后“Header+Body”的报文结构就完全消失了,协议看到的只是一个个的“碎片”.
我们先来看张图吧:
我们看图说话。帧开头就是三个字节的长度,默认上限是2^14到2^24,也就是说HTTP/2的帧的大小通常不超过16K,最大是16M。当然,这个长度不包括帧头(Frame Header)的9个字节.
长度后面的一个字节是 帧类型 ,大致可以分为 数据帧和控制帧 两类,HEADERS帧和DATA帧属于数据帧,存放的是HTTP报文,而SETTINGS、PING、PRIORITY等则是用来管理流的控制帧.
HTTP/2总共定义了10种类型的帧,但一个字节最多可以标识256种,所以也允许在标准之外定义其他类型实现功能扩展.
第五个字节是非常重要的 帧标志 信息,可以保存8个标志位,携带简单的控制信息。常用的标志位有 END_HEADERS 表示头数据结束,相当于 HTTP/1 里头后的空行(“\r\n”),END_STREAM 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\r\n\r\n”).
报文头里最后4个字节 流标识符 ,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”.
有了二进制格式的数据后,就可以把一整块的数据打散,然后发送出去。那碎片到了目的地后要怎么组装起来呢?
HTTP/2为此定义了一个流(Stream)的概念, 它是二进制帧的双向传输序列 ,同一个消息往返的帧会分配一个唯一的流ID。你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是HTTP/1里的请求报文和响应报文.
因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”( Multiplexing)——多个往返通信都复用一个连接来处理.
在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率.
为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的“流”,实现了优先级和流量控制,这些特性也和 TCP 协议非常相似.
HTTP/2 还在一定程度上改变了传统的“请求 - 应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push).
这么说还是有点僵硬,不那么好理解,我们来看张图,再深入的理解下什么是虚拟的流和多路复用.
我们先来看第一部分,有Stream1、Stream2标识的就代表着虚拟流,其实在实际的传输种并不存在,只是一种往返的标识,表示我是属于这一次通信的,所以才说流是虚拟的.
然后是下面的这一部分,就是打散的在TCP信道种传输的一个又一个二进制帧数据,每个帧数据种会有流ID,到达终点后会根据流ID来拼接成一个完整的数据。这样是不是就更好理解了什么是虚拟流.
在 HTTP/2 连接上,虽然帧是乱序收发的,但只要它们都拥有相同的流 ID,就都属于一个流,而且在这个流里帧不是无序的,而是有着严格的先后顺序.
其实上面的图稍微缺失了一点东西,我们把它加上:
我们看上图,其实在传输的时候是乱序的,每个帧都有其独立的流ID,然后就像是虚拟了流的传输.
我们学了不少关于HTTP/2流的内容,那么我们继续看看HTTP/2的流有哪些特点吧.
流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”; 。
客户端和服务器都可以创建流,双方互不干扰; 。
流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回; 。
流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的; 。
流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验; 。
流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数; 。
在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送; 。
第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制.
基于这些内容,我们还可以推断出一些更深层次的东西。比如说,HTTP/2 在一个连接上使用多个流收发数据,那么它本身默认就会是长连接,所以永远不需要“Connection”头字段(keepalive 或 close).
又比如,下载大文件的时候想取消接收,在 HTTP/1 里只能断开 TCP 连接重新“三次握手”,成本很高,而在 HTTP/2 里就可以简单地发送一个“RST_STREAM”中断流,而长连接会继续保持.
再比如,因为客户端和服务器两端都可以创建流,而流 ID 有奇数偶数和上限的区分,所以大多数的流 ID 都会是奇数,而且客户端在一个连接里最多只能发出 2^30,也就是 10 亿个请求。所以就要问了:ID 用完了该怎么办呢?这个时候可以再发一个控制帧“GOAWAY”,真正关闭 TCP 连接.
大家记不记得TCP的三次握手,其实本质上是数据包的交换和双方状态的转换,最开始的时候,客户端和服务器都处于CLOSED状态,当客户端发起一个SYN的时候,服务器会进入LISTEN状态。然后往复的数据包会使客户端和服务器切换状态,我们贴一下之前贴过的图:
那么,HTTP/2的流其实也有一个状态转换的过程。我们先来看下流状态转换的图:
最开始的时候,流都是空闲(idle)状态,也就是”不存在“,可以理解成是待分配的”号段资源“.
当客户端发送HEADERS帧后,有了流ID,流就进入了” 打开 “状态,两端都可以收发数据,然后客户端发送一个带“END_STREAM”标志位的帧,流就进入了“半关闭”状态.
这个“半关闭”状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据.
响应数据发完了之后,也要带上“END_STREAM”标志位,表示数据发送完毕,这样流两端就都进入了“关闭”状态,流就结束了.
刚才也说过,流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的“请求 - 应答”,流关闭就是一次通信结束.
下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送“GOAWAY”帧开一个新的 TCP 连接,流 ID 就又可以重头计数.
我们再看看这张图,是不是和 HTTP/1 里的标准“请求 - 应答”过程很像,只不过这是发生在虚拟的“流”上,而不是实际的 TCP 连接,又因为流可以并发,所以 HTTP/2 就可以实现无阻塞的多路复用.
本来我是想写个HTTP/2的例子的,但是代码其实Node官网有,我写也是照抄,另外,还需要本地安装openssl的证书(因为虽然协议不强制加密,但是现在的浏览器不加密就不能用HTTP/2),我嫌麻烦,就不写了~ 。
我们目前学完了HTTP/2的大部分核心特性,这些内容肯定不是HTTP/2的全部,但是却是最重要的一部分.
另外,HTTP/2为了兼容HTTP/1的明文特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。但是由于HTTPS是大势所趋,基本上互联网上的HTTP/2都是加密的。但是为了区分加密和明文这两个不同版本,HTTP/2定义了h2和h2c两个字符串来区分.
相比于HTTPS,HTTP/2的下层实际上是HPACK和STREAM,加密则是TLS1.2+,这个大家了解下就可以了.
最后,还有一个核心的概念叫做” 连接前言 “,我刚刚也说了,HTTP/2事实上是基于TLS的,所以在正式发送数据前就会有TCP握手和TLS握手,当TLS握手成功后,客户端必须发送一个”连接前言“,用来确认建立HTTP/2连接.
这个“连接前言”是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字“PRI”,全文只有 24 个字节:
PRI * HTTP/
2.0
\r\n\r\nSM\r\n\r\n
那为啥要这样做呢?没有为啥,就是王八的屁股~规定.
还有,HTTP/2固然有很多优点,不然还搞它干啥,但是HTTP/2也有不少的问题。最严重的问题就是丢包和TCP的重新连接。丢包问题是在TCP级别的,HTTP/2解决不了TCP级别的队头阻塞,所以当包丢失后,就要等待后续的包再重新传一遍,当达到一定的丢包率,甚至性能表现还不如HTTP/1。而重新连接,则发生在IP地址切换的时候,TCP就要再次握手,经历慢启动,而且之前连接里积累的HPACK字典也都没了,必须重新计算,导致带宽的浪费和延迟.
好啦,HTTP/2的内容很多,仅仅是这一篇文章肯定不够,但是大家学会了虚拟流、理解了多路复用、头部压缩的HPACK,其实也就了解了HTTP/2的核心,其它的细节,大家可以去规范中自行查阅学习.
最后此篇关于真正“搞”懂HTTP协议13之HTTP2的文章就讲到这里了,如果你想了解更多关于真正“搞”懂HTTP协议13之HTTP2的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
缓冲区溢出问题是众所周知的。因此,我们有幸使用标准库函数,例如 wcscat_s()。 Microsoft 的好心人已经创建了类似的安全字符串函数,例如 StringCbCat()。 但是我遇到了一个
HTTP缓存相关的问题好像是前端面试中比较常见的问题了,上来就会问什么cache-control字段有哪些,有啥区别啥的。嗯……说实话,我觉得至少在本篇来说,HTTP缓存还算不上复杂,只是字段稍
代理,其实全称应该叫做代理服务器,它是客户端与服务器之间得中间层,本质上来说代理就是一个服务器,在HTTP的链路中插入的一个中间环节,就是代理服务器啦。所谓的代理服务就是指:服务本身不生产内容,
我们在前两篇的内容中分别学习了缓存和代理,大致了解了缓存有哪些头字段,代理是如何服务于服务器和客户端的,那么把两者结合起来,代理缓存,也就是说代理服务器也可以缓存,当客户端请求数据的时候,未必一
在前面的章节,我们把HTTP/1.1的大部分核心内容都过了一遍,并且给出了基于Node环境的一部分示例代码,想必大家对HTTP/1.1已经不再陌生,那么HTTP/1.1的学习基本上就结束了。这两
我们前一篇学习了HTTP/2,相比于HTTP/1,HTTP/2在性能上有了大幅的改进,但是HTTP/2因为底层还是基于TCP协议的,虽然HTTP/2在应用层引入了流的概念,利用多路复用解决了队头
前面我们花了很大的篇幅来讲HTTP在性能上的改进,从1.0到1.1,再到2.0、3.0,HTTP通过替换底层协议,解决了一直阻塞性能提升的队头阻塞问题,在性能上达到了极致。 那么,接下
上一篇噢,我们搞明白了什么是安全的通信,这个很重要,特别重要,敲黑板!! 然后,我们还学了HTTPS到底是什么,以及HTTPS真正的核心SSL/TLS是什么。最后我们还聊了聊TLS的实
经过前两章的学习,我们知道了通信安全的定义以及TLS对其的实现~有了这些知识作为基础,我们现在可以正式的开始研究HTTPS和TLS协议了。嗯……现在才真正开始。 我记得之前大概聊过,当
这一篇文章,我们核心要聊的事情就是HTTP的对头阻塞问题,因为HTTP的核心改进其实就是在解决HTTP的队头阻塞。所以,我们会讲的理论多一些,而实践其实很少,要学习的头字段也只有一个,我会在最开始
我们在之前的文章中介绍HTTP特性的时候聊过,HTTP是无状态的,每次聊起HTTP特性的时候,我都会回忆一下从前辉煌的日子,也就是互联网变革的初期,那时候其实HTTP不需要有状态,就是个浏览页面
前面几篇文章,我从纵向的空间到横向的时间,再到一个具体的小栗子,可以说是全方位,无死角的覆盖了HTTP的大部分基本框架,但是我聊的都太宽泛了,很多内容都是一笔带过,再加上一句后面再说就草草结束了。
我的问题确实很简单,是否应该对适配器(设计模式)类进行单元测试,以及如何进行测试? 例子: 我想用PHP创建一个ClientSocket类,它是fsockopen,fread,fwrite的适配器。
目前,我在 PHP 脚本中使用此查询: SELECT * FROM `ebooks` WHERE `id`!=$ebook[id] ORDER BY RAND() LIMIT 125; 数据库最多大约
我们都知道可以使用 GetCustomAttributes 方法查询程序集的属性。我想用它来识别我的应用程序的扩展模块。但是,为了避免加载每个程序集,我更喜欢防御性方法: 使用 Assembly.Re
我正在移植一个非常大的代码库,我在处理旧代码时遇到了更多困难。 例如,这会导致编译器错误: inline CP_M_ReferenceCounted * FrAssignRef(CP_M_Refere
[关于此主题还有其他类似的问题,但是它们都没有回答我在这里提出的问题,即AFAICT。 (即,我已经阅读了所有答案,解释了为什么特定构造无法与发问者尝试进行的操作,在某些情况下,它们提供了获得所需结果
嗨 我想为需要全屏运行的网络艺术应用程序构建一个控制面板,因此所有控制颜色和速度值等内容的面板都必须位于不同的窗口中。 我的想法是建立一个数据库来存储所有这些值,当我在控制面板窗口中进行更改时,应用程
关闭。这个问题需要更多focused .它目前不接受答案。 想改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭 9 年前。 Improve this qu
假设我想实现一个分布式数据库(每个节点都是其他节点的副本);我听说 cdb 能够轻松地在两个节点之间进行同步,并且至少支持某种形式的冲突解决。 不幸的是我不知道 couchdb 因此我不得不问:节点“
我是一名优秀的程序员,十分优秀!