如何使用 WebSockets 组合应用程序
或者:如何在 WebSockets 或 SockJS 上正确实现多路复用
正如您可能知道的,WebSockets 是一项很棒的新 HTML5 技术,它允许您异步发送和接收消息。我们的兼容性层 - SockJS - 模拟了它,即使在旧浏览器或代理后面也能正常工作。WebSockets 在概念上非常简单。API 主要包括:连接、发送和接收。但是,如果您的 Web 应用程序有许多模块,并且每个模块都想要发送和接收数据,该怎么办?
理论上,你可以为每个模块打开多个 WebSocket 连接。虽然这种方法不是最优的(因为需要处理多个 TCP/IP 连接),但对于原生 WebSocket 来说是可行的。然而遗憾的是,由于 HTTP 的技术限制,SockJS 无法实现这一点:对于某些回退传输机制,无法同时向同一台服务器打开多个连接。这个问题是真实存在的,并且值得解决。让我换一种说法。
假设你只能与给定的主机保持单个连接,但有多个模块需要发送和接收数据,你该怎么办?
你需要多路复用(Multiplexing):将来自多个源的数据组合到一个连接中。接下来的问题是使用什么 API;如何在代码中实现多路复用?
Socket.io 的方式
Socket.io 提供了一种试图解决此问题的 API,它称之为“命名空间(namespaces)”。以下是浏览器客户端代码示例
var chat = io.connect('https:///chat');
chat.on('connect', function () {
chat.emit('hi!');
});
var news = io.connect('https:///news');
news.on('news', function () {
news.emit('woot');
});
我认为这个 API 非常令人困惑——在底层,Socket.io 只打开了一个连接,但阅读这段代码却让我们感觉完全是另一回事。
SockJS 的方式
与 Socket.io 不同,SockJS 没有任何“魔法”API。它看起来就像一个 WebSocket 对象,表现也像一个 WebSocket 对象。没什么令人惊讶的。
那么如何解决多路复用问题呢?
如果可能的话,尽量避免发明新的 API,而应使用现有的标准 API,这通常是个好主意。为什么不把每个多路复用的通道都呈现为一个 WebSocket 对象呢?
我的建议非常简单:你获取一个真实的 SockJS(或 WebSocket)连接,将其封装在一个多路复用层中,然后从中提取出任意数量的“伪”WebSocket 对象。它们在内部是多路复用的,但从模块的角度来看,这一切是完全透明的。对该模块而言,它就是在和一个标准的 WebSocket 对象对话。
就是这样。这有点像魔术师的帽子:放入一个 WebSocket 连接,就可以取出任意数量的“伪”WebSocket 连接。
这种方法比 Socket.io 提出的方案更好——你可以编写仅依赖于原生 WebSocket API 的代码。稍后,当需要时,你可以直接传入一个“伪”WebSocket 对象来代替真实的 WebSocket 对象。换句话说:它支持组合(compose)。问题解决了。
实现
如果之前在浏览器中你使用单个 SockJS 连接,就像这样
var sockjs = new SockJS('/echo');
你可以将客户端代码修改为
var real_sockjs = new SockJS('/echo');
var multiplexer = new WebSocketMultiplex(real_sockjs);
var fake_sockjs_1 = multiplexer.channel('ann');
var fake_sockjs_2 = multiplexer.channel('bob');
此时,“伪”对象将表现得与普通 SockJS 对象完全一致。你可以期待接收到 'open'、'message' 和 'close' 事件。(底层代码大约60 行 JavaScript)。同样在服务器端,它通常使用常规的 "net.Server" 和 "Stream" Node API。
var service = sockjs.createServer();
更改之后
var real_service = sockjs.createServer();
var multiplexer = new multiplex_server.MultiplexServer(real_service);
var fake_service_1 = multiplexer.registerChannel('ann');
var fake_service_2 = multiplexer.registerChannel('bob');
同样,“伪”对象将执行常规操作,当用户订阅该特定通道时,它们会发出 'connected' 事件。(底层的多路复用器代码也不太复杂)。
如果你想查看多路复用器代码的实际效果
最后的思考
值得强调的是,这种方法确实支持组合。任何模块都可以获取一个“伪”WebSocket 对象,并重复这一技巧来获取更多第二层的“伪”WebSocket 对象。不要去发明新的 API,只需编写依赖于传入构造函数的 WebSocket 实例的代码即可。这就是你使用 WebSocket 创建可组合代码所需要的全部!