默认
打赏 发表评论 7
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
使用WebSocket和SSE技术实现Web端消息推送
微信扫一扫关注!

1、前言


HTTP 服务器推送也称为 HTTP 流,是一种客户端-服务器通信模式,它将信息从 HTTP 服务器异步发送到客户端,而无需客户端请求。在高度交互的 Web 或移动应用程序中,一个或多个客户端需要连续不断地从服务器接收信息,服务器推送架构对这类应用程序特别有效。在本文中,您将了解 WebSocket(《新手快速入门:WebSocket简明教程》) 和 SSE(《SSE技术详解:一种全新的HTML5服务器推送事件技术》),它们是实现 HTTP 服务器推送的两种技术。

我首先将概述两种解决方案之间的技术差异,以及如何在 Web 架构中实现每种解决方案。我将通过一个示例应用程序,向您展示如何设置一个 SSE 实现,然后介绍一个类似的 WebSocket 实现。最后,我将比较两种技术,并提出我关于在不同类型 Web 或移动应用程序中使用它们的结论。

请注意,本文要求熟悉 HTTP 服务器推送的语言和概念。两个应用程序都是在 Python 中使用 CherryPy 编写的。

2、参考资料


新手入门贴:史上最全Web端即时通讯技术原理详解
Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
SSE技术详解:一种全新的HTML5服务器推送事件技术
Comet技术详解:基于HTTP长连接的Web端实时通信技术
新手快速入门:WebSocket简明教程
WebSocket详解(一):初步认识WebSocket技术
WebSocket详解(二):技术原理、代码演示和应用案例
WebSocket详解(三):深入WebSocket通信协议细节
socket.io实现消息推送的一点实践及思路
LinkedIn的Web端即时通讯实践:实现单机几十万条长连接
Web端即时通讯技术的发展与WebSocket、Socket.io的技术实践
Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)
开源框架Pomelo实践:搭建Web端高性能分布式IM聊天服务器
>> 更多同类文章 ……

3、传统“请求-响应”模式的局限性


网络上的客户端-服务器通信在过去曾是一种请求-响应模型,要求客户端(比如 Web 浏览器)向服务器请求资源。服务器通过发送所请求的资源来响应客户端请求。如果资源不可用,或者客户端没有权限访问它,那么服务器会发送一条错误消息。在请求-响应架构中,服务器绝不能向客户端发送未经请求的消息。

随着 Web 应用程序变得更强大和更具交互性,请求-响应模型的局限性也开始显现出来。需要更频繁更新的客户端应用程序被要求更频繁地发送 GET 请求。这种技术称为轮询,在高峰期间,这可能会使服务器不堪重负,并导致性能问题。该技术效率低下,因为客户端发送的许多请求都没有返回更新。此外,客户端只能按指定间隔进行轮询,这可能减缓客户端的响应速度。

HTTP 服务器推送技术的出现,就是为了解决与频繁轮询相关的性能问题和其他局限。尤其是对于交互式 Web 应用程序,比如游戏和屏幕共享服务,Web 服务器能更高效地在新数据可用时向客户端发送更新。

关于“请求-响应”模式的详细原理,请参见《新手入门贴:史上最全Web端即时通讯技术原理详解》。

4、比较 WebSocket 与 SSE


1基于区别


WebSocket 和 SSE 都是传统请求-响应 Web 架构的替代方案,但它们不是完全冲突的技术。WebSocket 架构在客户端与服务器之间打开一个套接字,用于实现全双工(双向)通信。无需发送 GET 消息并等待服务器响应,客户端只需监听该套接字,接收服务器更新,并使用收到的数据来发起或支持各种交互。客户端也可以使用套接字与服务器通信,例如在成功收到更新时发送 ACK 消息。

SSE 是一种更简单的标准,是作为 HTML5 的扩展而开发的。尽管 SSE 支持从服务器向客户端发送异步消息,但客户端无法向服务器发送消息。对于客户端只需接收从服务器传入的更新的应用程序,SSE 的半双工通信模型最适合。与 WebSocket 相比,SSE 的一个优势是它是基于 HTTP 而运行的,不需要其他组件。

对于需要在客户端与服务器之间频繁通信的多用途 Web 应用程序,显然应该选择 WebSocket。对于希望从服务器向客户端传输异步数据,而不需要响应的应用程序,SSE 更适合一些。

2浏览器支持


在比较 HTTP 协议时,浏览器支持是一个必要的考虑因素。浏览表 1 中的数据,可以看到所有现代浏览器都支持 WebSocket 协议,包括移动浏览器。Microsoft IE 和 Edge 不支持 SSE

表 1. 2017 年 1 月浏览器使用分布:
111.png

对于必须在所有浏览器中运行的应用程序,WebSocket 目前是更好的选择。

3开发工作量


在比较协议时,尤其是 WebSocket 和 SSE 等较新的协议,工作量是要考虑的另一个因素。这里的 “工作量” 指的是代码行,或者您要花多少时间为使用给定协议的应用程序编写代码。对于具有严格的时间限制或开发预算的项目,这个指标特别重要。

实现 SSE 的工作量比 WebSocket 要少得多。它适合任何使用 HTML5 编写的应用程序,主要负责在从服务器发送到客户端的消息中添加一个 HTTP 标头。如果给定了正确标头,客户端就会自动将消息识别为服务器发送的事件。不同于 WebSocket,SSE 不需要在服务器与客户端之间建立或维护套接字连接。

WebSocket 协议要求配置一个服务器端套接字来监听客户端连接。客户端自动打开一个与服务器的套接字并等待消息,消息可以异步发送。每个应用程序都可以定义自己的消息格式、持久连接(脉动信号)策略等。表 2 总结了 SSE 和 WebSocket 协议的优劣。

表 2. 比较 SSE 与 WebSocket:
222.png

现在让我们通过一个简单的 Web 应用程序示例来了解每种技术的工作原理。

5、开发 SSE 应用程序


SSE 是一种仅使用 HTTP 传送异步消息的 HTML5 标准。不同于 WebSocket,SSE 不需要在后端创建服务器套接字,也不需要在前端打开连接。这大大降低了复杂性。

1SSE 前端


清单 1 给出了一个使用 SSE 的简单 HTTP 服务器推送应用程序的 UI 代码:
var source = new EventSource(“/user-log-stream”);
source.onmessage = function(event) {
var message = event.data; 
// do stuff based on received message 
};

EventSource 是一个与服务器建立 HTTP 连接的接口。服务器使用事件流格式 将消息发送到客户端,这种格式是一种使用 UTF-8 编码的简单的文本数据流。建立联系后,HTTP 连接会对给定消息流保持开放,以便服务器能发送更新。在清单 1 中,我们创建了一个 HTTP 连接来接收与 /#tabs/user-log-stream URI 相关的事件。

2SSE 后端


在后端,我们为 URL /user-log-stream 创建了一个分派器。该分派器将从前端接收连接请求,并发回一条异步消息来发起通信。SSE 客户端不能向服务器发送消息。
清单 2 中的代码演示了后端代码。

清单 2. SSE 后端:
import cherrypy
  
class UserLogStream
    messages = []
  
    @cherrypy.expose
    def stream(self):
        cherrypy.response.headers["Content-Type"] = "text/event-stream"
        while True:
            if len(messages) > 0:
                for msg in messages:
                    data = “data:” + msg + “\n\n”
                    yield data
                 messages = []
  
routes_dispatcher = cherrypy.dispatch.RoutesDispatcher()
routes_dispatcher.connect(‘user-log-stream’, ‘/’, controller = UserLogStream(), action=’stream’)

UserLogStream 类的 stream 方法被分配给每个与 /user-log-stream URI 连接的 EventSource。任何待发送消息都将被发送到已连接的 EventSource。请注意,消息格式不是可选的:SSE 协议要求消息以 data: 开头,以 \n\n 结尾。尽管这些示例中使用了 HTTP 作为消息格式,但也可以使用 JSON 或另一种格式发送消息。

尽管这是一个非常基本的 SSE 实现示例,但它演示了该协议的简单性。

6、开发 WebSocket 应用程序


像 SSE 示例一样,WebSocket 应用程序基于 CherryPy(一种 Python Web 框架)而构建。Project WoK 是后端 Web 服务器,python-websockify 库插件处理套接字连接。该插件是 Kimchi 的一部分。

组件包括:

  • Project WoK:后端 Python 逻辑,提供要由推送服务器广播的消息。
  • Websockify 代理:此代理由 python-websockify 库实现,它使用 Unix 套接字从推送服务器接收消息,并将这些消息传送到 UI 上连接的 WebSocket。
  • 推送服务器:一个普通 Unix 套接字服务器,它向已连接的客户端广播后端消息。
  • 前端:UI 与 Websockify 代理建立 WebSocket 连接,并监听服务器消息。根据具体的消息,将会执行一个特定操作,例如刷新一个列表或向用户显示一条消息。

1Websockify 代理


Websockify 代理由 python-websockify 库实现。它使用 CherryPy Web 服务器引擎来实现初始化,如清单 3 所示。

清单 3. Websockify 代理:
params = {'listen_host': '127.0.0.1',
          'listen_port': config.get('server', 'websockets_port'),
          'ssl_only': False}
  
# old websockify: do not use TokenFile
if not tokenFile:
    params['target_cfg'] = WS_TOKENS_DIR
  
# websockify 0.7 and higher: use TokenFile
else:
    params['token_plugin'] = TokenFile(src=WS_TOKENS_DIR)
  
def start_proxy():
    try:
        server = WebSocketProxy(RequestHandlerClass=CustomHandler,
                                **params)
    except TypeError:
        server = CustomHandler(**params)
  
    server.start_server()
  
proc = Process(target=start_proxy)
proc.start()
return proc

Websockify 代理将所有已注册的令牌都绑定到它的 WebSocket URI,如下所示。

清单 4. 绑定已注册的令牌:
def add_proxy_token(name, port, is_unix_socket=False):
    with open(os.path.join(WS_TOKENS_DIR, name), 'w') as f:
        """
        From python documentation base64.urlsafe_b64encode(s)
        substitutes - instead of + and _ instead of / in the
        standard Base64 alphabet, BUT the result can still
        contain = which is not safe in a URL query component.
        So remove it when needed as base64 can work well without it.
        """
        name = base64.urlsafe_b64encode(name).rstrip('=')
        if is_unix_socket:
            f.write('%s: unix_socket:%s' % (name.encode('utf-8'), port))
        else:
            f.write('%s: localhost:%s' % (name.encode('utf-8'), port))

下面是添加一个名为 myUnixSocket 的 Websockify 代理条目的命令。

此条目通过代理将 WebSocket 连接传送到 Unix 套接字 /run/my_socket:
add_proxy_token(‘myUnixSocket’, ‘/run/my_socket’, True)

在 UI 中,我们使用以下 URI 打开一个与 Unix 套接字的 WebSocket 连接:
wss://<server_address>:<port>/websockify?token=<b64encodedtoken>

在 URI 中,b64encodedtoken 是 myUnixSocket 字符串的 base64 值,不包括 = 字符。有关此配置的更多信息,请参阅WebSocket 代理模块

2推送服务器


推送服务器有两个要求:

  • 它必须能同时处理多个连接;
  • 它必须能向所有已连接的客户端广播同一条消息。

出于此示例的用途,推送服务器将充当一个广播代理。尽管能够接收来自客户端的消息,但我们不希望这样做。

清单 5 给出了推送服务器的初始版的相关 Python 代码。(可访问 GitHub 上的 Project WoK 来获取 pushserver 模块的最终版本。)

清单 5. WebSocket 推送服务器:
class PushServer(object):
  
    def __init__(self):
        self.set_socket_file()
  
        websocket.add_proxy_token(TOKEN_NAME, self.server_addr, True)
  
        self.connections = []
  
        self.server_running = True
        self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET,
                                      socket.SO_REUSEADDR, 1)
        self.server_socket.bind(self.server_addr)
        self.server_socket.listen(10)
        wok_log.info('Push server created on address %s' % self.server_addr)
  
        self.connections.append(self.server_socket)
        cherrypy.engine.subscribe('stop', self.close_server, 1)
  
        server_loop = threading.Thread(target=self.listen)
        server_loop.setDaemon(True)
        server_loop.start()
  
  
    def listen(self):
        try:
            while self.server_running:
                read_ready, _, _ = select.select(self.connections,
                                                 [], [], 1)
                for sock in read_ready:
                    if not self.server_running:
                        break
  
                    if sock == self.server_socket:
  
                        new_socket, addr = self.server_socket.accept()
                        self.connections.append(new_socket)
                    else:
                        try:
                            data = sock.recv(4096)
                        except:
                            try:
                                self.connections.remove(sock)
                            except ValueError:
                                pass
  
        except Exception as e:
            raise RuntimeError('Exception occurred in listen() of pushserver '
                               'module: %s' % e.message)
  
  
    def send_notification(self, message):
        for sock in self.connections:
            if sock != self.server_socket:
                    sock.send(message)

PushServer() 类的 __init__() 将会初始化 Unix 套接字,以便监听连接。我们在 WebSocket 代理中也添加了一个令牌。此令牌将被前端用在 WebSocket 连接的 URI 中。

可通过许多方法实现服务器推送架构。出于广播服务器推送实现的用途,我认为使用一个监听器线程 (select.select()) 来控制已知连接是最简单的方法。在这里,listen() 方法在一个守护线程中运行,负责管理现有连接。select.select 方法以非阻塞函数的形式运行,它在来自 self.connections 数组的所有套接字都准备好读取时返回,或者在一秒超时后返回。建立新连接后,它会被添加到 self.connections 数组中。连接关闭时,会从数组中删除它。

listen() 方法可通过两种方法从数组中删除套接字:在 recv() 调用的 except 代码块中检测到套接字已关闭,或者收到一条 CLOSE 消息。send_notification() 方法负责将消息广播到订阅了它们的客户端。在客户端取消订阅时,我们使用了 except 代码块关闭套接字连接。不能单独使用 listen() 关闭客户端与服务器之间的套接字连接,因为这样做可能导致 send_notification() 方法中发生管道损坏错误。

下一节将更详细地解释这一点。

7、排除 WebSocket 前端的故障


后端设置和配置相对比较容易,但编写前端代码就没有那么简单了。从决定打开还是关闭浏览器连接到处理某些 API 的方式上的差异,WebSocket 前端带来了许多挑战。在解决这些挑战时,需要重新利用后端代码的某些元素。

第一个决定是为所有 UI 维护单个 WebSocket 连接,还是设置多个按需打开的 WebSocket 连接。我们可能会先考虑多个按需 WebSocket 连接。我们认为,仅在 UI 期望从后端收到一条特定消息时才应打开一个连接 — 例如等待某个任务完成。如果不想收到异步消息,则不需要打开连接。这会从 UI 和后端节约一些资源,但代价是需要管理每个已打开的 WebSocket 的生命周期。

对于期望从服务器收到大量异步消息的 UI,单个持久 WebSocket 连接可能更合适。这个持久连接被用作通知流,允许任何相关 UI 代码监听收到的消息并执行相关操作。

每个解决方案都各有用武之地,下面将试验每个解决方案。使用最适合您的应用程序的方法很重要。

1多个 WebSocket 连接


要打开和关闭 WebSocket,只需调用一个构造函数来打开和调用 close() 方法。但是,诸如页面刷新之类的浏览器行为上的差异可能让情况变得很复杂。每次打开 WebSocket 都会与 WebSocket 代理建立一个连接,然后与后端的实际推送服务器建立连接。该连接在关闭之前会保持打开。考虑以下代码。

清单 6. 在每条收到的消息上调用 refreshInterface():
notificationsWebSocket = new WebSocket(url, ['base64']);
notificationsWebSocket.onmessage = function(event) {
    refreshInterface();
};

在清单 6 中,onmessage 是一个事件处理函数,每次在套接字中收到一条新消息时都会调用它。refreshInterface() 函数在消息到达时被调用。如果打开多个 notificationsWebSocket 对象,则会调用 refreshInterface() 多次。

对前端和后端而言,打开比我们需要的更多的 WebSocket 连接显然是一种资源浪费。这还会带来意想不到的风险。为了解决这些问题,我们可以避免打开不必要的 WebSocket 连接,并在不再需要现有连接时关闭它们。可以手动或通过浏览器关闭 WebSocket。在我用于测试的浏览器中,在浏览器窗口刷新或关闭时,或者在域更改时,Chrome 和 Firefox 都会关闭已打开的 WebSocket 连接。

WebSocket 关闭时,会发生两件事:

  • 调用一个名为 onclose 的事件处理函数来执行任何附加清理;
  • 闭合端(在本例中为 UI WebSocket)启动一次关闭握手来提醒另一端。在此过程中,会发送一个值为 0x8 的关闭帧( close frame)。收到此帧时,后端会停止发送数据并立即关闭它所在的套接字端。无论 onclose 处理函数中配置了何种清理类型,此过程都会发生。

理想情况下,会调用 onclose 处理函数,而且推送服务器会收到关闭帧。遗憾的是,我在测试此阶段的实现时发生的过程不是这样的。

2Websockify 代理和关闭帧


截至编写本文时,Websockify 代理插件 (v0.8.0-2) 不会将关闭帧转发到 Unix 套接字,继而转发到推送服务器。因此,UI 套接字中的连接可能已关闭,但推送服务器毫不知情,仍使它所在的连接端保持打开。
推送服务器尝试将一条新消息广播到所有连接(包括已关闭的套接字)时,它会收到管道已损坏的错误。如果未正确处理,这可能导致推送服务器被终止运行。下面这个套接字发出了一条 send_notification 消息。

清单 7. send_notification 导致管道损坏错误:
def send_notification(self, message):
    for sock in self.connections:
        if sock != self.server_socket:
                sock.send(message)

处理这种情况的一个方法是将 sock.send() 调用包装在一个 try/except 中,然后处理管道损坏异常。一种替代方案是建立一次独特的关闭握手,使用 onclose 事件处理函数向后端服务器发送一条消息。推送服务器端然后关闭它自己的套接字连接,保持 send_notification 方法不变。

接下来我们将尝试该解决方案。

3使用 onclose 发送关闭消息


onclose 事件处理函数会在终止 WebSocket 连接之前被调用。但此刻连接仍然有效。我们的想法是允许干净地关闭连接,方法是向推送服务器发送一条关闭消息,让它知道套接字将在前端关闭。基本上讲,我们模拟的是关闭帧在关闭握手中的角色。

在清单 8 中,WebSocket 在关闭连接前向推送服务器发送一条 CLOSE 消息。

清单 8. 在 onclose 事件中发送 CLOSE 消息:
notificationsWebSocket = new WebSocket(url, ['base64']);
(...)
notificationsWebSocket.onclose = function() {
    notificationsWebSocket.send(window.btoa(‘CLOSE’));
};

在推送服务器中,我们监听从 UI 传入的所有消息,查看是否收到了 CLOSE 消息。如果收到该消息,则立即关闭套接字,如下所示。

清单 9. 推送服务器对 CLOSE 的响应:
def listen(self):
    try:
        while self.server_running:
            read_ready, _, _ = select.select(self.connections,
                                             [], [], 1)
            for sock in read_ready:
                if not self.server_running:
                    break
  
                if sock == self.server_socket:
  
                    new_socket, addr = self.server_socket.accept()
                    self.connections.append(new_socket)
                else:
                    try:
                        data = sock.recv(4096)
                    except:
                        try:
                            self.connections.remove(sock)
                        except ValueError:
                            pass
  
                        continue
                    if data and data == 'CLOSE':
                        try:
                            self.connections.remove(sock)
                        except ValueError:
                            pass
                        sock.close()

在 WebSocket 关闭和发送 CLOSE 消息之前调用 onclose,这样就能预防 send_notification 例程中发生管道损坏错误。不幸的是,尽管 onclose 在 Firefox 中运行良好,但测试表明,我们在 Chrome 中打开该应用程序时,仍在 send_notification 方法中获得了管道损坏错误。这是由于 Google Chrome 中的一个未解决的问题

4单一、持久的 WebSocket 连接


onclose 和关闭帧的问题促使我们采用单一、持久的 WebSocket 连接。对于仅在浏览器离开页面(通过关闭窗口、重新加载或转到另一个 URL)时才会关闭的单一连接,只要求推送服务器在发生管道损坏时关闭连接。一个集中化连接更容易在 Project WoK 中维护,而且 WoK 插件使我们无需从头实现 WebSocket 策略。

清单 10 给出了一个经过修改的 send_notification 消息。请注意,它处理管道损坏错误的方式是,从有效连接列表中删除套接字,然后关闭它。

清单 10. 针对单个持久连接的 send_notification:
def send_notification(self, message):
for sock in self.connections:
    if sock != self.server_socket:
        try:
            sock.send(message)
        except IOError as e:
            if 'Broken pipe' in str(e):
                sock.close()
                try:
                    self.connections.remove(sock)
                except ValueError:
                    pass


5配置脉动信号消息


在多连接策略中,您会打开一个连接,使用它,然后立即关闭它。单个持久连接应无限期地保持打开,即使没有发送任何消息。但是,Chrome 和 Firefox 中的测试表明,WebSocket 连接会在空闲一段时间后超时,而且连接会终止。

为了避免浏览器超时,我们实现了一条简单的脉动信号消息,定期向推送服务器发送该消息。此消息使 WebSocket 连接保持活动 — 推送服务器不需要响应它。默认超时大约为 300 秒,但我们不能假设所有浏览器都实现相同的超时。为安全起见,我们将脉动信号消息发送间隔设置为 30 秒,如清单 11 所示。

清单 11. 超时间隔:
notificationsWebSocket = new WebSocket(url, ['base64']);
var heartbeat = setInterval(function() {
    notificationsWebSocket.send(window.btoa('heartbeat'));
}, 30000);

这个简单的超时会使 WebSocket 连接保持活动,而且不会向推送服务器发送过多脉动信号消息。

6添加监听器


建立永久连接后,可以实现一个简单的监听器策略,允许示例应用程序中的其他所有 UI 代码监听消息。

下面是该策略的设计:

  • 一个监听器绑定到一种特定消息格式。我们的示例应用程序将发送许多通知消息。消息绑定使我们能仅为特定消息注册监听器;
  • 收到来自推送服务器的消息后,主要 WebSocket 通道验证它的内容,并调用与该消息绑定的所有监听器;
  • WebSocket 通道发起监听器清理。监听器必须在 URI 更改后弃用。例如,在用户浏览到 URI /#tabs/settings 中的 Settings 选项卡时,应弃用 URI /#tabs/user-log 中的 Activity Log 选项卡上的侦听器。清理操作被绑定到 hashchange 事件, URL (location.hash) 每次更改时都会触发该事件。因为我们只需对此过程执行一次,所以可以将 $.one()’ jQuery 调用设置为仅执行一次。

以下是示例应用程序中处理监听器的代码。

清单 12. Project WoK 的监听器配置:
wok.notificationListeners = {};
wok.addNotificationListener = function(msg, func) {
    var listenerArray = wok.notificationListeners[msg];
    if (listenerArray == undefined) {
        listenerArray = [];
    }
    listenerArray.push(func);
    wok.notificationListeners[msg] = listenerArray;
    $(window).one("hashchange", function() {
        var listenerArray = wok.notificationListeners[msg];
        var del_index = listenerArray.indexOf(func);
        listenerArray.splice(del_index, 1);
        wok.notificationListeners[msg] = listenerArray;
    });
};

以下是将消息发送到每个相关监听器的方式。

清单 13. 监听器通知:
wok.startNotificationWebSocket = function () {
    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
    wok.notificationsWebSocket.onmessage = function(event) {
        var message  = window.atob(event.data);
         if (message === "") {
                continue;
        }
              
        var listenerArray = wok.notificationListeners[message];
        if (listenerArray == undefined) {
            continue;
        }
        for (var i = 0; i < listenerArray.length; i++) {
            listenerArray[i](message);
        }
    }
 };

有了该通知,我们的操作就基本上完成了。但首先还需要解决一个问题。

7合并消息


尽管可以使用单一连接,但当前实现对前端如何接收消息做出了不合理的假设。如果推送服务器接连发送了两条消息,先发送 message1,然后发送 message2,结果会怎样?代码要求触发 onmessage 事件两次并从 WebSocket 中读取内容,但事实未必如此。事实上,更可能的情况是,收到的消息是 message1message2— 一条合并消息。

合并消息是一种常见的 TCP 套接字行为:当接连发送两条或更多消息时,接收套接字会对它们进行排队,而且仅为所有消息触发 onmessage 事件一次。如果我们未对此制定应对计划,此行为会破坏我们的代码。解决方案是定义一种消息格式,让应用程序确定一条消息在何处结束,另一条消息在何处开始。

一种流行的选择是 TLV 格式,表示类型-长度-值 (Type-Length-Value)。在此格式中,消息依次包含 3 个字段:type、length 和 value。类型和长度具有固定大小,值具有 length 字段所声明的可变大小。type 字段可用于区分消息类型,比如字符串、布尔值、二进制或整数。它也可用于特定于应用程序的用途,比如警告消息、信息消息、错误消息等。

但是,对我们而言,TLV 有点大材小用。我们的消息始终是字符串,所以不需要 type 字段。为了解决消息的串联问题,可以添加一个固定长度的字段,用于声明消息包含多少个字符。一种更简单的方法是,向发送的每条消息添加一个消息结束标记。收到消息缓存后,前端使用消息标记解析它,将它拆分为单独的前端消息。

清单 14 给出了修改后的 send_notification 方法,其中包含一个消息结束标记。

清单 14. send_notification 中的消息结束标记:
END_OF_MESSAGE_MARKER = '//EOM//'
  
    def send_notification(self, message):
        message += END_OF_MESSAGE_MARKER
        for sock in self.connections:
            if sock != self.server_socket:
                try:
                    sock.send(message)
                except IOError as e:
                    if 'Broken pipe' in str(e):
                        sock.close()
                        try:
                            self.connections.remove(sock)
                        except ValueError:
                            pass

请注意清单 14 中的 //EOM// 序列。可以使用任何序列来表示消息结束,只要它不是包含在有效消息中的内容 — 比如 end。

清单 15 给出了在添加消息结尾解析后的已完成的前端代码。请参阅 Project WoK 来获取完整源代码。

清单 15. WebSocket 前端:
wok.notificationListeners = {};
wok.addNotificationListener = function(msg, func) {
    var listenerArray = wok.notificationListeners[msg];
    if (listenerArray == undefined) {
        listenerArray = [];
    }
    listenerArray.push(func);
    wok.notificationListeners[msg] = listenerArray;
    $(window).one("hashchange", function() {
        var listenerArray = wok.notificationListeners[msg];
        var del_index = listenerArray.indexOf(func);
        listenerArray.splice(del_index, 1);
        wok.notificationListeners[msg] = listenerArray;
    });
};
  
wok.notificationsWebSocket = undefined;
wok.startNotificationWebSocket = function () {
    var addr = window.location.hostname + ':' + window.location.port;
    var token = wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
    var url = 'wss://' + addr + '/websockify?token=' + token;
    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
  
    wok.notificationsWebSocket.onmessage = function(event) {
        var buffer_rcv = window.atob(event.data);
        var messages = buffer_rcv.split("//EOM//");
        for (var i = 0;  i < messages.length; i++) {
            if (messages[i] === "") {
                continue;
            }
            var listenerArray = wok.notificationListeners[messages[i]];
            if (listenerArray == undefined) {
                continue;
            }
            for (var j = 0; j < listenerArray.length; j++) {
                listenerArray[j](messages[i]);
            }
        }
    };
  
    var heartbeat = setInterval(function() {
        wok.notificationsWebSocket.send(window.btoa('heartbeat'));
    }, 30000);
  
};

8、本文小结


对于只需要能向客户端传输异步服务器消息的 Web 应用程序,服务器发送的事件是一个优雅且简单的解决方案。作为半双工 HTTP 解决方案,SSE 不允许客户端向服务器回传消息。此外,在编写本文时,所有 Microsoft 浏览器都不支持 SSE。此限制是否是致命弱点,取决于应用程序的目标受众。

WebSocket 更复杂且要求更高,但全双工 TCP 连接使它适用于更广泛的应用场景。WebSocket 受大多数现代 Web 框架支持,而且兼容所有主要的 Web 和移动浏览器。尽管没有演示,但可以使用类似 Tornado 这样的服务器框架,它有助于快速配置推送服务器,而无需从头编写服务器代码。

SSE 是一个更简单且更快的解决方案,但它是不可扩展的。如果 Web 应用程序的需求发生了改变(例如,如果您认为前端应与后端交互),则需要使用 WebSocket 重构应用程序。WebSocket 需要做更多的前期工作,但它是一种更灵活的、可扩展的框架。它更适合会不断添加新功能的复杂应用程序。

9、相关链接


编写 WebSocket 服务器
Web 和移动浏览器对服务器发送的事件的支持
Web 和移动浏览器对 WebSocket 协议的支持

(原文链接:https://www.ibm.com/developerworks/cn/web/wa-http-server-push-with-websocket-sse/index.html

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

标签:推送技术
上一篇:信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑下一篇:小白请教用UDP来做消息推送的可行性

本帖已收录至以下技术专辑

推荐方案
评论 7
很不错的资料,虽然服务端的python看不懂,但技术原理有借鉴意义
签名: 该会员没有填写今日想说内容.
不错不错
非常好的文章,多谢了。
提示: 该帖被管理员或版主屏蔽
看看
签名:
提示: 该帖被管理员或版主屏蔽
引用:xiaoyan2017 发表于 2019-03-14 11:10
很棒的分享,学习到了很多知识,可以相互学习下。
https://www.cnblogs.com/xiaoyan2017/p/10163043.html

你这是见缝插针地插播你的广告啊
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部