Chrome 的秘密实验
缓存、长连接、管道化轮番上阵,请求还是不够快。HTTP 和 TCP 决定偷偷开个新部门,结果实验一炮而红,连 Firefox 都找上门了……
还能更快!
“咚咚咚” 浏览器老大的门被敲响了三次,“谁啊!”
“我!” TCP 喊道,“还有我!” HTTP 随后接道。
“进来吧,都有什么事啊?” 说着,浏览器老大放下了手头的工作。
“你来说吧!毕竟是你想到的。” TCP 对 HTTP 说道。
“那我就不客气了!老大,还记得之前提的让请求变快的方式吗?”
“当然,首先是缓存,然后是长连接,接着你们自己还定了个 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
“那行,我来总结总结,你们想新开一个部门的主意,我同意了!但这个东西涉及的东西有点多,先在我们内部实现下,还有服务器那边,还需要你们去沟通一下!这部门主要实现以下几点:”
- 管理请求以及响应,
HTTP这边现有的方式不变,请求的顺序以及响应的封装由该部门内部实现。 - 将
HTTP请求分为头帧与数据帧,同时将对应的字节流给TCP。 - 保存同一域名下的报文头状态(包括请求头和响应头),同步服务器的状态信息。
- 保存由服务器主动推送过来的资源,当
HTTP请求当命中该资源时,直接返回。
“以上四点,你俩看看,是不是你们需要的?”
“嗯!” TCP 与 HTTP 同时说道。
“这一整套实现,总归得有个名字,部门叫二进制分帧层,这一整套实现我们叫他 SPDY 吧,由于涉及的比较多,我们自己内部先试用试用!”
“嗯嗯,好的!”
“还呆着干嘛呢?去看看新部门?”
“这么快你就建好了?”
“当然,谁让我是这儿的老大呢?心之所想,事之所至!但服务端的 TCP 和 HTTP 那儿,需要你们赶紧去沟通沟通哦!还有 TCP ,这项算是实验功能,你要确保你能继续使用之前的形式处理请求哦 ~”
“那当然,必须的!” TCP 自信满满地说道。
实验
“怎么样啦,我这边已经安排好啦!” 浏览器老大晃悠到 HTTP 的手术室说道。
“都没问题啦!” TCP 回答道:“刚刚我们一起已经把计划通知到服务器那边了,现在在等他们回电话!”
“哦哦,那我也来一起等吧,二进制分帧层里我已经派了几个人过去了。” 浏览器老大晃悠到 TCP 与 HTTP 身边。
“苍茫的天涯的是我的爱!” 电话铃声突然想起,HTTP 尴尬的接起了电话。
“老大,HTTP 这品味有点高啊,这彩铃!牛皮!哈哈哈!” TCP 大笑道。
“喂,HTTP 吗?我们这边已经弄好了,赶紧发个请求试试!” 电话里传来了兴奋的女声。
“呦呵!HTTP 你这小子!High 啊!” TCP 小声道:“快说!什么时候勾搭上的?”
“嘘,被她听到你可要挨揍了!她是个技术狂,ps:爱好跆拳道!在 HTTP 学院可是出了名的!” HTTP 小声对 TCP 说道。
“赶紧的,磨磨蹭蹭干嘛!HTTP,抓紧!” 电话里的声音变得急躁起来。
“好的好的,那我就请求这个网页——https://breeze.vin。” 说着,HTTP 把一个报文丢进一旁的通道。
“我已经帮你的通道接到二进制分帧层了,过去看看?” 浏览器老大说道:“看看是不是符合你们的预期!”
说话间,三人来到新建的部门,门上写着 BF(Binary Framing) 两个大字,只见房间很大,隔了很多个小间,突然间其中一个小间突然蹦出了刚刚 HTTP 丢到通道中的报文。
只见其中一人略带生疏的操作了起来:拿起请求头,放进了一旁的机器,不一会出来了一段二进制流。
“你们看,这里他把请求头单独处理,那台机器就实现同步服务器报文头算法的关键,我已经和服务器那边商量过了,那边有相同的实现,出来的就是二进制流,会直接丢给 TCP,当然这台机器还对数据进行了一定的压缩。”
浏览器老大说着,里面的操作员把二进制流丢进了另一个通道中,浏览器老大接着解释道:“这个是 GET 请求,所以没有请求体,如果有请求体的话,他还会把数据处理成二进制流接着给 TCP。我们接着去看看 TCP 那边吧!”
“这里就由我来说吧!” TCP 抢过了话题,说道:“我们这里呢就比较简单了,不管他给什么,我直接发给服务器。你看那个实习生,开始工作了,三次握手,发送数据,做的不错!”
“嗯,但关键不是这个哦,等一等!” TCP 神秘的一笑。
“你们看,数据来了,按照之前说的,我这边只要把响应给二进制分帧层就 OK 了!”
“我靠,一下子来这么多响应啊!” HTTP 张大了嘴巴:“一个网页应该不需要这么多数据啊!”
“嘿嘿!你忘了我之前提的服务器推送?这些都是服务器提前推过来的。” TCP 兴奋地说,“接下来我们去看二进制分帧层吧!”
说话间,三人再次来到了二进制分帧层外。
“你看,他正在组合响应,只是动作还有点生疏。” 浏览器老大解释道。不一会儿,https://breeze.vin 的响应便被拼好,送进一侧的通道。
“去 HTTP 那儿吧!” 浏览器老大兴奋地说道。
“等等呀,你们看他,还在拼数据?” HTTP 有点困惑。
“等下你就知道了!先去你那儿吧!” TCP 一脸的得意。
“我那有什么好看的,无非就把响应给老大你啊!” HTTP 还是想看看屋内的人在干嘛。
“行啦,测试要紧,等下你就知道啦!” 浏览器老大说道,拎着 HTTP 走向手术室。
“呐,这个就是响应的网页,老大你来解析下吧!”
“请求 jquery.js 和 main.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 协议。
后日谈:服务器推送曾是这套体系里最受期待的能力之一,但现实中推送时机的判断远比想象复杂,实际收益有限,主流浏览器后来陆续移除了对它的支持(Chrome 于 106 版本移除)。真正留下来挑大梁的,是二进制分帧、头部压缩与多路复用。
银弹!?
以下内容改编自 Will__ 的评论,经适当润色后收录于此。
见 HTTP/2 如此厉害,浏览器老大们心头不胜欢喜,这下子用户体验又可以上一个台阶了。
“去网络不稳定的移动设备上试试身手?” 浏览器老大带着 Chrome 实验室的众人,摇身一变,钻进了地铁上一个小伙子的安卓手机。
只见小伙子打开了浏览器 App,轻车熟路地输入了一串网址。
嗯,是小伙子平时看小姐姐照骗的地方。
TCP 报告道:“该网站支持 HTTP/2,协议协商成功。”
浏览器窃喜:“这不正是展示我快人一等的天赐良机吗?” 窃喜间,10 张小姐姐照片的请求已提交到了请求队列里。
“小意思,TCP 这家伙靠谱,多路复用不是盖的。来人啊,吩咐 HTTP 造好 10 个小姐姐照片的请求,一次性给服务器送过去。数据帧该压缩的给我压好,头帧用我刚写好的 HPACK 压缩一下,赶紧的,动起来!”
浏览器老大刚说完,停顿了一下,又赶忙叫来了图片解析器和渲染引擎,心想着:“是时候展现什么是速度了!你们两看着哈,一旦小姐姐照片来了给我立马呈现!可不能在你们这给耽误了!”
时间一秒一秒过去,小伙子显得有些不耐烦。浏览器心慌了,不对啊,按常理至少有一张小姐姐的照片返回数据了。
“HTTP 你怎么回事,10 个小姐姐一个都没有返回吗?”
“报告老大,我也很焦急。我早问过二进制分帧层那边的人了, TCP 那边零零散散给了一些帧数据,但是根本不足以拼出来任何一个完整的小姐姐啊!” HTTP 委屈巴巴地说。
“走,去 TCP 那边看看。什么情况?” 浏览器老大有些疑惑又有些生气,说罢带着 HTTP 往 TCP 工作间走去。
“TCP,怎么回事啊,发生什么了?10 张小姐姐照片怎么过去这么久了一张完整的也没给返回?”
“稍安勿躁,实在是抱歉啊二位,小姐姐照片的数据在传输过程中丢包了,正在等服务器重传呢。你们也知道,这地铁里面信号不好,丢包总是不可避免的嘛!” TCP 找起了借口。
“现在还在等哪张图的丢包重传?” 浏览器老大的语气有些急躁。
“这个你还真问住我了。我看看啊,缺的数据帧编号是 306 ...”
“是第一张小姐姐的!这块数据已经等了好久了!什么情况啊?” 二进制分帧层里的部门头儿突然冒了出来。兴许也是等着急了,听到这边有声音就过来了。
“那其他几张呢?传完了没有?” 浏览器老大问。
“零零散散的都有点缺,都在等着呢。”
“等什么?不是说好的顺序不重要么?其他几张的数据赶紧继续传啊!” 想着小伙子看到的还是一片空白,浏览器老大就气不打一处来。
“不是,不是。HTTP/2 各个流的帧可以交错,但它们最终都跑在同一条有序的 TCP 字节流上。前面的 TCP 数据一旦丢失,后面的数据即使到达,也要等重传完成后才能继续交付。不过快了,我已经通知服务器重传了。” TCP 挠了挠头,也很无奈。
“啊?要是这样的话,那还不如 HTTP/1.1 时代呢。那时候老大能给我安排 6 路 TCP,虽然不能一条 TCP 多路复用,但至少我不用在一颗树上吊死吧。” HTTP 埋怨道。
“行了,数据链路上的传输不是我们所能控制的,新的 TCP 通道也不一定比现在来的快,况且建立通道的开销也是问题。” 浏览器老大边说边陷入了深思。
说话间,丢包的数据已经重传成功,TCP 工作间又开始忙碌了起来。
“行了,数据来了,大家都回去吧。” 浏览器老大双手交叉在后背,沉思着走开了。大家也都不发一言各自回到岗位上。
“看来 HTTP/2 也不是银弹啊。”