Horrorgeschichten erzählen oft von der Websocket-Technologie, zum Beispiel, dass sie von Webbrowsern nicht unterstützt wird oder dass Anbieter / Administratoren den Websocket-Verkehr unterdrücken - daher kann sie nicht in Anwendungen verwendet werden. Andererseits sehen Entwickler nicht immer die Fallstricke voraus, die die Websocket-Technologie wie jede andere Technologie hat. Zu den angeblichen Einschränkungen möchte ich sofort sagen, dass 96,8% der Webbrowser heute die Websocket-Technologie unterstützen. Man kann sagen, dass 3,2%, die über Bord bleiben, viel sind, das sind Millionen von Benutzern. Ich stimme dir vollkommen zu. Im Vergleich ist nur alles bekannt. Dieselbe XmlHttpRequest, die seit vielen Jahren in Ajax verwendet wird, unterstützt 97,17% der Webbrowser (nicht viel mehr, oder?) Und ruft im Allgemeinen 93,08% der Webbrowser ab. Im Gegensatz zu Websocket hat ein solcher Prozentsatz (und früher war er sogar noch niedriger) niemanden lange Zeit daran gehindert, die Ajax-Technologie zu verwenden. Daher macht die Verwendung von Fallback bei langen Abfragen derzeit keinen Sinn. Wenn nur, weil Webbrowser, die Websocket nicht unterstützen, dieselben Webbrowser sind, die XmlHttpRequest nicht unterstützen, und in Wirklichkeit kein Fallback auftritt.
Die zweite Horrorgeschichte, die das Verbot von Websockets von Anbietern oder Administratoren von Unternehmensnetzwerken vorsieht, ist ebenfalls unvernünftig, da jetzt jeder das https-Protokoll verwendet und es unmöglich ist zu verstehen, dass die Websocket-Verbindung offen ist (ohne https zu unterbrechen).
In diesem Beitrag werde ich am Beispiel der Entwicklung des Webadministrationsbereichs der Anwendung die tatsächlichen Einschränkungen und Möglichkeiten zur Überwindung dieser Einschränkungen erläutern.
Das WebSocket-Objekt im Webbrowser verfügt also offen gesagt über eine sehr präzise Reihe von Methoden: send () und close () sowie die vom EventTarget-Objekt geerbten Methoden addEventListener (), removeEventListener () und dispatchEvent (). Daher muss der Entwickler Bibliotheken (normalerweise) oder unabhängig (fast unmöglich) verwenden, um mehrere Probleme zu lösen.
Beginnen wir mit der verständlichsten Aufgabe. Die Verbindung zum Server wird regelmäßig unterbrochen. Das Wiederverbinden ist einfach genug. Wenn Sie sich jedoch daran erinnern, dass Nachrichten sowohl vom Client als auch vom Server zu diesem Zeitpunkt weiterhin gesendet werden, wird alles sofort und viel komplizierter. Im Allgemeinen kann eine Nachricht verloren gehen, wenn kein Bestätigungsmechanismus für die empfangene Nachricht bereitgestellt wird, oder erneut zugestellt werden (sogar mehrmals), wenn ein Bestätigungsmechanismus bereitgestellt wird. Der Fehler trat jedoch erst im Moment nach dem Empfang und vor der Bestätigung der Nachricht auf.
Wenn Sie eine garantierte Nachrichtenübermittlung und / oder Nachrichtenübermittlung ohne Duplikate benötigen, gibt es spezielle Protokolle für die Implementierung, z. B. AMQP und MQTT, die auch mit dem Websocket-Transport funktionieren. Aber heute werden wir sie nicht berücksichtigen.
Die meisten Bibliotheken für die Arbeit mit Websocket unterstützen transparent für den Programmierer, der sich wieder mit dem Server verbindet. Die Verwendung einer solchen Bibliothek ist immer zuverlässiger als die Entwicklung Ihrer Implementierung.
Als Nächstes müssen Sie die Infrastruktur zum Senden und Empfangen von asynchronen Nachrichten implementieren. Verwenden Sie dazu den "nackten" onmessage-Ereignishandler ohne zusätzliche Bindung, eine undankbare Aufgabe. Eine solche Infrastruktur kann beispielsweise ein Remote Procedure Call (RPC) sein. Die ID-ID wurde in die json-rpc-Spezifikation aufgenommen, speziell für die Arbeit mit dem Websocket-Transport, mit dem Sie den Remoteprozeduraufruf des Clients der Antwortnachricht vom Webserver zuordnen können. Ich würde dieses Protokoll allen anderen Möglichkeiten vorziehen, aber bisher habe ich keine erfolgreiche Implementierung dieses Protokolls für den Serverteil auf node.js gefunden.
Und schließlich müssen Sie die Skalierung implementieren. Denken Sie daran, dass die Verbindung zwischen dem Client und dem Server regelmäßig hergestellt wird. Wenn uns die Leistung eines Servers nicht ausreicht, können wir mehrere weitere Server erhöhen. In diesem Fall kann nach dem Trennen der Verbindung die Verbindung zu demselben Server nicht garantiert werden. In der Regel wird ein Redis-Server oder ein Cluster von Redis-Servern verwendet, um mehrere Websocket-Server zu koordinieren.
Und leider werden wir früher oder später ohnehin auf die Systemleistung stoßen, da die Fähigkeiten von node.js in Bezug auf die Anzahl der gleichzeitig geöffneten Websocket-Verbindungen (verwechseln Sie dies nicht mit der Leistung) erheblich geringer sind als bei spezialisierten Servern wie Nachrichtenwarteschlangen und Brokern. Und die Notwendigkeit eines gegenseitigen Austauschs zwischen allen Instanzen von Websocket-Servern über einen Redis-Server-Cluster wird nach einem kritischen Punkt die Anzahl offener Verbindungen nicht wesentlich erhöhen. Die Lösung dieses Problems besteht darin, spezialisierte Server wie AMQP und MQTT zu verwenden, die auch beim Websocket-Transport funktionieren. Aber heute werden wir sie nicht berücksichtigen.
Wie Sie der Liste der aufgelisteten Aufgaben entnehmen können, ist das Radfahren während der Arbeit mit Websocket äußerst zeitaufwändig und sogar unmöglich, wenn Sie die Lösung auf mehrere Websocket-Server skalieren müssen.
Daher schlage ich vor, mehrere beliebte Bibliotheken in Betracht zu ziehen, die die Arbeit mit Websocket implementieren.
Ich werde diejenigen Bibliotheken, die ausschließlich auf veraltete Verkehrsträger zurückgreifen, sofort von der Prüfung ausschließen, da diese Funktionalität heute nicht relevant ist, und Bibliotheken, die in der Regel eine breitere Funktionalität implementieren, auch auf veraltete Verkehrsträger zurückgreifen.
Ich beginne mit der beliebtesten Bibliothek - socket.io. Jetzt können Sie die wahrscheinlich faire Meinung hören, dass diese Bibliothek in Bezug auf Ressourcen langsam und teuer ist. Höchstwahrscheinlich ist es so und es funktioniert langsamer als ein nativer Websocket. Heute ist es jedoch die am weitesten entwickelte Bibliothek. Bei der Arbeit mit Websocket ist der Hauptbegrenzungsfaktor nicht die Geschwindigkeit, sondern die Anzahl der gleichzeitig offenen Verbindungen mit eindeutigen Clients. Und diese Frage lässt sich am besten bereits lösen, indem Verbindungen mit Clients zu spezialisierten Servern hergestellt werden.
So implementiert soket.io eine zuverlässige Wiederherstellung, wenn die Verbindung zum Server getrennt und die Skalierung mithilfe eines Servers oder eines Clusters von Redis-Servern durchgeführt wird. In der Tat implementiert socket.io ein eigenes individuelles Messaging-Protokoll, mit dem Sie Messaging zwischen Client und Server implementieren können, ohne an eine bestimmte Programmiersprache gebunden zu sein.
Eine interessante Funktion von socket.io ist die Bestätigung der Ereignisverarbeitung, bei der ein beliebiges Objekt vom Server an den Client zurückgegeben werden kann, wodurch Remoteprozeduraufrufe möglich sind (obwohl es nicht dem json-rpc-Standard entspricht).
Außerdem habe ich vorläufig zwei weitere interessante Bibliotheken untersucht, auf die ich im Folgenden kurz eingehen werde.
Faye Bibliothek
faye.jcoglan.com . Es implementiert das Bayeux-Protokoll, das im CometD-Projekt entwickelt wurde, und implementiert das Abonnieren / Verteilen von Nachrichten an Nachrichtenkanäle. Dieses Projekt unterstützt auch die Skalierung mithilfe eines Servers oder eines Clusters von Redis-Servern. Ein Versuch, einen Weg zur Implementierung von RPC zu finden, war erfolglos, da er nicht in das Bayeux-Protokollschema passte.
Im Projekt socketcluster
socketcluster.io liegt der Schwerpunkt auf der Skalierung des Websocket-Servers. Gleichzeitig wird der Websocket-Server-Cluster nicht wie in den beiden erstgenannten Bibliotheken auf Basis des Redis-Servers erstellt, sondern auf Basis von node.js. In dieser Hinsicht war es bei der Bereitstellung des Clusters erforderlich, eine recht komplexe Infrastruktur von Maklern und Arbeitnehmern einzurichten.
Fahren wir nun mit der Implementierung von RPC auf socket.io fort. Wie oben erwähnt, hat diese Bibliothek bereits die Möglichkeit implementiert, Objekte zwischen Client und Server auszutauschen:
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) }
Dies ist das allgemeine Schema. Nun werden wir jeden der Teile in Bezug auf eine bestimmte Anwendung betrachten. Um das Admin-Panel zu erstellen, habe ich die React-Admin-Bibliothek
github.com/marmelab/react-admin verwendet . Der Datenaustausch mit dem Server in dieser Bibliothek wird mithilfe eines Datenanbieters implementiert, der ein sehr praktisches Schema aufweist, fast eine Art Standard. Um beispielsweise eine Liste zu erhalten, wird die Methode aufgerufen:
dataProvider( 'GET_LIST', ' ', { pagination: { page: {int}, perPage: {int} }, sort: { field: {string}, order: {string} }, filter: { Object } }
Diese Methode in einer asynchronen Antwort gibt ein Objekt zurück:
{ data: [ ], total: }
Derzeit gibt es eine beeindruckende Anzahl von Implementierungen von React-Admin-Datenanbietern für verschiedene Server und Frameworks (z. B. Firebase, Spring Boot, Graphql usw.). Im Fall von RPC erwies sich die Implementierung als die prägnanteste, da das Objekt in seiner ursprünglichen Form an den Funktionsaufruf emit übertragen wird:
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); } }); });
Leider musste auf der Serverseite etwas mehr Arbeit geleistet werden. Um die Zuordnung von Funktionen zu organisieren, die den Remote-Aufruf verarbeiten, wurde ein Router ähnlich wie express.js entwickelt. Nur anstelle der Middleware-Signatur (req, res, next) stützt sich die Implementierung auf die Signatur (Socket, Payload, Callback). Als Ergebnis haben wir alle den üblichen Code erhalten:
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}`}); }
Details zur Implementierung des Routers finden Sie
im Projekt-Repository.Sie müssen lediglich einen Anbieter für die Admin-Komponente zuweisen:
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;
Nützliche Links
1. www.infoq.com/articles/Web-Sockets-Proxy-Serversapapacy@gmail.com
14. Juli 2019