Ferramentas de desenvolvedor do Node.js. Chamada de procedimento remoto nos soquetes da Web

Histórias de horror costumam contar sobre a tecnologia websocket, por exemplo, que ela não é suportada por navegadores da web ou que provedores / administradores suprimem o tráfego da websocket - portanto, não pode ser usada em aplicativos. Por outro lado, os desenvolvedores nem sempre prevêem as armadilhas que a tecnologia WebSocket possui, como qualquer outra tecnologia. Quanto às alegadas limitações, direi imediatamente que 96,8% dos navegadores da Web suportam a tecnologia Websocket hoje. Você pode dizer que 3,2% do total de marés restantes são muitos, são milhões de usuários. Eu concordo completamente com você. Apenas tudo é conhecido em comparação. O mesmo XmlHttpRequest, que todo mundo usa no Ajax há muitos anos, suporta 97,17% dos navegadores da web (não muito mais, certo?), E busca, em geral, 93,08% dos navegadores da web. Ao contrário do websocket, essa porcentagem (e antes era ainda menor) não impede ninguém por um longo tempo ao usar a tecnologia Ajax. Portanto, o uso de fallback em pesquisas longas atualmente não faz sentido. Se apenas os navegadores da Web que não suportam o websocket são os mesmos que não suportam o XmlHttpRequest e, na realidade, nenhum fallback ocorrerá.

A segunda história de horror, banir o websocket de provedores ou administradores de redes corporativas, também é irracional, já que agora todo mundo usa o protocolo https e é impossível entender que a conexão com o websocket está aberta (sem interromper o https).

Quanto às limitações reais e maneiras de superá-las, vou contar neste post, no exemplo do desenvolvimento da área de administração da web do aplicativo.

Portanto, o objeto WebSocket no navegador da Web possui, francamente, um conjunto muito conciso de métodos: send () e close (), bem como os métodos addEventListener (), removeEventListener () e dispatchEvent () herdados do objeto EventTarget. Portanto, o desenvolvedor deve usar bibliotecas (geralmente) ou de forma independente (quase impossível) para resolver vários problemas.

Vamos começar com a tarefa mais compreensível. A conexão com o servidor é interrompida periodicamente. Reconectar é fácil o suficiente. Mas se você se lembrar de que as mensagens do cliente e do servidor continuam sendo enviadas no momento, tudo se torna imediato e muito mais complicado. Em geral, uma mensagem pode ser perdida se um mecanismo de confirmação da mensagem recebida não for fornecida ou entregue novamente (mesmo várias vezes) se for fornecido um mecanismo de confirmação, mas a falha ocorreu exatamente no momento após o recebimento e antes da confirmação da mensagem.

Se você precisar de entrega garantida de mensagens e / ou entrega de mensagens sem tomadas, existem protocolos especiais para implementar isso, por exemplo, AMQP e MQTT, que funcionam com o transporte de websocket. Mas hoje não vamos considerá-los.

A maioria das bibliotecas para trabalhar com o websocket oferece suporte transparente para o programador se reconectar ao servidor. Usar essa biblioteca é sempre mais confiável do que desenvolver sua implementação.

Em seguida, você precisa implementar a infraestrutura para enviar e receber mensagens assíncronas. Para fazer isso, use o manipulador de eventos "nu" na mensagem sem ligação adicional, uma tarefa ingrata. Essa infraestrutura pode ser, por exemplo, RPC (chamada de procedimento remoto). O id id foi introduzido na especificação json-rpc, especificamente para trabalhar com o transporte do websocket, que permite mapear a chamada de procedimento remoto pelo cliente para a mensagem de resposta do servidor da web. Eu preferiria esse protocolo a todas as outras possibilidades, mas até agora não encontrei uma implementação bem-sucedida desse protocolo para a parte do servidor no node.js.

E, finalmente, você precisa implementar a escala. Lembre-se de que a conexão entre o cliente e o servidor ocorre periodicamente. Se o poder de um servidor não for suficiente para nós, podemos criar vários outros servidores. Nesse caso, após a conexão ser desconectada, a conexão com o mesmo servidor não é garantida. Normalmente, um servidor redis ou um cluster de servidores redis é usado para coordenar vários servidores websocket.

E, infelizmente, mais cedo ou mais tarde, executaremos o desempenho do sistema, já que os recursos do node.js no número de conexões de websocket abertas simultaneamente (não confunda isso com desempenho) são significativamente menores do que com servidores especializados, como filas de mensagens e intermediários. E a necessidade de troca cruzada entre todas as instâncias de servidores Websocket por meio de um cluster de servidores redis, após algum ponto crítico, não proporcionará um aumento significativo no número de conexões abertas. A maneira de resolver esse problema é usar servidores especializados, como AMQP e MQTT, que funcionam, inclusive com o transporte de websocket. Mas hoje não vamos considerá-los.

Como você pode ver na lista de tarefas listadas, pedalar enquanto trabalha com o websocket é extremamente demorado e até impossível se você precisar dimensionar a solução para vários servidores de websocket.

Portanto, proponho considerar várias bibliotecas populares que implementam o trabalho com o websocket.

Excluirei imediatamente da consideração as bibliotecas que implementam fallback exclusivamente em modos de transporte obsoletos, já que hoje essa funcionalidade não é relevante e as bibliotecas que implementam funcionalidade mais ampla, como regra, também implementam fallback em modos de transporte obsoletos.

Vou começar com a biblioteca mais popular - socket.io. Agora você pode ouvir a opinião, provavelmente justa, de que essa biblioteca é lenta e cara em termos de recursos. Provavelmente é, e funciona mais lentamente que o websocket nativo. Hoje, no entanto, é a biblioteca mais desenvolvida por seus meios. E, mais uma vez, ao trabalhar com o websocket, o principal fator limitante não é a velocidade, mas o número de conexões abertas simultaneamente com clientes únicos. E essa questão já está resolvida melhor fazendo conexões com clientes para servidores especializados.

Portanto, o soket.io implementa uma recuperação confiável ao se desconectar do servidor e escalar usando um servidor ou um cluster de servidores redis. O socket.io, de fato, implementa seu próprio protocolo de mensagens individual, que permite implementar mensagens entre o cliente e o servidor sem estar vinculado a uma linguagem de programação específica.

Um recurso interessante do socket.io é a confirmação do processamento de eventos, na qual um objeto arbitrário pode ser retornado do servidor para o cliente, o que permite chamadas de procedimento remoto (embora não esteja em conformidade com o padrão json-rpc).

Também, preliminarmente, examinei mais duas bibliotecas interessantes, as quais discutirei brevemente abaixo.

Biblioteca Faye faye.jcoglan.com . Ele implementa o protocolo bayeux, desenvolvido no projeto CometD e implementa a assinatura / distribuição de mensagens nos canais de mensagens. Este projeto também suporta o dimensionamento usando um servidor ou um cluster de servidores redis. Uma tentativa de encontrar uma maneira de implementar o RPC não teve êxito porque não se encaixava no esquema do protocolo bayeux.

No projeto socketcluster socketcluster.io , a ênfase está no dimensionamento do servidor websocket. Ao mesmo tempo, o cluster do servidor websocket não é criado com base no servidor redis, como nas duas primeiras bibliotecas mencionadas, mas com base no node.js. Nesse sentido, ao implantar o cluster, era necessário iniciar uma infraestrutura bastante complexa de corretores e trabalhadores.

Agora vamos à implementação do RPC no socket.io. Como eu disse acima, esta biblioteca já implementou a capacidade de trocar objetos entre o cliente e o servidor:

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) } 

Este é o esquema geral. Agora, consideraremos cada uma das partes em relação a uma aplicação específica. Para criar o painel de administração, usei a biblioteca react -admin github.com/marmelab/react-admin . A troca de dados com o servidor nesta biblioteca é implementada usando um provedor de dados, que possui um esquema muito conveniente, quase um tipo de padrão. Por exemplo, para obter uma lista, o método é chamado:

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

Este método em uma resposta assíncrona retorna um objeto:

 { data: [  ], total:      } 

Atualmente, há um número impressionante de implementações de provedor de dados react-admin para vários servidores e estruturas (por exemplo, firebase, spring boot, graphql etc.). No caso do RPC, a implementação acabou sendo a mais concisa, pois o objeto é transferido em sua forma original para a chamada da função emit:

 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); } }); }); 

Infelizmente, um pouco mais de trabalho teve que ser feito no lado do servidor. Para organizar o mapeamento de funções que tratam da chamada remota, foi desenvolvido um roteador semelhante ao express.js. Somente em vez da assinatura do middleware (req, res, next), a implementação depende da assinatura (soquete, carga útil, retorno de chamada). Como resultado, todos obtivemos o código usual:

 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}`}); } 

Detalhes da implementação do roteador podem ser encontrados no repositório do projeto.

Tudo o que resta é atribuir um provedor ao componente 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; 


Links úteis

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

apapacy@gmail.com
14 de julho de 2019

Source: https://habr.com/ru/post/pt459978/


All Articles