Outils de développement Node.js. Appel de procédure à distance sur les sockets Web

Les histoires d'horreur parlent souvent de la technologie Websocket, par exemple, qu'elle n'est pas prise en charge par les navigateurs Web ou que les fournisseurs / administrateurs suppriment le trafic Websocket - elle ne peut donc pas être utilisée dans les applications. D'un autre côté, les développeurs ne prévoient pas toujours les pièges de la technologie Websocket, comme toute autre technologie. En ce qui concerne les limitations alléguées, je dirai tout de suite que 96,8% des navigateurs Web prennent en charge la technologie Websocket aujourd'hui. On peut dire que 3,2% restant à la mer, c'est beaucoup, ce sont des millions d'utilisateurs. Je suis totalement d'accord avec toi. Seul tout est connu en comparaison. Le même XmlHttpRequest, que tout le monde utilise en Ajax depuis de nombreuses années, prend en charge 97,17% des navigateurs Web (pas beaucoup plus, non?), Et récupère en général, 93,08% des navigateurs Web. Contrairement à websocket, un tel pourcentage (et auparavant encore plus bas) n'a arrêté personne depuis longtemps lors de l'utilisation de la technologie Ajax. Donc, utiliser le repli sur les longs sondages n'a actuellement aucun sens. Si ce n'est que parce que les navigateurs Web qui ne prennent pas en charge WebSocket sont les mêmes navigateurs Web qui ne prennent pas en charge XmlHttpRequest, et en réalité, aucune solution de secours ne se produira.

La deuxième histoire d'horreur, interdisant sur le Websocket des fournisseurs ou des administrateurs de réseaux d'entreprise, est également déraisonnable, car maintenant tout le monde utilise le protocole https, et il est impossible de comprendre que la connexion Websocket est ouverte (sans casser https).

Quant aux vraies limitations et aux moyens de les surmonter, je vous en parlerai dans cet article, sur l'exemple du développement de la zone d'administration web de l'application.

Ainsi, l'objet WebSocket dans le navigateur Web a, franchement, un ensemble de méthodes très concis: send () et close (), ainsi que les méthodes addEventListener (), removeEventListener () et dispatchEvent () héritées de l'objet EventTarget. Par conséquent, le développeur doit utiliser des bibliothèques (généralement) ou indépendamment (presque impossible) pour résoudre plusieurs problèmes.

Commençons par la tâche la plus compréhensible. La connexion au serveur est interrompue périodiquement. La reconnexion est assez simple. Mais si vous vous souvenez que les messages du client et du serveur continuent de circuler à ce moment, tout devient immédiatement et beaucoup plus compliqué. En général, un message peut être perdu si un mécanisme de confirmation pour le message reçu n'est pas fourni, ou remis (même plusieurs fois) si le mécanisme de confirmation est fourni, mais l'échec s'est produit juste au moment après la réception et avant la confirmation du message.

Si vous avez besoin d'une livraison de message garantie et / ou d'une livraison de message sans prise, il existe des protocoles spéciaux pour l'implémentation, par exemple, AMQP et MQTT, qui fonctionnent avec le transport websocket. Mais aujourd'hui, nous ne les considérerons pas.

La plupart des bibliothèques pour travailler avec websocket prennent en charge la transparence pour que le programmeur se reconnecte au serveur. L'utilisation d'une telle bibliothèque est toujours plus fiable que le développement de votre implémentation.

Ensuite, vous devez implémenter l'infrastructure d'envoi et de réception de messages asynchrones. Pour ce faire, utilisez le gestionnaire d'événements onmessage «nu» sans liaison supplémentaire, une tâche ingrate. Une telle infrastructure peut être, par exemple, un appel de procédure à distance (RPC). L'id id a été introduit dans la spécification json-rpc, spécifiquement pour travailler avec le transport websocket, qui vous permet de mapper l'appel de procédure distante par le client au message de réponse du serveur web. Je préférerais ce protocole à toutes les autres possibilités, mais jusqu'à présent, je n'ai pas trouvé d'implémentation réussie de ce protocole pour la partie serveur sur node.js.

Et enfin, vous devez implémenter la mise à l'échelle. Rappelez-vous que la connexion entre le client et le serveur se produit périodiquement. Si la puissance d'un serveur ne nous suffit pas, nous pouvons augmenter plusieurs serveurs. Dans ce cas, une fois la connexion déconnectée, la connexion au même serveur n'est pas garantie. En règle générale, un serveur Redis ou un cluster de serveurs Redis est utilisé pour coordonner plusieurs serveurs WebSocket.

Et, malheureusement, tôt ou tard, nous rencontrerons de toute façon les performances du système, car les capacités de node.js dans le nombre de connexions Websocket qui sont ouvertes simultanément (ne confondez pas cela avec les performances) sont nettement inférieures à celles des serveurs spécialisés tels que les files d'attente de messages et les courtiers. Et la nécessité d'un échange croisé entre toutes les instances de serveurs WebSocket via le cluster de serveurs Redis, après un certain point critique, n'entraînera pas une augmentation significative du nombre de connexions ouvertes. La façon de résoudre ce problème consiste à utiliser des serveurs spécialisés, tels que AMQP et MQTT, qui fonctionnent, y compris avec le transport Websocket. Mais aujourd'hui, nous ne les considérerons pas.

Comme vous pouvez le voir dans la liste des tâches répertoriées, faire du vélo tout en travaillant avec websocket prend beaucoup de temps, et même impossible si vous avez besoin de faire évoluer la solution sur plusieurs serveurs websocket.

Par conséquent, je propose de considérer plusieurs bibliothèques populaires qui implémentent le travail avec websocket.

Je vais immédiatement exclure de la considération les bibliothèques qui mettent en œuvre exclusivement le repli sur les modes de transport obsolètes, car aujourd'hui cette fonctionnalité n'est pas pertinente, et les bibliothèques qui implémentent une fonctionnalité plus large, en règle générale, implémentent également le repli sur les modes de transport obsolètes.

Je vais commencer par la bibliothèque la plus populaire - socket.io. Vous pouvez maintenant entendre l'opinion, très probablement juste, que cette bibliothèque est lente et coûteuse en termes de ressources. C'est probablement le cas, et cela fonctionne plus lentement que le Websocket natif. Cependant, c'est aujourd'hui la bibliothèque la plus développée par ses moyens. Et, encore une fois, lorsque vous travaillez avec websocket, le principal facteur limitant n'est pas la vitesse, mais le nombre de connexions ouvertes simultanément avec des clients uniques. Et cette question est déjà mieux résolue en établissant des connexions avec les clients vers des serveurs spécialisés.

Ainsi, soket.io implémente une récupération fiable lors de la déconnexion du serveur et de la mise à l'échelle à l'aide d'un serveur ou d'un cluster de serveurs redis. socket.io, en fait, implémente son propre protocole de messagerie individuel, qui vous permet d'implémenter la messagerie entre le client et le serveur sans être lié à un langage de programmation spécifique.

Une caractéristique intéressante de socket.io est la confirmation du traitement des événements, dans lequel un objet arbitraire peut être renvoyé du serveur au client, ce qui permet des appels de procédure à distance (bien qu'il ne soit pas conforme à la norme json-rpc).

Aussi, préliminaire, j'ai examiné deux autres bibliothèques intéressantes, dont je vais brièvement parler ci-dessous.

Bibliothèque Faye faye.jcoglan.com . Il implémente le protocole bayeux, qui a été développé dans le cadre du projet CometD et implémente l'abonnement / distribution de messages aux canaux de messages. Ce projet prend également en charge la mise à l'échelle à l'aide d'un serveur ou d'un cluster de serveurs redis. Une tentative pour trouver un moyen d'implémenter RPC a échoué car il ne cadrait pas avec le schéma du protocole de Bayeux.

Dans le projet socketcluster socketcluster.io , l'accent est mis sur la mise à l'échelle du serveur websocket. Dans le même temps, le cluster de serveurs websocket n'est pas créé sur la base du serveur redis, comme dans les deux premières bibliothèques mentionnées, mais sur la base de node.js. À cet égard, lors du déploiement du cluster, il a été nécessaire de lancer une infrastructure assez complexe de courtiers et de travailleurs.

Passons maintenant à l'implémentation de RPC sur socket.io. Comme je l'ai dit plus haut, cette bibliothèque a déjà implémenté la possibilité d'échanger des objets entre le client et le serveur:

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

C'est le schéma général. Nous allons maintenant considérer chacune des parties par rapport à une application spécifique. Pour construire le panneau d'administration, j'ai utilisé la bibliothèque react -admin github.com/marmelab/react-admin . L'échange de données avec le serveur de cette bibliothèque est mis en œuvre à l'aide d'un fournisseur de données, qui a un schéma très pratique, presque une sorte de standard. Par exemple, pour obtenir une liste, la méthode est appelée:

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

Cette méthode dans une réponse asynchrone renvoie un objet:

 { data: [  ], total:      } 

Il existe actuellement un nombre impressionnant d'implémentations de fournisseurs de données React-Admin pour divers serveurs et frameworks (par exemple Firebase, Spring Boot, Graphql, etc.). Dans le cas de RPC, l'implémentation s'est avérée être la plus concise, puisque l'objet est transféré dans sa forme d'origine à l'appel de fonction 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); } }); }); 

Malheureusement, un peu plus de travail a dû être fait côté serveur. Pour organiser le mappage des fonctions qui gèrent l'appel distant, un routeur similaire à express.js a été développé. Ce n'est qu'au lieu de la signature du middleware (req, res, next) que l'implémentation s'appuie sur la signature (socket, payload, callback). En conséquence, nous avons tous obtenu le code habituel:

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

Les détails de la mise en œuvre du routeur peuvent être trouvés dans le référentiel du projet.

Il ne reste plus qu'à assigner un fournisseur pour le composant 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; 


Liens utiles

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

apapacy@gmail.com
14 juillet 2019

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


All Articles