极客时间已完结课程限时免费阅读

03 | 换个角度解决问题:服务端推送技术

03 | 换个角度解决问题:服务端推送技术-极客时间

03 | 换个角度解决问题:服务端推送技术

讲述:四火

时长16:51大小15.42M

你好,我是四火。
今天我们继续和 HTTP“过不去”。在上一讲,我们讲到了 HTTP 在安全传输方面的局限,并介绍了怎样使用经过 TLS 加密的 HTTPS 连接来解决这样的弊端。
今天,我要给你讲讲传统 HTTP 的另一个在交互模式上的局限,就是只能由客户端主动发起消息传递,而服务端只能被动响应消息的局限,并介绍它的解决办法。

Pull 模型的问题

让我们来思考这样一个场景,假设你设计了一款网页版的即时聊天工具,现在你使用浏览器打开了聊天页面,正在和朋友愉快地聊天。这时有朋友给你发送了一条消息,可是由于 HTTP 本身机制的限制,服务端无法主动推送消息,告知浏览器上的聊天页面“你有一条消息”,进而影响到了消息的即时送达。那么,这个问题怎么解决?
你可能会立即想到轮询(Poll),比如浏览器每隔十秒钟去问一下服务端是不是有新消息不就完了嘛。这看起来是个好思路,但明显存在这样两个问题:
消息还是不够即时。换言之,假如正好在某次询问之后服务器收到了消息,那么这条消息的获取延迟可能达到至少十秒。
大量的请求 - 响应,带宽和服务器资源浪费。如果你开着聊天工具页面一个小时,除了这一条消息,却没有进一步的聊天行为,于是按照每十秒发送一次请求计算,一共发起了 360 次请求,而其中居然只有 1 次返回了聊天消息是有实际意义的。
显然,轮询这个方案不好。说到底,其实我们并没有抛开对 HTTP 的已有印象,从问题本身出发去思考解决问题的最佳方式,而是潜意识地受限于 HTTP 的传统交互模式,考虑其中的变通方法。
在进一步分析之前,我们先来看两个容易弄混的概念:Pull 和 Poll。
“Pull”指的是去主动发起行为获取消息,一般在客户端 / 服务器(C/S,Client/Server)或浏览器 / 服务器(B/S,Browser/Server)交互中,客户端或浏览器主动发起的网络请求数据的行为。
而“Poll”,尽管在某些场景下也和 Pull 通用了,但在计算机网络的领域里,通常把它解释为“轮询”,或者“周期性查询”,在 Pull 的基础上增加了“周期性”的概念,这也是它和 Pull 相比最本质的区别。
相应地,和 Pull 行为相对的,从服务端主动发起,发送数据到客户端的行为叫做“Push”。Push 相比 Pull 而言,具备这样两个明显的优势:
高效性。如果没有更新发生,就不会有任何更新消息推送的动作,即每次消息推送都发生在确确实实的更新事件之后,都是有意义的,不会出现请求和响应的资源浪费。
实时性。事件发生后的第一时间即可触发通知操作,理论上不存在任何可能导致通知延迟的硬伤。
可是,有趣的是,事实上 Pull 的应用却远比 Push 更广泛,特别是在分布式系统中。这里有多个原因,其中很重要的一条是:
服务端不需要维护客户端的列表,不需要知晓客户端的情况,不需要了解客户端查询的策略。这有助于把服务端从对客户端繁重的管理工作中解放出来,而成为无状态的简单服务,变得具备幂等性(idempotent,指执行多次和执行一次的结果一样),更容易横向扩展。
尤其在分布式系统中,状态经常成为毒药,有了状态,就不得不考虑状态的保存、丢失、一致性等问题,因此这种无状态往往可以很大程度地简化系统的设计。

服务端推送技术

有了这些基础知识,我们就可以来谈谈实际的服务端推送技术了,这些都从一定程度上解决了 HTTP 传统方式 Pull 的弊端。

1. Comet

严格说,Comet 是一种 Web 应用客户端和服务端交互的模型,它有几种服务端推送的具体实现,但是,它们的大致原理是一样的:客户端发送一个普通的 HTTP 请求到服务端以后,服务端不像以往一样在处理后立即返回数据,而是保持住连接不释放,每当有更新事件发生,就使用分块传输的方式返回数据(如果你忘记了块传输的方式,请回看 [第 1 讲])。
若干次数据返回以后可以完成此次请求响应过程(分块传输返回长度为 0 的块,表示传输结束),等待客户端下一次请求发送。这种过程看起来也属于轮询,但是每个周期可包含多次服务端数据返回,因而也被形象地称为“长轮询”(Long Polling)。
在服务端推送技术中,Comet 最大的好处是,它 100% 由 HTTP 协议实现,当然,分块传输要求 HTTP 至少是 1.1 版本。但也正因为这点,它也存在一些弊端,比如,客户端必须在服务端结束当次传输后才能向服务端发送消息;HTTP 协议限制了它在每次请求和响应中必须携带完整的头部,这在一定程度上也造成了浪费(这种为了传输实际数据而使用的额外开销叫做 overhead)。
下面我给出了一个 Comet 实现的示例图。浏览器在发出 1 号请求要求数据,连接保持,接着陆续收到几个不同大小的响应数据,并且最后一个大小为 0,浏览器被告知此次传输完成。过了一会儿,浏览器又发出 2 号请求,开始第二轮的类似交互。
在 Comet 方式下,看起来服务端有了推送行为,其实只是对于客户端请求有条件、讲时机的多次返回,因此我们把它称为服务端“假 Push”。

2. WebSocket

HTML 5 规范定义了 WebSocket 协议,它可以通过 HTTP 的端口(或者 HTTPS 的端口)来完成,从而最大程度上对 HTTP 协议通透的防火墙保持友好。但是,它是真正的双向、全双工协议,也就是说,客户端和服务端都可以主动发起请求,回复响应,而且两边的传输都互相独立。
和上文的 Comet 不同,WebSocket 的服务端推送是完全可以由服务端独立、主动发起的,因此它是服务端的“真 Push”。
WebSocket 是一个可谓“科班出身”的二进制协议,也没有那么大的头部开销,因此它的传输效率更高。同时,和 HTTP 不一样的是,它是一个带有状态的协议,双方可以约定好一些状态,而不用在传输的过程中带来带去。而且,WebSocket 相比于 HTTP,它没有同源的限制,服务端的地址可以完全和源页面地址无关,即不会出现臭名昭著的浏览器“跨域问题”。
另外,它和我们之前学习的加密传输也丝毫不冲突,由于它在网络分层模型中位于 TLS 上方,因此他可以使用和 HTTP 一样的加密方式传输:
HTTP → WS
HTTPS → WSS
最后,最有意思的事情在于,和我们之前的认识不同,WebSocket 是使用 HTTP 协议“升级”的方法来帮助建立连接的,下面我们动手来试一试。
首先,我们需要找到一个可以支持 WebSocket 测试的网站,比如 websocket.org,然后我们将使用 Chrome 的网络工具来捕获和显示通过浏览器发送和接收的消息。如果这是你第一次使用 Chrome 的开发者工具,那么你需要好好熟悉它了,因为它将在你今后全栈的道路上派上大用场。
使用 Chrome 打开 Echo Test 页面,在这里你可以发送建立一个 WebSocket 连接。但是别急,我们先打开 Chrome 的开发者工具,并选中 Network 标签,接着点击左上角的清除按钮,把已有页面加载的网络消息清除掉,以获得一个清爽的网络报文监视界面:
接着,确保页面上建立 WebSocket 连接的对端地址和传递的信息都已经填写,比如:
Location:
wss://echo.websocket.org
Message:
Rock it with HTML5 WebSocket
于是就可以点击“Connect”按钮了,旁边的日志框将出现“CONNECTED”字样,同时,Chrome 开发者工具将捕获这样的请求(如果在开发者工具中网络监视界面上,选中消息的消息头处于“parsed”展示模式,你需要点击 Request Headers 右侧的 “view source” 链接来查看原始消息头):
GET wss://echo.websocket.org/?encoding=text HTTP/1.1
Host: echo.websocket.org
Origin: https://www.websocket.org
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: xxx
... (省略其它 HTTP 头)
好,你可以看到,这是一个普普通通的 HTTP GET 请求,但是 URL 是以加密连接“wss”开头的,并且有几个特殊的 HTTP 头:Origin 指出了请求是从哪个页面发起的,Connection: Upgrade 和 Upgrade: websocket 这两个表示客户端要求升级 HTTP 协议为 WebSocket。
好,再来看响应,消息的头部为:
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Sec-WebSocket-Accept: xxx
Upgrade: websocket
... (省略其它 HTTP 头)
嗯,返回码是 101,描述是“Web Socket Protocol Handshake”,并且,它确认了连接升级为“websocket”的事实。

3. 更多推送技术

到这里,我已经介绍了几种服务端的推送技术,事实上还有更多,但是,如果你依次了解以后认真思考,就会发现,这些原理居然都在某种程度上和我介绍的 Comet 和 WebSocket 这两种类似,有的甚至来自于它们。
这些技术包括:
SSE,即 Server-Sent Events,又叫 EventSource,是一种已被写入 HTML 5 标准的服务端事件推送技术,它允许客户端和服务端之间建立一个单向通道,以让服务端向客户端单方向持续推送事件消息;
为了提高性能,HTTP/2 规范中新添加的服务端推送机制,我们在 [第 01 讲] 中提到过,并在该讲的扩展阅读中有它的原理介绍;
WebRTC,即 Web Real-Time Communication,它是一个支持网页进行视频、语音通信的协议标准,不久前已被加入 W3C 标准,最新的 Chrome 和 Firefox 等主流浏览器都支持;
还有一些利用浏览器插件和扩展达成的服务端推送技术,比如使用 Flash 的 XMLSocket,比如使用 Java 的 Applet,但这些随着 HTML 5 的普及,正慢慢被淘汰。
你看,通过学习一两个典型的技术,再拓展开,去类比了解和分析思考同一领域内的其它技术,就能掌握到最核心的东西,这就是我推荐的一种学习全栈技术的方式。

总结思考

今天我们从 HTTP 的交互局限性引出了网络交互中 Pull 和 Push 的两大模型,比较了它们的优劣。服务端 Push 的方式具备高效性和实时性的优势,而客户端 Pull 的方式令服务端免去状态的维护,从根本上简化了系统。
之后我们以 Comet 和 WebSocket 为重点,介绍了服务端推送的不同方式,尤其是用了实际抓包分析,介绍了通过 HTTP “升级”的方式来建立 WebSocket 连接的原理。
今天学习得怎样呢?来看这样两个问题:
文中介绍了 Push 和 Pull 在原理上的不同,在你的实际项目中,是否应用了 Push 或 Pull 的模型呢?
文中介绍了 Push 比 Pull 具备高效性和实时性的优势,而 Pull 比 Push 则具备使得服务变得无状态的优势,除了最重要的这几个,你还能说出更多它们各自的优势吗?
今天的内容就到这里。以 HTTP 协议为核心,介绍网络协议的三讲文章已经更新完毕了,你是否对于全栈技术本身,还有适合自己的学习方法,有了新的理解呢?欢迎留言和我讨论。

扩展阅读

文中提到了跨域问题,如果感兴趣,推荐你阅读 MDN 的 HTTP 访问控制(CORS)这篇文章。
TutorialsPoint 的 WebSocket 系统教程,对于本文介绍的 WebSocket 协议,需要进一步了解的一个好去处。
关于 HTTP Update 头的 RFC 2616 协议片段和 WebSocket 的 RFC 6445,你也许对响应和请求中的其它 HTTP 头心存疑问,和之前介绍的 HTTP 的 RFC 协议一样,你通常不需要仔细阅读,但它是对协议有问题时的最终去处。
Stream Updates with Server-Sent Events,一篇非常好的介绍 SSE 基础,和同类技术比较优劣,并给出代码示例的文章;如果你对 WebRTC 感兴趣,那么可以先看看这个胶片,再阅读这篇基础知识 Getting Started with WebRTC
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 7

提建议

上一篇
02 | 为HTTP穿上盔甲:HTTPS
下一篇
04 | 工整与自由的风格之争:SOAP和REST
unpreview
 写留言

精选留言(20)

  • LiYanbin
    2020-01-09
    感谢回答!我的表述有误,其实我是想知道,服务器推送技术,像websock这种技术,其实就是服务器和客户端建立一条连接,建立连接可以基于TCP也可以基于UDP,但是却很少见到基于UDP的上层应用协议。 如果说是因为TCP可靠性,时序性,保活这些特性才选择TCP,那这些特性转移到应用层这一层来维护也非不可。 那究竟是何原因导致在服务器推送技术领域中,没有广泛应用的基于UDP的上层应用协议呢?
    展开

    作者回复: 我是这样认为的: 就像你说的,比如可靠性,保序性,拥塞控制等等,完全是可以放到应用层来做的。 但是,对应用层来说,它们关心的内容,不需要,也不应该包括这些传输方面的基本特性,或者说,已经有传输层的TCP把这些事做好了。而协议的设计者,关心他/她设计的协议想解决的问题,这些传输方面的特性,拿来用就可以了,而不需要再实现一遍。 往大了说,软件分层的目的最大目的,就是解耦。解耦之后“谁”做“什么事”就是每一层的职责,7层/5层模型里的传输层也一样。

    共 2 条评论
    2
  • LiYanbin
    2020-01-06
    服务端能够推送消息给客户端,就需要事先知道客户端开放的端口。这就不难理解各种服务端推送技术了。
    3
  • 学不动了
    2019-09-29
    老师你好,我有个疑问,是关于comet: 客户端必须在服务端结束当次传输后才能向服务端发送消息,服务端会采用分块传输,也就是"我说完你再说",客户端这时候有新的请求就只能等着才能发送?要是长时间不结束客户端的请求就没法发送了嘛?

    作者回复: 好问题,是这样的,HTTP/1.1 Comet 的一个局限性就在这里,也就是说,传输是单工的,从协议的角度来说,服务端的响应分多次返回,因此这个过程中,返回并没有传输完,那么对于该次 HTTP 连接来说,客户端也就无法发送新的请求。 当然,除了升级到 HTTP/2 或者采用其他技术以外,对于这个问题本身,还是有一些改进办法的,你可以思考一下。:)

    共 2 条评论
    1
  • Chocolate
    2019-09-16
    第一个问题,我们的后台系统里的二维码扫码登录用的就是 Pull 的形式,每隔一段时间就去轮询有没有扫码,有没有确认登录,对于后台这类系统是可以满足需求的;在调度系统里的状态实时推送里用到了 WebSocket 技术,可以实时反馈任务的执行情况。 第二个问题,Push 模型的优势是高效性和实时性,Pull 模型的优点除了无状态外,优势可能是实现起来更方便,不用引入新的框架
    展开

    作者回复: 对于大多数分布式的系统来说,Pull 确实要比 Push 实现起来方便一些,但是也不绝对。至于你说的“是否引入新的框架”这个描述比较模糊,两种方式和是否引入框架没有必然联系。

    共 2 条评论
    1
  • 记忆犹存
    2019-09-16
    Push 的模型 优点:响应及时 缺点: a.如果客户端接收不及时,造成数据包堆积; b. 需要客户端做流控处理; Pull模型 优点: a. 提取消息方便 b. 传输失败不用重试 缺点:实时性差
    展开

    作者回复: 感谢分享! 对于 Push 模型,你说的内容都是正确的,就提一点,对于你提到的数据包堆积和流控的问题,可以引入第三方的消息队列服务来协调消息发布者和订阅者,来缓解你说的问题。 对于 Pull 模型,“传输失败不用重试”这个不对,因为是不是重试并非 Push 或 Pull 的区别,二者都可以重试,也都可以不重试,取决于设计。其它正确。 当然,这是一个开放性问题,还有其它的优缺点,你也可以看看其他人的答复。

    1
  • 許敲敲
    2019-09-16
    谢谢老师给这么多资料
    2
  • richie
    2021-01-01
    热情似火的优秀
  • Geek_02f3e8
    2020-09-01
    老师讲的非常详细,我还想额外补充一个日常使用webcosket时需要注意的点。 websocket在解除跨域限制的同时,也产生了新的安全问题:在upgrade到ws协议时,服务端对于origin字段校验的缺失可能会导致websocket跨域劫持的安全问题。 攻击者诱导受害者点击包含一段以ws协议发送指定恶意内容的js的钓鱼页面,即可以受害者身份发送该ws数据,这是一个类似HTTP CSRF的问题:CSWH(Cross-site websocket-hijacking)。
    展开

    作者回复: 赞

    1
  • LiYanbin
    2020-01-06
    对于服务端推送技术,之前有考虑过,有一个疑问,为何客户端和服务器端为何不直接通过UDP进行通信呢?换句话说,为何在服务器推送技术中没有一种 比较被广泛应用的+基于UDP的 应用层协议呢?

    作者回复: 其实基于 UDP 的应用层协议也有一些,常听说的比如有 DHCP。之所以我们平时接触的不多,主要还是由它们的特点所决定的,你可以看一下 https://en.wikipedia.org/wiki/User_Datagram_Protocol#Comparison_of_UDP_and_TCP 比如其中提到的可靠性和有序性就是我们一般应用层的基本通信要求。 另外一个,HTTP/3 也会使用 UDP 代替 TCP,主要原因还是在物理设备改进,传输质量提升了以后,出于性能的考虑。可以参见:https://thenewstack.io/http-3-replaces-tcp-with-udp-to-boost-network-speed-reliability/

    1
  • 💢 星星💢
    2019-11-15
    老师我看不懂英文资料,是不是多看看就会有感觉了?有没有啥秘诀。。。

    作者回复: 最佳实践那一章的最后一篇特别放送,我会讲一讲程序员怎么学英语。可以期待一下。

  • liansng
    2019-10-22
    Location: wss://echo.websocket.org Message: Rock it with HTML5 WebSocket 这个的操作方式我没太看懂,老师可以帮忙解释一下吗?
    展开

    作者回复: 这是你打开 https://www.websocket.org/echo.html 的时候,它已经默认帮你填好的内容

  • vip3
    2019-10-16
    老师您好,请教个问题。 我想让pad和PC之上的两个应用全双工通信,想到两种办法。 一是和服务器建立websocket连接,让其作为通讯的中转,但是这种方法网络不好的情况下容易挂掉; 为了解决办法一的问题,想了第二个方法,在PC上启动一个websocket server,让pad应用与PC应用内启的这个server建立连接,实现局域网内部全双工通信,但这有个限制,必须要求pad和PC连到同一局域网下。但是因为PC可能连接着网线,也可能在用无线网络,且无线网络不止一个,我检测不出来PC和pad连接的是否是同一子网。 所以想问问老师,有没有什么办法能解决上述两个办法的问题?
    展开
    共 1 条评论
  • CC
    2019-09-25
    思考题 1: 工作中主要用到 Pull,还没有使用到 Push。 思考题 2: Push 的优势还有: - 客户端可以提前 cache push 过来的内容,在其他页面上使用。 - 服务端可以决定 push 的优先级,把影响 performance 的重要资源优先 push。 - 客户端可以根据自己的情况,选择是否接受 push,或者限制数量。 Pull 的 stateless 带来的优势强大,其他的优势还没有查到。
    展开

    作者回复: 👍,你也可以看看其他回答

  • pyhhou
    2019-09-24
    思考题: 1. 在实际的项目中用 pull 的例子比较多,没怎么接触过 push,感觉上 push 的话是不是会相对来说复杂些,因为要维护不同 client 的状态;相对来说 pull 的话,只用基于 request 参数做相应的 response 即可 2. 这里我在想,pull 的无状态优势只是对于服务器端来说的,如果在客户端,对于 pull 来说,是不是需要保存服务器的状态?另外,由于 push 是有状态的,那么状态在服务器上的存储会不会成为一个难题?从分析来看,pull 的缺点是 push 的优点,push 的缺点又是 pull 的优点,我想到了前一章我们讲过对称加密和非对称加密,对称加密高效但是 key 不易分享,非对称加密 key 易分享但是不高效,因此 HTTPS 将他们两个做了个结合。这里或许我们也可以将 push 和 pull 结合起来使用,比如,消息更新的时候,服务器端使用 push 通知客户端更新资源,然后客户端使用 pull 获取资源,这样会不会比单一使用 pull 或 push 更好? 很赞同老师说的,学习就是需要去用自己学过的东西对比理解和分析新学的东西,深度理解他们的本质思想,以及区别和共同点,这样才能形成知识体系,这会对新知识的掌握更快,对技术也会有更高维度的认识 关于学习全栈专栏的认识: 学完了这几章,感觉自己知道的东西还是太少,老师推荐的文章都很好,也很有深度,但是有些文章真的没有时间去仔细推敲,我在想,先把整个专栏跟下来,对全栈知识有个宏观的认识,然后在仔细阅读这些著作,老师你觉得这样是否可行呢? 收获很多,谢谢老师
    展开

    作者回复: 感谢回复。 1. 正确。 2. 不一定。而且即便客户端需要保持服务端状态,也比服务端保持客户端状态,要简单得多(数量上是 1 和 N 的关系,在横向扩展的时候,这个 N 就可能成为瓶颈) 你说的这个 push-pull 结合的方式很好,其实它是一种非常常见的实现方式,你可以想想互联网有哪些应用。:)

    共 2 条评论
  • jxs1211
    2019-09-22
    请问今天的课堂实例如果要自己用代码来实现,前后段代码是否有相关的实例代码可以做参考?

    作者回复: 你可以自己尝试实现一个最简单的 Comet: 客户端,你甚至都不用写 JavaScript,一个普通的 get 请求即可——浏览器直接访问某一个服务端暴露的接口(URL)。 服务端则是往响应中写数据,使用按块传输,返回类型设置为 plain/text,随便写一点数据并 flush,然后 sleep 10 秒,再写一点并 flush,再 sleep 10 秒,如是多次,这样可以模拟多次返回。 这样,你会看到浏览器一直在加载,页面十秒十秒地新增新的内容。

    1
  • sky
    2019-09-18
    之前一个项目中IM是用websocket实现的。
  • 靠人品去赢
    2019-09-17
    老师你好,文章写的真好?但是我有点疑问,就是看comet是一个单工的,也就是“我说完了,你再说”。但是他还是一个基于HTTP协议的,我的理解就是keep-alive。那问题来了我要是用HTTP2.0的话是不是可以把comet转化为双工的呢,因为HTTP2.0是双工通讯?

    作者回复: HTTP/2 确实有了专门的 server push,但这个最主要用来解决资源的预加载问题。因此,如果要完全替代 Comet,实现任意时刻的推送,可以使用 SSE 等技术,或者是将 server push 和 SSE 等技术结合起来(即让浏览器端预留一个回调来响应服务端的推送)。

  • 四喜
    2019-09-16
    老师讲得真好。 提问🙋,Comet这种假PUSH方式,由客户端发起的HTTP请求服务器不释放,但是会受超时限制吧?

    作者回复: 非常好的问题! 是的,完全正确。你可以考虑这样几点: 1. 客户端和服务端,还有网关等中间节点,都要商定好一个最大的超时时间。并且,业务端配置的超时时间要稍短于其它,以留足余量。 2. 服务端即便没有消息需要推送,也要定期返回一个无业务意义的消息,以避免超时发生。 3. 客户端如果在若干个推送周期后,可以结束连接,休息一段时间,再重新连接。

    1
  • 许童童
    2019-09-16
    四火老师讲得真好,服务端推送模型最后都可以看作是Comet和WebSocket的变形。

    作者回复: 谢谢。 很多技术都是相通的,学习全栈技术都是可以用类比的方式进行的。

  • 饭团
    2019-09-16
    1)公司一个简单的聊天业务系统!就用了push模型! 2)push模型处理信息比poll稍微复杂一些!存在解包封包!需要定一套消息结构!

    作者回复: 感谢分享! 关于第 2) 个答复:解包和封包其实并不是 push 和 poll 的本质区别,poll 也可以由解包封包,也可以定义一套消息结构。

    共 2 条评论