要强的 TCP
缓存刚抢完风头,TCP 就在总结大会上举手:“请求加速怎么没叫上我?”长连接顺利上岗,管道化却很快撞了墙……
圆桌会议
“又到每月一次的总结大会了,大家说说最近工作中遇到的问题吧。” 浏览器老大坐在圆桌最前端,“好建议赶紧提,想吐槽也可以。”
“我!我!我!” TCP 喊道。
“嗯,你说。”
“上次你们优化请求,怎么没叫上我呢?对于请求速度优化我也有点想法!” TCP 说道。
“上次啊,我其实也就是到 HTTP 那儿抱怨了一句,主要还是 HTTP 和他服务器的朋友一起弄好的。” 浏览器老大看了 HTTP 一眼,眼中满是得意,转头看向 TCP 说道:“说说你的想法吧。”
“上次你们研究缓存时,我也仔细查看了经我发送的报文,总结出一个规律。” 说着,TCP 拿出事先准备好的一张图,投到圆桌对面的屏幕上。
“大家请看,这是用户打开 https://breeze.vin 时发出的一系列请求,我按域名做了分类。” TCP 站起来走到屏幕前,指向下面的表格:
| 域名 | 请求数 |
|---|---|
| blog.acohome.cn | 33 |
| cdn.bootcss.com | 4 |
| code.jquery.com | 1 |
| hm.baidu.com | 4 |
“大家请看,该页面的资源来自 4 个域名,共有 42 个请求。老大,我们当前的策略是:每个 HTTP 请求都新建一个 TCP 连接,对吧?”
“嗯,早期的实现确实如此。” 浏览器老大陷入沉思,似乎猜到了 TCP 想说什么。
“建立和释放一个 TCP 连接,需要三次握手和通常所说的四次挥手。照上面的情况,这套流程要重复 42 次,其中不少连接的两端其实完全相同。比如,我要和 blog.acohome.cn 所在的服务器反复建立、释放 33 次连接,会造成明显的时间和资源开销,服务器那边的哥们已经抱怨好久了!”
“那说说你的解决方案吧!” 浏览器老大意识到了问题的严重性,确实这一块有较大的资源浪费。
长连接
“解决方案其实很简单:完成一次请求后先别急着关闭连接,让后续请求复用它,也就是持久连接。对同一个源可以复用已有连接,请求很多时也可以并行建立少量连接。” TCP 自信满满地说。
“嗯!不错的想法” 浏览器老大闭眼思考了一会,说道:“但是不能泛泛而谈,也有可能会出现需要及时释放的情况。这块容我想想该怎么实现!”
“嗯,确实会有特殊情况发生,需要好好考虑下。” TCP 也思考了一下。
“这样吧,我仔细想想!然后给你一个解决方案。” 说着浏览器老大站起身,说道:“那今天的会议先到这!”
keep-alive
大概过了 500ms 的样子,浏览器老大来到 TCP 的办公室。
“你提的确实是个好想法,但是是否保持数据通道不该由你来定,应该由上层的调用者来确定,就是 HTTP,我顺便把他也叫过来了,你们商量商量。” 说着 HTTP 穿着白大褂走了进来。
“刚刚我听了,很不错的想法。老大,TCP 说的没错,我们这儿的请求基本上都是需要长连接的。要不就都这么处理吧!” HTTP 转向浏览器老大说道。
“不不不,你忘了上次擅自决定缓存策略惹出的麻烦吗?这里不能写死!连接是否复用应当遵循协议规则和通信双方的协商。” 浏览器老大想起前几天的事情,慎重地说道。
“确实哈,那就和之前加缓存头一样?” HTTP 略微有点尴尬。
“对,字段就叫 Connection 吧。HTTP/1.0 通常用 keep-alive 表示希望复用连接;到了 HTTP/1.1,持久连接默认启用,发送 close 才表示本次通信后关闭。这样 OK 吧?”
“没问题!” TCP 和 HTTP 同时说道。
“那我顺便把持久连接的规则带到 HTTP/1.1 规范大会上吧。” HTTP 补充道。
“嗯,让大家都用上也好。” 浏览器老大得意地说着,走了出去。
哎呦,不错哦
“TCP 牛啤啊!” 说着 HTTP 搂上了 TCP 的肩膀。
“那当然,风头不能都让你占了啊!”
“长连是好想法,但是请求的顺序不能乱哦,我给你一个报文,你要返给我一个,不然我可不知道怎么对应的!”
“哎 ~” TCP 叹了口气。
“怎么了?不能保证?” HTTP 显得有点慌乱。
“不是啊,我是能保证,但需要你一个一个给。”
“对啊,现在就是这样啊!你叹什么气?”
“其实吧,我还有个更激进的方案,要不要了解一下?”
“真的?还有更好的?” HTTP 兴奋地问道。
管道化
“是有啊,但是和我服务的哥们讨论下来,实际的可行性可能有点问题。” TCP 面露尴尬。
“试试嘛,没准能行呢?”
“那我先说说实现原理哈。”
“嗯,你说!”
“首先,你按顺序把多个请求连续交给我,不必等前一个响应回来。服务端收到后,也必须按请求顺序返回响应。” 说着,TCP 拿起纸笔画了起来。

“你看哈,最左边是现有的方式,中间是加了 keep-alive 的实现,右边呢是我刚提到的想法,我把最后这种实现叫做管道化,你看看。”
“管道化,有意思!牛皮啊!你给解释解释呗!我看你画的有点复杂。” HTTP 皱起了眉头。
“你把请求一开始就按顺序给我,由于通道是双向的,在建立连接后,我就可以边发送数据边接收数据,也就是说在第一个请求发出去后,我就可以开始接收第一个请求的数据了,也就是图中响应和请求出现交叉的地方。”
“原来你还有同时发送和接收的能力啊!确实,如果能实现的话,是够快的,那么问题在哪?”
“一考虑网络状况,问题就出现了。假如我刚发完所有请求,连接却因为网络原因崩溃了,会发生什么?”
“按正常流程处理啊,重建连接,再次发送!”
“问题就出在这!那第一个请求的响应怎么办?如果这个请求是 POST 请求,不就发了两次吗?要是这次多余的 POST 请求出了状况,那责任谁担?”
“emmmm”
“即使网络状况很好,响应也必须按请求顺序返回。如果服务器处理第一个请求耗时很长,后面的响应即使早已准备好,也只能等着。这就是应用层的队头阻塞。”
“emmmm”
“还不如我另开一个 TCP 通道来的快!”
“emmmm”
“所以,管道化只在特定条件下才可能有效,而且客户端和服务器都得正确支持。现实中的兼容性和重试语义很复杂,后来主流浏览器索性放弃了它。”
“经你这么一说,确实啊,实际使用情况有限。兄弟,其实在设计缓存那一块时,我发现了一个真理!”
“什么真理?”
“先让开发者在可控环境里试验!”
“什么意思?”
“我们这边其实并不能真正了解开发者的意图,因此能做的也有限,但如果你把是否使用管道化的权限交给开发者,在开发者确认能使用的情况下,就使用,这样就能在一个安全稳定的环境下,对性能进行提升了!”
“对哦,既然这个并不能针对到所有的情况,那么交给页面开发者就好了!真是个好想法!”
“而且你不能直接接触开发者,还是由我提供试验入口吧。管道化本来就是 HTTP/1.1 规范中的可选能力,我们先验证它在真实环境中的效果。”
“好兄弟!”
算了,关了吧!
屏幕外:
“听说 HTTP/1.1 有个叫管道化的技术,你了解过吗?”
“嗯,听说过,但网上对这个技术褒贬不一啊,并不能解决真正实际的问题。”
“怎么开呢?我想试试!”
“这个实验环境里有个设置,你打开就行,你看,在这儿!”
“嗯,打开了!”
“后端收到请求确实快了,一下子来这么多????”
“你有对你的服务器做过相关优化吗?”
“没有啊!”
“那还不如不开,你多试几次,看看是不是真的有提升?”
脚本一开,一顿操作,自动化测了 1000 次。
“emmmm 好像还不如不开...”
“...”
“算了,关了吧。”