Node.js开发人员工具。 Web套接字上的远程过程调用

恐怖的故事经常讲述Websocket技术,例如,Web浏览器不支持Websocket技术,或者提供商/管理员抑制Websocket流量-因此无法在应用程序中使用。 另一方面,开发人员并不总是能像其他任何技术一样预见到websocket技术所具有的陷阱。 至于所谓的限制,我马上说,今天有96.8%的网络浏览器支持websocket技术。 您可以说剩余3.2%的用户过多,这是数百万的用户。 我完全同意你的看法。 比较而言,只有所有已知的信息。 每个人都在Ajax中使用了多年的同一个XmlHttpRequest,它支持97.17%的Web浏览器(不多,对吧?),并且通常可以获取93.08%的Web浏览器。 与websocket不同,在使用Ajax技术时,这种百分比(甚至更低)甚至没有停止很长时间。 因此,目前在长轮询上使用回退是没有意义的。 如果仅由于不支持websocket的Web浏览器与不支持XmlHttpRequest的Web浏览器相同,并且实际上不会发生任何回退。

第二个恐怖故事,禁止公司网络的提供商或管理员使用websocket,这也是不合理的,因为现在每个人都使用https协议,并且无法理解websocket连接是打开的(不破坏https)。

至于真正的限制和克服这些限制的方法,我将在本文中以开发应用程序的Web管理区域为例进行介绍。

因此,坦率地说,Web浏览器中的WebSocket对象具有一组非常简洁的方法:send()和close()以及从EventTarget对象继承的addEventListener(),removeEventListener()和dispatchEvent()方法。 因此,开发人员必须(通常)或独立(几乎不可能)使用库来解决几个问题。

让我们从最容易理解的任务开始。 与服务器的连接会定期中断。 重新连接非常容易。 但是,如果您还记得此时客户端和服务器发出的消息仍在继续,则一切都会立即变得复杂得多。 通常,如果未提供用于接收消息的确认机制,则消息可能会丢失;如果提供了确认机制,则消息可能会重新发送(甚至多次),但失败发生在接收后立即确认消息之前。

如果您需要保证消息传递和/或没有重复的消息传递,则可以使用特殊协议来实现此目的,例如AMQP和MQTT,它们也可以与websocket传输一起使用。 但是今天我们不会考虑它们。

大多数用于websocket的库都支持程序员透明地重新连接到服务器。 使用这样的库总是比开发实现更可靠。

接下来,您需要实现用于发送和接收异步消息的基础结构。 为此,请使用“裸” onmessage事件处理程序,而无需附加绑定,这是一项不费力的任务。 这样的基础结构可以是例如远程过程调用(RPC)。 id id是引入到json-rpc规范中的,专门用于Websocket传输,它允许您将客户端的远程过程调用映射到Web服务器的响应消息。 与所有其他可能性相比,我更希望使用此协议,但是到目前为止,我还没有在node.js的服务器部分找到该协议的成功实现。

最后,您需要实现扩展。 回想一下,客户端和服务器之间的连接会定期发生。 如果一台服务器的功能对我们来说不够用,我们可以再增加几台服务器。 在这种情况下,断开连接后,将无法保证与同一服务器的连接。 通常,redis服务器或redis服务器群集用于协调多个websocket服务器。

而且,不幸的是,无论如何,迟早我们都会遇到系统性能问题,因为在同时打开的websocket连接数量(不将其与性能混淆)中,node.js的功能大大低于消息队列和代理之类的专用服务器。 在某个关键点之后,需要通过redis服务器群集在websocket服务器的所有实例之间进行交叉交换,这不会显着增加打开连接的数量。 解决此问题的方法是使用专门的服务器,例如AMQP和MQTT,它们可以工作,包括通过websocket传输。 但是今天我们不会考虑它们。

从列出的任务列表中可以看到,在使用websocket时进行循环非常耗时,如果您需要将解决方案扩展到多个websocket服务器,甚至是不可能的。

因此,我建议考虑使用websocket实现工作的几种流行的库。

我将立即从考虑中排除那些仅在过时的传输方式上实现回退的库,因为今天该功能已不相关,而实现更广泛功能的库通常也可以在过时的传输方式上实现回退。

我将从最受欢迎的库-socket.io开始。 现在您可以听到这样的意见(很可能是公平的):就资源而言,此库速度慢且昂贵。 最有可能的是,它的工作速度比本地websocket慢。 但是,从今天的角度来看,它是最发达的图书馆。 而且,再次使用websocket时,主要的限制因素不是速度,而是与唯一客户端同时打开的连接数。 通过与客户端连接到专用服务器,已经可以最好地解决此问题。

因此,当与服务器断开连接并使用服务器或Redis服务器群集进行扩展时,soket.io可实现可靠的恢复。 实际上,socket.io实现了自己的消息协议,该协议使您无需绑定特定的编程语言即可在客户端和服务器之间实现消息传递。

socket.io一个有趣的功能是事件处理的确认,其中可以将任意对象从服务器返回到客户端,这允许进行远程过程调用(尽管它不符合json-rpc标准)。

另外,初步地,我研究了另外两个有趣的库,下面将对其进行简要讨论。

Faye图书馆faye.jcoglan.com 。 它实现了在CometD项目中开发的Bayeux协议,并实现了消息到消息通道的订阅/分发。 该项目还支持使用服务器或Redis服务器群集进行扩展。 尝试找到实现RPC的方法的尝试失败,因为它不适合Bayeux协议方案。

在socketcluster socketcluster.io项目中,重点是扩展Websocket服务器。 同时,与前面提到的两个库一样,websocket服务器集群不是基于redis服务器创建的,而是基于node.js创建的。 在这方面,在部署集群时,有必要启动一个相当复杂的代理和工作者基础结构。

现在让我们继续在socket.io上实现RPC。 如前所述,该库已经实现了在客户端和服务器之间交换对象的功能:

import io from 'socket.io-client'; const socket = io({ path: '/ws', transports: ['websocket'] }); const remoteCall = data => new Promise((resolve, reject) => { socket.emit('remote-call', data, (response) => { if (response.error) { reject(response); } else { resolve(response); } }); }); 

 const server = require('http').createServer(); const io = require('socket.io')(server, { path: '/ws' }); io.on('connection', (socket) => { socket.on('remote-call', async (data, callback) => { handleRemoteCall(socket, data, callback); }); }); server.listen(5000, () => { console.log('dashboard backend listening on *:5000'); }); const handleRemoteCall = (socket, data, callback) => { const response =... callback(response) } 

这是一般方案。 现在,我们将考虑与特定应用程序相关的每个部分。 为了构建管理面板,我使用了react-admin库github.com/marmelab/react-admin 。 与该库中服务器的数据交换是使用数据提供程序实现的,该数据提供程序具有非常方便的方案,几乎是一种标准。 例如,要获取列表,该方法称为:

 dataProvider( 'GET_LIST', ' ', { pagination: { page: {int}, perPage: {int} }, sort: { field: {string}, order: {string} }, filter: { Object } } 

异步响应中的此方法返回一个对象:

 { data: [  ], total:      } 

当前,针对各种服务器和框架(例如Firebase,spring boot,graphql等)的反应管理数据提供程序实现数量惊人。 在RPC的情况下,实现是最简洁的,因为该对象已以其原始形式传输到了send函数调用:

 import io from 'socket.io-client'; const socket = io({ path: '/ws', transports: ['websocket'] }); export default (action, collection, payload = {}) => new Promise((resolve, reject) => { socket.emit('remote-call', {action, collection, payload}, (response) => { if (response.error) { reject(response); } else { resolve(response); } }); }); 

不幸的是,必须在服务器端做更多的工作。 为了组织处理远程调用的功能的映射,开发了类似于express.js的路由器。 该实现仅依赖于签名(套接字,有效负载,回调)而不是中间件(req,res,next)签名。 结果,我们都得到了通常的代码:

 const Router = require('./router'); const router = Router(); router.use('GET_LIST', (socket, payload, callback) => { const limit = Number(payload.pagination.perPage); const offset = (Number(payload.pagination.page) - 1) * limit return callback({data: users.slice(offset, offset + limit ), total: users.length}); }); router.use('GET_ONE', (socket, payload, callback) => { return callback({ data: users[payload.id]}); }); router.use('UPDATE', (socket, payload, callback) => { users[payload.id] = payload.data return callback({ data: users[payload.id] }); }); module.exports = router; const users = []; for (let i = 0; i < 10000; i++) { users.push({ id: i, name: `name of ${i}`}); } 

路由器实现的详细信息可以在项目存储库中找到

剩下的就是为Admin组件分配一个提供程序:

 import React from 'react'; import { Admin, Resource, EditGuesser } from 'react-admin'; import UserList from './UserList'; import dataProvider from './wsProvider'; const App = () => <Admin dataProvider={dataProvider}> <Resource name="users" list={UserList} edit={EditGuesser} /> </Admin>; export default App; 


有用的链接

1.www.infoq.com/articles/Web-Sockets-Proxy-Servers

apapacy@gmail.com
2019年7月14日

Source: https://habr.com/ru/post/zh-CN459978/


All Articles