术→技巧, 研发

Web应用中的实时消息技术

钱魏Way · · 547 次浏览

在互联网应用中,很多时候我们需要在客户端和服务端之间进行实时的消息交互,比如下面这些场景:

  • SNS网站用户互动消息通知(weibo/twitter)
  • 门户网站实时滚动新闻(突发事件)、文字直播(体育赛事)
  • 在线聊天室(在线客服)
  • 实时数据展示(实时股价,实时商品价,服务器实时监控等)

接下来就来看看web开发中常见的实时消息的实现技术方案,每种方案都各有优缺点,在不同的应用场景下有不同的选择。

Web领域的实时推送技术,也被称作Realtime技术。这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新。它有着广泛的应用场景,比如在线聊天室、在线客服系统、评论系统、WebIM等。

普通的http

HTTP 协议有一个特点:被动性。何为被动性呢,其实就是,服务端不能主动联系客户端,只能由客户端发起。举例来说,我们想获得某个数据,就得是客户端(如浏览器)向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息,比如收到新邮件的提示。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。

  • 客户端从服务器端请求网页
  • 服务器作出相应的反应
  • 服务器返回相应到客户端

在WebSocket协议之前,有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和iframe流(streaming)。

常见实时消息技术

AJAX Polling(轮询)

轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

  • 客户端使用普通的http方式向服务器端请求网页
  • 客户端执行网页中的JavaScript轮询脚本,定期循环的向服务器发送请求(例如每5秒发送一次请求),获取信息
  • 服务器对每次请求作出响应,并返回相应信息,就像正常的http请求一样

客户端定时请求服务器来询问是否有新的消息产生,这种情况下客户端每次请求都要建立一次http连接,服务器都要产生一个响应信息。

用大白话举例就是:

while True:
 客户端:妹子,请你吃饭有空吗?(Request)
 服务端:没有!(Response)
 客户端:妹子,请你吃饭有空吗?(Request)
 服务端:没有。。(Response)
 客户端:妹子,请你吃饭有空吗?(Request)
 服务端:你好烦啊,没有啊。。(Response)
 客户端:妹子,请你吃饭有空吗?(Request)
 服务端:好啦好啦,有啦。(Response)
 客户端:妹子,请你吃饭有空吗?(Request)
 服务端:。。。。。没。。。。没。。。没有(Response)

可以看到,使用轮询的方式,客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。其缺点也很明显:连接数会很多,一个接受,一个发送。而且每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率。

  • 优点:实现简单,使用方便,开发成本很低,适合刚起步的小型应用或是其它方案的备用方案。
  • 缺点:消息有延迟,网络通信消耗大(特别是移动网络下),服务器容易产生峰值请求。
  • 实现:浏览器里用js定时请求,或是移动设备上nativeapp里定时发http请求。
  • 场景:适于小型应用。

AJAX Long-Polling(长轮询)

长轮询其实原理跟轮询差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

  • 客户端使用普通的http方式向服务器端请求网页
  • 客户端执行网页中的JavaScript脚本,向服务器发送数据、请求信息
  • 服务器并不是立即就对客户端的请求作出响应,而是等待有效的更新
  • 当信息是有效的更新时,服务器才会把数据推送给客户端
  • 当客户端接收到服务器的通知时,立即会发送一个新的请求,进入到下一次的轮询

还是用大白话举例:

while True:
 客户端:妹子,请你吃饭有空吗?没有的话就等有了再返回给我吧!(Request)
 服务端:(额。。现在好忙,先不回复他,电话先不挂。)
 服务端:现在有空了。(Response)
 客户端:妹子,请你吃饭有空吗?没有的话就等有了再返回给我吧!(Request)
 服务端:(额。。现在好忙,先不回复他,电话先不挂。)
 服务端:现在有空了。(Response)

本质上,长轮询是对轮询的改进版,客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待。当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和CPU利用率等问题。由于http数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费。

跟上面的polling模式相比:

优点:消息到达客户端更及时;减少了http请求不停地创建、关闭成的不必要浪费。在无消息的情况下不会频繁的请求。Long-polling支持大多数当前的浏览器。

缺点:服务端需要保持大量连接,http连接的维护开销较大;每次产生消息后都需要重新创建连接。

实现:客户端只要简单的发送请求,等待响应即可。服务器端需要做两方面的工作:一是保持大量的连接(Non-Blocking I/O);二是读取后台的消息更新(异步定时轮询或由事件触发)。是一个近实时的异步方式。

实例:WebQQ、Hi网页版、Facebook IM。

基于流的模式(http-streaming / iframe-streaming)

在这种情况下,客户端与服务器端保持一个持久的连接,服务器端有新消息产生时通过这个持久连接不断地返回给客户端。这种模式与上面的long polling差不多,差别就是只需要创建一次连接就可以了。另外,要注意http header中需要设置Connection: keep-alive和Transfer-Encoding: chunked这两个属性。跟上面longpolling模式相比:

  • 优点:消息可以实时到达客户端;客户端与服务端之间只需要建立一次连接。
  • 缺点:服务器端也要维持大量连接,开销很大。
  • 实现:客户端一般有两种方式:一是隐藏iframe的src指向服务器端url,不断地进行dom渲染;二是使用ajax里的XMLHttpRequest对象来实现。对于服务端来说,和long polling一样,要保持大量连接和处理后台的消息更新。

HTML5 Server Sent Events (SSE) / EventSource

传统意义上服务器端不会主动推送给客户端消息,一般都是客户端主动去请求服务器端获取最新的数据。 SSE就是一种可以主动从服务端推送消息的技术。

SSE的本质其实就是一个HTTP的长连接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream,所以客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。
  • 客户端使用普通的http方式向服务器端请求网页
  • 客户端执行网页中的JavaScript脚本,与服务器之间建立了一个连接
  • 当服务器端有有效的更新时,会发送一个事件到客户端
    • 服务器到客户端数据的实时推送,大多数内容是你需要的
    • 你需要一台可以做Event Loop的服务器
    • 不允许跨域的连接
    • 默认延时3秒,但是可以调整。
    • 除非Server-Sent Events不必在每一次响应发出后都关闭连接。
    • 支持Chrome9+、Firefox6+、Opera11+、Safari5+

HTML5 Websockets

WebSocket诞生于2008年,在2011年成为国际标准,现在所有的浏览器都已支持(详见《新手快速入门:WebSocket简明教程》)。它是一种全新的应用层协议,是专门为web客户端和服务端设计的真正的全双工通信协议,可以类比HTTP协议来了解websocket协议。

它们的不同点:

  • HTTP的协议标识符是http,WebSocket的是ws;
  • HTTP请求只能由客户端发起,服务器无法主动向客户端推送消息,而WebSocket可以;
  • HTTP请求有同源限制,不同源之间通信需要跨域,而WebSocket没有同源限制。

它们的相同点:

  • 都是应用层的通信协议;
  • 默认端口一样,都是80或443;
  • 都可以用于浏览器和服务器间的通信;
  • 都基于TCP协议。

两者和TCP的关系图:

  • 客户端使用普通的http方式向服务器端请求网页
  • 客户端执行网页中的JavaScript脚本,与服务器之间建立了一个连接
  • 服务器和客户端之间,可以双向的发送有效数据到对方
    • 服务器可以实时的发送数据到客户端,同时客户端也可以实时的发送数据到服务器
    • 你需要一台可以做Event Loop的服务器
    • 使用 WebSockets 允许跨域的建立连接
    • 它同样支持第三方的websocket主机服务器,例如Pusher或者其它。这样你只需要关心客户端的实现 ,降低了开发难度。

  • 优点:实时通讯,双向交互,节省服务器资源和带宽。真正的实时。
  • 缺点:浏览器支持不足。
  • 实现:客户端需要html5来实现,服务器端一般的web服务器都有支持。

目前WebSocket协议仍在开发中, Chrome和Safri浏览器默认支持WebSocket,而Firefox和Opera出于安全考虑,默认关闭了WebSocket,IE则不支持(包括9),目前WebSocket协议最新的为“76号草案”。有兴趣可以看以下资料。

如果你觉得这些还不够,想要了解更多,可以参考下面的文件和手册

Flash Socket

在页面中内嵌入一个使用了Socket类的 Flash 程序JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。

  • 优点:实现真正的即时通信,而不是伪即时。
  • 缺点:客户端必须安装Flash插件;非HTTP协议,无法自动穿越防火墙。(Flash已死)
  • 实例:网络互动游戏。

如何选择方案?

在我们实际的应用中,各种方案应该怎么选择?

polling轮询机制很简单,使用方便,都是http短连接,在应用架构上跟普通接口api一致,非常适合小型应用和刚起步的应用,可以节省很多开发成本。

当用户量上升到一定程度后(比如日活跃百万级),产品做得越来越受欢迎了,我们对消息到达的及时性也就有更高的要求了,而且这种轮询机制对服务器压力也是一种考验,这时候我们就可以考虑long polling或http streaming的方式了,这两种方式其实在服务端实现上差不多,主要是保持大量长连接和异步(或事件触发)获取消息。

在保持连接方面,在java的nio出现后,为我们提供了便利,主流的应用服务器像tomcat和jetty都有支持,但是,工作在jvm上的服务器保持大量socket连接时gc是个很严重的问题。可以借助nginx的push模块来实现保持连接以及消息实时推送。

这两个模块工作在nginx上,可以维持大量的http连接,实现了pub/sub协议来支持消息发送,基本流程是下图这的,发送者(publisher)把消息推送到nginx服务器上,然后push模块负责把消息发送给订阅者(client):借助nginx的这个push模块,我们可以省去大量的工作,应用程序只需要关注业务逻辑(也就是publisher做的事),简化了应用架构,同时nginx的高性能也有不错的表现。

当我们的产品发展到千万级的日活跃时,我们可能就要考虑更好的方案了,比如上面的提到的WebSocket,但是为浏览器兼容的问题,这个方案并不是主流的解决方案。一般这种情况下,我们就要考虑基于tcp的socket长连接模式了,通过某种消息推送协议(xmpp/mqtt/自定议协议等)来实现客户端和服务器端的实时交互。

Socket.IO

Socket.IO是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架,它包括了客户端的JavaScript和服务器端的Node.js。是一个为实时应用提供跨平台实时通信的库。socket.io 旨在使实时应用在每个浏览器和移动设备上成为可能,模糊不同的传输机制之间的差异。socket.io的名字源于它使用了浏览器支持并采用的HTML5 WebSocket 标准,因为并不是所有的浏览器都支持 WebSocket ,所以该库支持一系列降级功能:

  • Websocket
  • Adobe® Flash® Socket
  • AJAX long polling
  • AJAX multipart streaming
  • Forever Iframe
  • JSONP Polling

在大部分情境下,你都能通过这些功能选择与浏览器保持类似长连接的功能。

Socket.IO设计的目标是构建能够在不同浏览器和移动设备上良好运行的实时应用,如实时分析系统、二进制流数据处理应用、在线聊天室、在线客服系统、评论系统、WebIM等。Socket.IO已经具有众多强大功能的模块和扩展API,如(session.socket.io)(http session中间件,进行session相关操作)、socket.io-cookie(cookie解析中间件)、session-web-sockets(以安全的方式传递Session)、socket-logger(JSON格式的记录日志工具)、websocket.MQ(可靠的消息队列)、socket.io-mongo(使用MongoDB的适配器)、socket.io-redis(Redis的适配器)、socket.io-parser(服务端和客户端通讯的默认协议实现模块)等。

Socket.IO实现了实时、双向、基于事件的通讯机制,它解决了实时的通信问题,并统一了服务端与客户端的编程方式。启动了Socket以后,就像建立了一条客户端与服务端的管道,两边可以互通有无。它还能够和Express.js提供的传统请求方式很好的结合,即可以在同一个域名,同一个端口提供两种连接方式:request/response, websocket(flashsocket,ajax…).

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注