Las historias de horror a menudo cuentan sobre la tecnología websocket, por ejemplo, que no es compatible con los navegadores web, o que los proveedores / administradores suprimen el tráfico websocket, por lo tanto, no se puede usar en aplicaciones. Por otro lado, los desarrolladores no siempre prevén las trampas que tiene la tecnología websocket, como cualquier otra tecnología. En cuanto a las presuntas limitaciones, diré de inmediato que el 96.8% de los navegadores web admiten la tecnología websocket en la actualidad. Se puede decir que el 3,2% restante por la borda es mucho, son millones de usuarios. Estoy completamente de acuerdo contigo. Solo todo se sabe en comparación. El mismo XmlHttpRequest, que todos han estado usando en Ajax durante muchos años, admite el 97,17% de los navegadores web (no mucho más, ¿verdad?), Y busca en general el 93,08% de los navegadores web. A diferencia de websocket, dicho porcentaje (y anteriormente era aún más bajo) no ha detenido a nadie durante mucho tiempo al usar la tecnología Ajax. Por lo tanto, el uso de respaldo en encuestas largas actualmente no tiene sentido. Aunque solo sea porque los navegadores web que no son compatibles con websocket son los mismos navegadores web que no son compatibles con XmlHttpRequest, y en realidad no se producirá ningún retroceso.
La segunda historia de terror sobre la prohibición en websocket de proveedores o administradores de redes corporativas tampoco es razonable, ya que ahora todos usan el protocolo https, y es imposible entender que la conexión websocket esté abierta (sin romper https).
En cuanto a las limitaciones reales y las formas de superarlas, contaré en esta publicación, sobre el ejemplo del desarrollo del área de administración web de la aplicación.
Entonces, el objeto WebSocket en el navegador web tiene, francamente, un conjunto muy conciso de métodos: send () y close (), así como los métodos addEventListener (), removeEventListener () y dispatchEvent () heredados del objeto EventTarget. Por lo tanto, el desarrollador debe usar bibliotecas (generalmente) o de forma independiente (casi imposible) para resolver varios problemas.
Comencemos con la tarea más comprensible. La conexión al servidor se interrumpe periódicamente. Reconectarse es bastante fácil. Pero si recuerda que los mensajes tanto del cliente como del servidor continúan en este momento, todo se vuelve inmediato y mucho más complicado. En general, se puede perder un mensaje si no se proporciona un mecanismo de confirmación para el mensaje recibido, o volver a entregarlo (incluso varias veces) si se proporciona el mecanismo de confirmación, pero la falla ocurrió justo en el momento posterior a la recepción y antes de que se confirmara el mensaje.
Si necesita una entrega de mensajes garantizada y / o entrega de mensajes sin tomas, existen protocolos especiales para implementar esto, por ejemplo, AMQP y MQTT, que funcionan con el transporte websocket. Pero hoy no los consideraremos.
La mayoría de las bibliotecas para trabajar con websocket son transparentes para que el programador se vuelva a conectar al servidor. Usar una biblioteca de este tipo siempre es más confiable que desarrollar su implementación.
A continuación, debe implementar la infraestructura para enviar y recibir mensajes asincrónicos. Para hacer esto, utilice el controlador de eventos onmessage "desnudo" sin enlace adicional, una tarea ingrata. Dicha infraestructura puede ser, por ejemplo, llamada a procedimiento remoto (RPC). El id de identificación se introdujo en la especificación json-rpc, específicamente para trabajar con el transporte websocket, que le permite asignar la llamada a procedimiento remoto del cliente al mensaje de respuesta del servidor web. Preferiría este protocolo a todas las demás posibilidades, pero hasta ahora no he encontrado una implementación exitosa de este protocolo para la parte del servidor en node.js.
Y finalmente, necesita implementar el escalado. Recuerde que la conexión entre el cliente y el servidor se produce periódicamente. Si la potencia de un servidor no es suficiente para nosotros, podemos generar varios servidores más. En este caso, después de desconectar la conexión, no se garantiza la conexión al mismo servidor. Por lo general, se usa un servidor redis o un grupo de servidores redis para coordinar múltiples servidores websocket.
Y, desafortunadamente, tarde o temprano nos toparemos con el rendimiento del sistema de todos modos, ya que las capacidades de node.js en el número de conexiones websocket abiertas al mismo tiempo (no confunda esto con la velocidad) son significativamente menores que con servidores especializados como colas de mensajes y corredores. Y la necesidad de intercambio cruzado entre todas las instancias de servidores websocket a través de un clúster de servidores redis, después de algún punto crítico, no dará un aumento significativo en el número de conexiones abiertas. La forma de resolver este problema es utilizar servidores especializados, como AMQP y MQTT, que funcionan, incluso con el transporte websocket. Pero hoy no los consideraremos.
Como puede ver en la lista de tareas enumeradas, el ciclismo mientras se trabaja con websocket consume mucho tiempo e incluso es imposible si necesita escalar la solución a varios servidores websocket.
Por lo tanto, propongo considerar varias bibliotecas populares que implementan el trabajo con websocket.
Inmediatamente excluiré de la consideración aquellas bibliotecas que implementan solo el respaldo en modos de transporte obsoletos, ya que hoy esta funcionalidad no es relevante, y las bibliotecas que implementan una funcionalidad más amplia, como regla, también implementan respaldo en modos de transporte obsoletos.
Comenzaré con la biblioteca más popular: socket.io. Ahora puede escuchar la opinión, muy probablemente justa, de que esta biblioteca es lenta y costosa en términos de recursos. Lo más probable es que funcione, y funciona más lento que el websocket nativo. Sin embargo, hoy es la biblioteca más desarrollada por sus medios. Y, una vez más, cuando se trabaja con websocket, el principal factor limitante no es la velocidad, sino la cantidad de conexiones abiertas simultáneamente con clientes únicos. Y esta pregunta ya se resuelve mejor haciendo conexiones con clientes a servidores especializados.
Entonces, soket.io implementa una recuperación confiable al desconectarse del servidor y escalar usando un servidor o un grupo de servidores redis. socket.io, de hecho, implementa su propio protocolo de mensajería individual, que le permite implementar mensajes entre el cliente y el servidor sin estar vinculado a un lenguaje de programación específico.
Una característica interesante de socket.io es la confirmación del procesamiento de eventos, en el que se puede devolver un objeto arbitrario del servidor al cliente, lo que permite llamadas a procedimientos remotos (aunque no cumple con el estándar json-rpc).
Además, preliminarmente, examiné dos bibliotecas más interesantes, que analizaré brevemente a continuación.
Biblioteca Faye
faye.jcoglan.com . Implementa el protocolo bayeux, que se desarrolló en el proyecto CometD e implementa la suscripción / distribución de mensajes a los canales de mensajes. Este proyecto también admite el escalado utilizando un servidor o un grupo de servidores redis. Un intento de encontrar una manera de implementar RPC no tuvo éxito porque no encajaba en el esquema del protocolo bayeux.
En el proyecto socketcluster
socketcluster.io , el énfasis está en escalar el servidor websocket. Al mismo tiempo, el clúster del servidor websocket no se crea sobre la base del servidor redis, como en las dos primeras bibliotecas mencionadas, sino sobre la base de node.js. En este sentido, al implementar el clúster, fue necesario lanzar una infraestructura bastante compleja de corredores y trabajadores.
Ahora pasemos a la implementación de RPC en socket.io. Como dije anteriormente, esta biblioteca ya ha implementado la capacidad de intercambiar objetos entre el cliente y el 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 es el esquema general. Ahora consideraremos cada una de las partes en relación con una aplicación específica. Para construir el panel de administración, utilicé la biblioteca
react -admin
github.com/marmelab/react-admin . El intercambio de datos con el servidor en esta biblioteca se implementa utilizando un proveedor de datos, que tiene un esquema muy conveniente, casi una especie de estándar. Por ejemplo, para obtener una lista, el método se llama:
dataProvider( 'GET_LIST', ' ', { pagination: { page: {int}, perPage: {int} }, sort: { field: {string}, order: {string} }, filter: { Object } }
Este método en una respuesta asincrónica devuelve un objeto:
{ data: [ ], total: }
Actualmente hay un número impresionante de implementaciones de proveedores de datos de administración de reacción para varios servidores y marcos (por ejemplo, firebase, spring boot, graphql, etc.). En el caso de RPC, la implementación resultó ser la más concisa, ya que el objeto se transfiere en su forma original a la llamada a la función de emisión:
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); } }); });
Desafortunadamente, se tuvo que hacer un poco más de trabajo en el lado del servidor. Para organizar la asignación de funciones que manejan la llamada remota, se desarrolló un enrutador similar a express.js. Solo en lugar de la firma de middleware (req, res, next) la implementación se basa en la firma (socket, payload, callback). Como resultado, todos obtuvimos el código habitual:
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}`}); }
Los detalles de la implementación del enrutador se pueden encontrar
en el repositorio del proyecto.Todo lo que queda es asignar un proveedor para el 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;
Enlaces utiles
1.www.infoq.com/articles/Web-Sockets-Proxy-Serversapapacy@gmail.com
14 de julio de 2019