Chrome 的秘密实验

缓存、长连接、管道化轮番上阵,请求还是不够快。HTTP 和 TCP 决定偷偷开个新部门,结果实验一炮而红,连 Firefox 都找上门了……

分享
Chrome 的秘密实验

还能更快!

“咚咚咚” 浏览器老大的门被敲响了三次,“谁啊!”

“我!” TCP 喊道,“还有我!” HTTP 随后接道。

“进来吧,都有什么事啊?” 说着,浏览器老大放下了手头的工作。

“你来说吧!毕竟是你想到的。” TCPHTTP 说道。

“那我就不客气了!老大,还记得之前提的让请求变快的方式吗?”

“当然,首先是缓存,然后是长连接,接着你们自己还定了个 pipelining 对吧?怎么啦?难不成又有新发现!” 浏览器老大两眼开始放光。

emmm,那个 pipelining 不太好用。不过我们确实有了新发现!这几天,我和 TCP 仔细研究了一番,发现速度还能提升,所以过来和你讨论讨论。” HTTP 显得有些兴奋。

回忆

故事开始在一周前。

TCP,你提的 pipelining 想法很美好,但局限性太大,就没有更通用的办法吗?” HTTP 惋惜地说道。

“还不是因为响应必须按请求顺序返回!服务器那边前一个响应卡住,后面的响应就算准备好了也交不出来,这就是队头阻塞。”

“我也没办法呀,请求是一个个过来的,我总得一个个的对应啊,就只能让他们排队啦!”

“哎,其实我也知道你没办法。” TCP 叹了口气,接着说道,“我一个数据传输通道,为什么要管 HTTP 消息如何对应呢?要是上层能给每组数据编号,我只管传输,不就快多了吗?”

“但是,顺序很重要!” HTTP 一脸的无奈,突然间灵光一闪:“顺序!对!顺序!哈哈哈!你干嘛要管顺序呢!交给别人来管不就好了!来来来,TCP 你听我说!”

“嗯哼?我不管顺序?别人?你吗?你不是得按顺序给吗,但我不想要顺序!”

“你听我说嘛!” HTTP 兴奋地说道,“如果有人专门管理请求和响应的对应关系,不就好了?”

“你仔细说说。” TCP 来了兴趣。

“如果想提升速度,首先得让多个响应不再互相排队,对吧?”

“对!最好只让我负责发送和接收字节。”

“好。那就让上层把标记好的数据交给你,你从服务器收到数据后再原样交回上层。”

“是啊,但我的上层不就是你吗?不同 HTTP 消息混在一起,我怎么知道每段数据属于谁?” TCP 一脸疑惑。

“顺序只是最终呈现的结果。只要给每个请求和响应加上标识,即使数据帧交错传输,上层也能把它们重新组合起来。”

“经你这么一说,确实啊!只要有人根据标识把帧重新组装,我就只管收发字节了!” TCP 兴奋起来,却又突然想到一个问题:“不对,那是不是要等所有响应都结束后,才能交给你?”

“干嘛非要等所有响应都结束?只要每一帧都标明自己属于哪条流,哪条流先完成,就先把哪条流的响应交给我。其他还没完成的流继续传,彼此不用排队。” HTTP 得意地说道。

“哦,对!这个想法太妙了!那谁来负责拆帧和组帧呢?”

“这个啊,我不是很想做诶!” HTTP 看向了别处,明显不想干这个脏活。

“我也不想啊,好不容易才让我只关注数据传输,我可不干!” TCP 反抗道,“再说了,要实现这个方案,还得让服务器那边也按同一套规则处理。我可忙不过来!”

“要不,让老大新开个部门?专门处理,专人专事嘛!”

“嘿嘿,不错的想法!”

SPDY 的诞生

“老大!这就是我们想要的新部门!我们管它叫顺序层!” TCP 说道。

“哟!最近你们俩表现得很耀眼啊,简直是浏览器界的两颗新星!” 浏览器老大瞬间被两人的想法折服,“说吧,你们想怎么干?我来协调。”

“首先,我要保证我的形式不变!” HTTP 抢先说道:“我还是把我的请求顺序的给我的下一层,也就是这个新层,其他的我就不管了!”

“老大,我也想提点要求!” TCP 在一旁举手示意。

“嗯,你说!”

“能不能直接给我二进制的数据流啊,既然要加,那就直接给我二进制流吧,我处理起来快!”

“那这一层就像一个容器:HTTP 把消息交进来,容器将它拆成带有流标识的二进制帧,再交给 TCP;收到响应帧后,容器按流重新组合,再交回 HTTP,对吧?” 浏览器老大想了想,接着说道,“既然使用二进制帧通信,这一层就叫 二进制分帧层 吧。既然要成立新部门,不如继续想想它还能做什么?”

三人陷入了沉思,不一会,HTTP 举手了。

“如果说,二进制分帧层建立了,那服务器那边应该需要实现一套与之相对应的操作,对吧?” HTTP 问道。

“当然!” TCP 说道:“因为和现有的形式完全不一样!”

“那既然双方都有对应的结构,并且 TCP 通道是长连接的话,那一些重复性的数据是不是就可以不传了?”

“嗯,让我想想!” TCP 陷入沉思。

“这样,你举个例子!” 浏览器老大在一旁说道。

“就比如说 user-agent cookie 这些请求头,因为如果是对同一域名进行请求的话,这些基本上是一样的!既然第一次给了,第二次最好不给,这些请求头有时候比请求体还要大,去掉的话,可以节省不少的网络流量!”

“对!这些请求附带的信息目前一直都会重复的被发送!” TCP 说道:“如果现在有了这层的话,这些状态完全可以保存在那里,然后两边同步状态!如果有变更,直接在通过我发一次专门修改请求头的数据就好了!但是如果要是实现同步的话,这个报文头得专门单独出来处理,通过一些算法让两边的状态保持同步。”

“我来总结一下:这里要解决的是报文头重复发送的问题。通信双方维护一致的动态表,再用索引表示重复字段,就能减少传输量。我们可以把头部和消息体分开编码,交给 TCP 的数据也分成头部帧与数据帧。”

bingo ~ 老大总结的就是不一样。”

浏览器老大呵呵一笑,对 TCP 说道:“你看看他这样子,你呢,有什么想法?”

“老大,你说,有二进制分帧层的话,服务器推送是不是可以实现了啊!由于之前我给 HTTP 的数据要和他给我顺序必须要一致,因此也不能存储其他的数据资源,现在有这个二进制分帧层让他直接存下来就行啦!”

“我靠,我怎么没想到!” HTTP 明显是被这个想法给惊到了。

“服务器推送,你仔细说说!” 浏览器老大看向了 TCP

“在通道建立之后,服务器就可以推送给我数据啦,什么数据我不管啦,我都把数据给二进制分帧层就好啦!”

“别说的这么简单。举个例子!”

“就比如:用户打开了网页,这不是会产生一个 HTTP 请求嘛!我这边就会与服务器开启 TCP 数据通道,之后服务器开始返回对应的网页数据,这时,服务器其实知道后续还会有什么请求,比如说页面内的 javascript 资源,那这时候服务器直接把 javascript 资源发给我,毕竟通道还在嘛!然后存储在二进制分帧层,接下来 HTTP 会发起对该 javascript 资源的请求,二进制分帧层接到请求后,其实他早就已经开始接收 javascript 的数据,那么响应就会变快了不少,数据通道也能被充分的利用!”

“嗯,不错啊,你看看人家,这可是质的飞越啊,当然你的想法也很好!” 浏览器老大对 HTTP 说道。

SPDY

“那行,我来总结总结,你们想新开一个部门的主意,我同意了!但这个东西涉及的东西有点多,先在我们内部实现下,还有服务器那边,还需要你们去沟通一下!这部门主要实现以下几点:”

  1. 管理请求以及响应,HTTP 这边现有的方式不变,请求的顺序以及响应的封装由该部门内部实现。
  2. HTTP 请求分为头帧与数据帧,同时将对应的字节流给 TCP
  3. 保存同一域名下的报文头状态(包括请求头和响应头),同步服务器的状态信息。
  4. 保存由服务器主动推送过来的资源,当 HTTP 请求当命中该资源时,直接返回。

“以上四点,你俩看看,是不是你们需要的?”

“嗯!” TCPHTTP 同时说道。

“这一整套实现,总归得有个名字,部门叫二进制分帧层,这一整套实现我们叫他 SPDY 吧,由于涉及的比较多,我们自己内部先试用试用!”

“嗯嗯,好的!”

“还呆着干嘛呢?去看看新部门?”

“这么快你就建好了?”

“当然,谁让我是这儿的老大呢?心之所想,事之所至!但服务端的 TCPHTTP 那儿,需要你们赶紧去沟通沟通哦!还有 TCP ,这项算是实验功能,你要确保你能继续使用之前的形式处理请求哦 ~”

“那当然,必须的!” TCP 自信满满地说道。

实验

“怎么样啦,我这边已经安排好啦!” 浏览器老大晃悠到 HTTP 的手术室说道。

“都没问题啦!” TCP 回答道:“刚刚我们一起已经把计划通知到服务器那边了,现在在等他们回电话!”

“哦哦,那我也来一起等吧,二进制分帧层里我已经派了几个人过去了。” 浏览器老大晃悠到 TCPHTTP 身边。

“苍茫的天涯的是我的爱!” 电话铃声突然想起,HTTP 尴尬的接起了电话。

“老大,HTTP 这品味有点高啊,这彩铃!牛皮!哈哈哈!” TCP 大笑道。

“喂,HTTP 吗?我们这边已经弄好了,赶紧发个请求试试!” 电话里传来了兴奋的女声。

“呦呵!HTTP 你这小子!High 啊!” TCP 小声道:“快说!什么时候勾搭上的?”

“嘘,被她听到你可要挨揍了!她是个技术狂,ps:爱好跆拳道!在 HTTP 学院可是出了名的!” HTTP 小声对 TCP 说道。

“赶紧的,磨磨蹭蹭干嘛!HTTP,抓紧!” 电话里的声音变得急躁起来。

“好的好的,那我就请求这个网页——https://breeze.vin。” 说着,HTTP 把一个报文丢进一旁的通道。

“我已经帮你的通道接到二进制分帧层了,过去看看?” 浏览器老大说道:“看看是不是符合你们的预期!”

说话间,三人来到新建的部门,门上写着 BFBinary Framing) 两个大字,只见房间很大,隔了很多个小间,突然间其中一个小间突然蹦出了刚刚 HTTP 丢到通道中的报文。

只见其中一人略带生疏的操作了起来:拿起请求头,放进了一旁的机器,不一会出来了一段二进制流。

“你们看,这里他把请求头单独处理,那台机器就实现同步服务器报文头算法的关键,我已经和服务器那边商量过了,那边有相同的实现,出来的就是二进制流,会直接丢给 TCP,当然这台机器还对数据进行了一定的压缩。”

浏览器老大说着,里面的操作员把二进制流丢进了另一个通道中,浏览器老大接着解释道:“这个是 GET 请求,所以没有请求体,如果有请求体的话,他还会把数据处理成二进制流接着给 TCP。我们接着去看看 TCP 那边吧!”

“这里就由我来说吧!” TCP 抢过了话题,说道:“我们这里呢就比较简单了,不管他给什么,我直接发给服务器。你看那个实习生,开始工作了,三次握手,发送数据,做的不错!”

“嗯,但关键不是这个哦,等一等!” TCP 神秘的一笑。

“你们看,数据来了,按照之前说的,我这边只要把响应给二进制分帧层就 OK 了!”

“我靠,一下子来这么多响应啊!” HTTP 张大了嘴巴:“一个网页应该不需要这么多数据啊!”

“嘿嘿!你忘了我之前提的服务器推送?这些都是服务器提前推过来的。” TCP 兴奋地说,“接下来我们去看二进制分帧层吧!”

说话间,三人再次来到了二进制分帧层外。

“你看,他正在组合响应,只是动作还有点生疏。” 浏览器老大解释道。不一会儿,https://breeze.vin 的响应便被拼好,送进一侧的通道。

“去 HTTP 那儿吧!” 浏览器老大兴奋地说道。

“等等呀,你们看他,还在拼数据?” HTTP 有点困惑。

“等下你就知道了!先去你那儿吧!” TCP 一脸的得意。

“我那有什么好看的,无非就把响应给老大你啊!” HTTP 还是想看看屋内的人在干嘛。

“行啦,测试要紧,等下你就知道啦!” 浏览器老大说道,拎着 HTTP 走向手术室。

“呐,这个就是响应的网页,老大你来解析下吧!”

“请求 jquery.jsmain.css 吧,网页解析器要用!” 浏览器老大叫来了网页解析器一顿解析。

“好嘞,发起请求!” HTTP 说着,就把请求发了出去:“我们去看看那屋,我想看看他到底在拼什么!”

“嘿嘿!” TCP 神秘的一笑,却不为所动。

“走啊,你们怎么不走!”

“你看,你的响应已经到了!”

“不可能,我才刚发出去!”

“不信你看。” TCP 往通道努了努嘴。

HTTP 扭头往通道看去,果然,两个大大的包裹已经出现。HTTP 惊的张大了嘴巴:“怎么可能!什么情况?”

“就知道刚刚在说服务器推送的时候你没仔细听!刚刚服务器一直在发送这些数据给我们,因为他们知道我们迟早会有这些请求,他就提前推了过来!”

“哦哦!服务器推送,好东西啊!我就说呢,明明网页都给我了,他怎么还在拼数据,原来是在拼之后的请求啊!厉害了! TCP 牛皮!”

“低调低调!”

“好了,完整性测试通过!SPDY 实现成功!” 浏览器老大兴奋的挥了挥手。

羡慕

午夜降临,各个浏览器开始闲了下来,这不一天一次的浏览器总结大会展开了。

Chrome,这就是你的不对了!” Firefox 在一旁说道,“好东西怎么能私藏呢?”

“什么嘛!我私藏什么了?” Chrome 说道。

“我们那儿的 HTTP 听服务器端的朋友说,你们这儿有个叫 SPDY 的东西,请求快得飞起!”

“哦,那个啊,这是文档,给你吧,我也没想私藏,只是试验一下可行性!这不是带来推广了嘛?”

“真的?”

“真的!但这东西需要和服务器端配合,如果说服务端没实现 SPDY 的话,那我们实现了也白搭。”

“那我们至少可以支持嘛!”

“是的,这也是我把协议具体实现带过来的原因,大家有个统一的标准服务器实现起来也简单,就能更快的推广了!”

“嗯,不错不错!” Firefox 开心的竖起来大拇指。

HTTP/2

“老大!老大!” HTTP 跑到了浏览器老大的办公室。

“怎么了,慌慌张张的?”

“我想把 SPDY 这一整套体系提到 HTTP/2 标准里!这样就算是一个公认的标准了!推广那简直是轻而易举。”

“我是没有意见,你问问 TCP 毕竟这也有他的功劳!”

“我已经问啦,他没意见呢!如果这东西能成为标准,让大家都实现的话,他也是很期待的呢!”

“行,那你提吧!”

不久后,HTTP/2 标准化工作正式展开。它吸收了 SPDY 的许多核心设计,并经过标准化调整,逐渐成为广泛使用的新一代 HTTP 协议。

后日谈:服务器推送曾是这套体系里最受期待的能力之一,但现实中推送时机的判断远比想象复杂,实际收益有限,主流浏览器后来陆续移除了对它的支持(Chrome106 版本移除)。真正留下来挑大梁的,是二进制分帧、头部压缩与多路复用。

银弹!?

以下内容改编自 Will__ 的评论,经适当润色后收录于此。

HTTP/2 如此厉害,浏览器老大们心头不胜欢喜,这下子用户体验又可以上一个台阶了。

“去网络不稳定的移动设备上试试身手?” 浏览器老大带着 Chrome 实验室的众人,摇身一变,钻进了地铁上一个小伙子的安卓手机。

只见小伙子打开了浏览器 App,轻车熟路地输入了一串网址。

嗯,是小伙子平时看小姐姐照骗的地方。

TCP 报告道:“该网站支持 HTTP/2,协议协商成功。”

浏览器窃喜:“这不正是展示我快人一等的天赐良机吗?” 窃喜间,10 张小姐姐照片的请求已提交到了请求队列里。

“小意思,TCP 这家伙靠谱,多路复用不是盖的。来人啊,吩咐 HTTP 造好 10 个小姐姐照片的请求,一次性给服务器送过去。数据帧该压缩的给我压好,头帧用我刚写好的 HPACK 压缩一下,赶紧的,动起来!”

浏览器老大刚说完,停顿了一下,又赶忙叫来了图片解析器和渲染引擎,心想着:“是时候展现什么是速度了!你们两看着哈,一旦小姐姐照片来了给我立马呈现!可不能在你们这给耽误了!”

时间一秒一秒过去,小伙子显得有些不耐烦。浏览器心慌了,不对啊,按常理至少有一张小姐姐的照片返回数据了。

HTTP 你怎么回事,10 个小姐姐一个都没有返回吗?”

“报告老大,我也很焦急。我早问过二进制分帧层那边的人了, TCP 那边零零散散给了一些帧数据,但是根本不足以拼出来任何一个完整的小姐姐啊!” HTTP 委屈巴巴地说。

“走,去 TCP 那边看看。什么情况?” 浏览器老大有些疑惑又有些生气,说罢带着 HTTPTCP 工作间走去。

TCP,怎么回事啊,发生什么了?10 张小姐姐照片怎么过去这么久了一张完整的也没给返回?”

“稍安勿躁,实在是抱歉啊二位,小姐姐照片的数据在传输过程中丢包了,正在等服务器重传呢。你们也知道,这地铁里面信号不好,丢包总是不可避免的嘛!” TCP 找起了借口。

“现在还在等哪张图的丢包重传?” 浏览器老大的语气有些急躁。

“这个你还真问住我了。我看看啊,缺的数据帧编号是 306 ...”

“是第一张小姐姐的!这块数据已经等了好久了!什么情况啊?” 二进制分帧层里的部门头儿突然冒了出来。兴许也是等着急了,听到这边有声音就过来了。

“那其他几张呢?传完了没有?” 浏览器老大问。

“零零散散的都有点缺,都在等着呢。”

“等什么?不是说好的顺序不重要么?其他几张的数据赶紧继续传啊!” 想着小伙子看到的还是一片空白,浏览器老大就气不打一处来。

“不是,不是。HTTP/2 各个流的帧可以交错,但它们最终都跑在同一条有序的 TCP 字节流上。前面的 TCP 数据一旦丢失,后面的数据即使到达,也要等重传完成后才能继续交付。不过快了,我已经通知服务器重传了。” TCP 挠了挠头,也很无奈。

“啊?要是这样的话,那还不如 HTTP/1.1 时代呢。那时候老大能给我安排 6TCP,虽然不能一条 TCP 多路复用,但至少我不用在一颗树上吊死吧。” HTTP 埋怨道。

“行了,数据链路上的传输不是我们所能控制的,新的 TCP 通道也不一定比现在来的快,况且建立通道的开销也是问题。” 浏览器老大边说边陷入了深思。

说话间,丢包的数据已经重传成功,TCP 工作间又开始忙碌了起来。

“行了,数据来了,大家都回去吧。” 浏览器老大双手交叉在后背,沉思着走开了。大家也都不发一言各自回到岗位上。

“看来 HTTP/2 也不是银弹啊。”