要强的 TCP

缓存刚抢完风头,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 吧?”

“没问题!” TCPHTTP 同时说道。

“那我顺便把持久连接的规则带到 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 好像还不如不开...”

“...”

“算了,关了吧。”