Ordenar el caos

Como muestra la práctica, una gran parte de los problemas surge no por las soluciones en sí, sino por la forma en que se produce la comunicación entre los componentes del sistema. Si hay un desorden en la comunicación entre los componentes del sistema, entonces, como no intenta escribir bien los componentes individuales, el sistema en su conjunto fallará.


Precaución Dentro de la bicicleta.


Problema o enunciado del problema


Hace algún tiempo, pasó a trabajar en un proyecto para una empresa que trae a las masas deleites como CRM, sistemas ERM y derivados. Además, la compañía emitió un producto bastante completo desde software para cajas registradoras hasta call center con la posibilidad de alquilar operadores por un monto de hasta 200 almas.


Yo mismo trabajé en una aplicación front-end para call center.


Es fácil imaginar que es información de todos los componentes del sistema que fluye hacia la aplicación del operador. Y si tenemos en cuenta el hecho de que no se trata de un solo operador, sino también de un gerente y administrador, entonces puede imaginar cuánta comunicación e información la aplicación debe "digerir" y relacionarse entre sí.


Cuando el proyecto ya se lanzó e incluso funcionó de manera bastante estable por sí mismo, el problema de la transparencia del sistema surgió en todo su crecimiento.


Este es el punto. Hay muchos componentes y todos trabajan con sus fuentes de datos. Pero casi todos estos componentes alguna vez fueron escritos como productos independientes. Es decir, no como un elemento del sistema general, sino como decisiones separadas para la venta. Como resultado, no existe una API (sistema) única ni estándares de comunicación comunes entre ellos.


Te lo explicaré. Algunos componentes envían JSON, "alguien" envía líneas con clave: valor dentro, "alguien" envía binarios en general y hace lo que quiera con él. Pero, y la solicitud final para el centro de llamadas tuvo que obtenerlo todo y procesarlo de alguna manera. Bueno y lo más importante, no había ningún enlace en el sistema que pudiera reconocer que el formato / estructura de datos ha cambiado. Si algún componente envió JSON ayer, y hoy decidió enviar binario, nadie verá esto. Solo la aplicación final comenzará a fallar como se esperaba.


Pronto se hizo evidente (para quienes me rodeaban, no para mí, ya que hablé sobre el problema en la etapa de diseño) que la ausencia de un "lenguaje único de comunicación" entre los componentes conduce a problemas serios.


El caso más simple es cuando el cliente solicitó cambiar algún conjunto de datos. Describen la tarea al joven que "posee" el componente para trabajar con bases de datos de bienes / servicios, por ejemplo. Él hace su trabajo, implementa un nuevo conjunto de datos y, para él, imbécil, todo funciona. Pero, el día después de la actualización ... oh ... la aplicación en el centro de llamadas de repente comienza a funcionar no como esperan de ella.


Probablemente ya lo hayas adivinado. Nuestro héroe cambió no solo el conjunto de datos, sino también la estructura de datos que su componente envía al sistema. Como resultado, la aplicación de centro de llamadas simplemente ya no puede trabajar con este componente y otras dependencias vuelan a lo largo de la cadena.


Comenzaron a pensar en lo que, de hecho, queremos obtener al salir. Como resultado, formulamos los siguientes requisitos para una posible solución:


Primero y principal: cualquier cambio en la estructura de datos debe ser inmediatamente "resaltado" en el sistema. Si alguien ha realizado cambios en algún lugar y estos cambios son incompatibles con lo que el sistema espera, debería producirse un error en la etapa de prueba de componentes, que se ha modificado.


El segundo Los tipos de datos deben verificarse no solo durante la compilación, sino también durante el tiempo de ejecución.


El tercero Dado que un gran número de personas con niveles de habilidad completamente diferentes trabajan en componentes, el lenguaje de descripción debería ser más simple.


Cuarto . Cualquiera sea la solución, debería ser lo más conveniente posible trabajar con ella. Si es posible, el IDE debe resaltar tanto como sea posible.


El primer pensamiento fue implementar protobuf. Simple, legible y fácil. Escritura de datos estricta. Parece ser lo que recetó el médico. Pero, por desgracia, no toda la sintaxis de protobuf parecía simple. Además, incluso el protocolo compilado requería una biblioteca adicional, pero Javascript no era compatible con protobuf y era el resultado del trabajo comunitario. En general, se negaron.


Entonces surgió la idea de describir el protocolo en JSON. Bueno, cuanto mas facil?


Bueno, entonces renuncié. Y sobre esto, esta publicación podría haberse completado, ya que después de mi partida, nadie más comenzó a lidiar particularmente con el problema.


Sin embargo, dado un par de proyectos personales en los que el tema de la comunicación entre los componentes volvió a alcanzar su máximo potencial, decidí comenzar a implementar la idea por mi cuenta. Lo que se discutirá a continuación.


Por lo tanto, presento a su atención el proyecto ceres , que incluye:


  • generador de protocolo
  • proveedor
  • el cliente
  • implementación de transportes

Protocolo


La tarea era hacer que:


  • fue fácil establecer la estructura del mensaje en el sistema.
  • fue fácil determinar el tipo de datos de todos los campos de mensaje.
  • fue posible definir entidades auxiliares y referirnos a ellas.
  • y por supuesto, para que todo esto quede resaltado por el IDE

Creo que de una manera completamente natural, como lenguaje en el que se convierte el protocolo, se eligió TypeScript no como Javascript puro. Es decir, todo lo que hace el generador de protocolos es convertir JSON en Typecript.


Para describir los mensajes disponibles en el sistema, solo necesita saber qué es JSON. Con lo cual, estoy seguro de que nadie tiene ningún problema.


En lugar de Hello World, ofrezco un ejemplo no menos trillado: el chat.


{ "Events": { "NewMessage": { "message": "ChatMessage" }, "UsersListUpdated": { "users": "Array<User>" } }, "Requests": { "GetUsers": {}, "AddUser": { "user": "User" } }, "Responses": { "UsersList": { "users": "Array<User>" }, "AddUserResult": { "error?": "asciiString" } }, "ChatMessage": { "nickname": "asciiString", "message": "utf8String", "created": "datetime" }, "User": { "nickname": "asciiString" }, "version": "0.0.1" } 

Todo es escandalosamente simple. Tenemos un par de eventos NewMessage y UsersListUpdated; así como un par de solicitudes UsersList y AddUserResult. Hay dos entidades más: ChatMessage y Usuario.


Como puede ver, la descripción es bastante transparente y comprensible. Un poco sobre las reglas.


  • Un objeto en JSON se convertirá en una clase en el protocolo generado
  • El valor de la propiedad es una definición de tipo de datos o una referencia a una clase (entidad)
  • Los objetos anidados desde el punto de vista del protocolo generado se convertirán en clases "anidadas", es decir, los objetos anidados heredarán todas las propiedades de sus padres.

Ahora es suficiente generar un protocolo para comenzar a usarlo.


 npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r 

Como resultado, obtenemos un protocolo generado por Typecript. Nos conectamos y usamos:


imagen

Entonces, el protocolo ya le da algo al desarrollador:


  • El IDE destaca lo que tenemos en el protocolo. El IDE también destaca todas las propiedades esperadas.
  • Typecript, que ciertamente nos dirá si algo está mal con los tipos de datos. Por supuesto, esto se hace en la etapa de desarrollo, pero el protocolo en sí ya comprobará los tipos de datos de tiempo de ejecución y arrojará una excepción si se detecta una violación
  • En general, puede olvidarse de la validación. El protocolo hará todas las verificaciones necesarias.
  • El protocolo generado no requiere ninguna biblioteca adicional. Todo lo que necesita para trabajar, ya lo contiene. Y es muy conveniente.

Sí, el tamaño del protocolo generado puede sorprenderlo, por decir lo menos. Pero no se olvide de la minificación, a la que el archivo de protocolo generado se presta bien.

Ahora podemos "empacar" el mensaje y enviarlo


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); const packet: Uint8Array = message.stringify(); // Send packet somewhere 

Es importante hacer una reserva aquí, el paquete será una matriz de bytes, lo cual es muy bueno y correcto desde el punto de vista de la carga de tráfico, ya que enviar los mismos "costos" JSON, por supuesto, es más costoso. Sin embargo, el protocolo tiene un truco: en modo de depuración generará JSON legible para que el desarrollador pueda "mirar" el tráfico y ver qué sucede.


Esto se hace directamente en tiempo de ejecución.


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); // Switch to debug mode Protocol.Protocol.state.debug(true); // Now packet will be present as JSON string const packet: string = message.stringify(); // Send packet somewhere 

En el servidor (o cualquier otro destinatario), podemos descomprimir fácilmente el mensaje:


 import * as Protocol from '../../protocol/protocol.chat'; const smth = Protocol.parse(packet); if (smth instanceof Error) { // Oops. Something wrong with this packet. } if (Protocol.ChatMessage.instanceOf(smth) === true) { // This is chat message } 

El protocolo admite todos los tipos de datos principales:


TipoValoresDescripciónTamaño, bytes
utf8StringCadena codificada UTF8x
asciiStringcadena ascii1 carácter - 1 byte
int8-128 a 1271
int16-32768 a 327672
int32-2147483648 a 21474836474 4
uint80 a 2551
uint160 a 655352
uint320 a 42949672954 4
float321.2x10 -38 a 3.4x10 384 4
float645.0x10 -324 a 1.8x10 3088
booleano1

Dentro del protocolo, estos tipos de datos se denominan primitivos. Sin embargo, otra característica del protocolo es que le permite agregar sus propios tipos de datos (llamados "tipos de datos adicionales").


Por ejemplo, probablemente ya haya notado que ChatMessage tiene un campo creado con un tipo de datos de fecha y hora . En el nivel de aplicación, este tipo corresponde a la Fecha , y dentro del protocolo se almacena (y se envía) como uint32 .


Agregar su tipo al protocolo es bastante simple. Por ejemplo, si queremos tener un tipo de datos de correo electrónico , digamos para el siguiente mensaje en el protocolo:


 { "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" } 

Todo lo que necesita hacer es escribir una definición para el tipo de correo electrónico.


 export const AdvancedTypes: { [key:string]: any} = { email: { // Binary type or primitive type binaryType : 'asciiString', // Initialization value. This value is used as default value init : '""', // Parse value. We should not do any extra decode operations with it parse : (value: string) => { return value; }, // Also we should not do any encoding operations with it serialize : (value: string) => { return value; }, // Typescript type tsType : 'string', // Validation function to valid value validate : (value: string) => { if (typeof value !== 'string'){ return false; } if (value.trim() === '') { // Initialization value is "''", so we allow use empty string. return true; } const validationRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/gi; return validationRegExp.test(value); }, } }; 

Eso es todo Al generar el protocolo, obtenemos soporte para el nuevo tipo de datos de correo electrónico . Cuando intentamos crear una entidad con la dirección incorrecta, recibimos un error


 const user: Protocol.User = new Protocol.User({ nickname: 'Brad', email: 'not_valid_email' }); console.log(user); 

Oh ...


 Error: Cannot create class of "User" due error(s): - Property "email" has wrong value; validation was failed with value "not_valid_email". 

Por lo tanto, el protocolo simplemente no permite datos "malos" en el sistema.


Tenga en cuenta que, al definir un nuevo tipo de datos, especificamos un par de propiedades clave:


  • binaryType : una referencia a un tipo de datos primitivo que debe usarse para almacenar, codificar / decodificar datos. En este caso, indicamos que la dirección es una cadena ascii.
  • tsType es una referencia al tipo de Javascript, es decir, cómo debe representarse el tipo de datos en el entorno de Javascript. En este caso estamos hablando de cuerdas
  • También vale la pena señalar que necesitamos definir un nuevo tipo de datos solo al momento de generar el protocolo. En la salida, obtenemos un protocolo generado que ya contiene un nuevo tipo de datos.

Puede ver información detallada sobre todas las características del protocolo aquí ceres.protocol .

Proveedor y cliente


En general, el protocolo en sí puede usarse para organizar la comunicación. Sin embargo, si estamos hablando del navegador y nodejs, entonces el proveedor y el cliente están disponibles.


Cliente


Creación


Para crear un cliente, necesita el cliente y el transporte.


Instalación


 # Install consumer (client) npm install ceres.consumer --save # Install transport npm install ceres.consumer.browser.ws --save 

Creación


 import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); 

El cliente, así como el proveedor, están diseñados específicamente para el protocolo. Es decir, solo funcionarán con el protocolo (ceres.protocol).

Eventos


Una vez creado el cliente, el desarrollador puede suscribirse a eventos


 import * as Protocol from '../../protocol/protocol.chat'; import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); // Subscribe to event consumer.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }).then(() => { console.log('Subscription to "NewMessage" is done'); }).catch((error: Error) => { console.log(`Fail to subscribe to "NewMessage" due error: ${error.message}`); }); 

Tenga en cuenta que el cliente llamará al controlador de eventos solo si los datos del mensaje son completamente correctos. En otras palabras, nuestra aplicación está protegida contra datos incorrectos y el controlador de eventos NewMessage siempre se llamará con una instancia de Protocol.Events.NewMessage como argumento.


Naturalmente, el cliente puede generar eventos.


 consumer.emit(new Protocol.Events.NewMessage({ message: 'This is new message' })).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

Tenga en cuenta que no especificamos nombres de eventos en ninguna parte, simplemente usamos un enlace a la clase desde el protocolo o pasamos una instancia del mismo.


También podemos enviar un mensaje a un grupo limitado de destinatarios especificando un objeto simple de tipo { [key: string]: string } como segundo argumento. Dentro de ceres, este objeto se llama consulta .


 consumer.emit( new Protocol.Events.NewMessage({ message: 'This is new message' }), { location: "UK" } ).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

Por lo tanto, al indicar { location: "UK" } , podemos estar seguros de que solo los clientes que hayan identificado su posición como Reino Unido recibirán este mensaje.


Para asociar el propio cliente con una consulta específica, solo necesita llamar al método ref :


 consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); }); 

Después de haber conectado al cliente con la consulta , tiene la oportunidad de recibir mensajes "personales" o "grupales".


Consultas


También podemos hacer solicitudes


 consumer.request( new Protocol.Requests.GetUsers(), // Request Protocol.Responses.UsersList // Expected response ).then((response: Protocol.Responses.UsersList) => { console.log(`Available users: ${response.users}`); }).catch((error: Error) => { console.log(`Fail to get users list due error: ${error.message}`); }); 

Vale la pena prestar atención a que, como segundo argumento, especificamos el resultado esperado ( Protocol.Responses.UsersList ), lo que significa que nuestra solicitud se completará con éxito solo si la respuesta es una instancia de UsersList , en todos los demás casos " caeremos " a atrapar Nuevamente, esto nos asegura el procesamiento de datos incorrectos.


El cliente mismo también puede hablar con aquellos que pueden procesar solicitudes. Para hacer esto, solo necesita "identificarse" como "responsable" de la solicitud.


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, callback: (error: Error | null, results : any ) => any) { // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; consumer.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers, { location: "UK" }).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

Tenga en cuenta que, opcionalmente, como tercer argumento, podemos especificar un objeto de consulta que se puede utilizar para identificar al cliente. Por lo tanto, si alguien envía una consulta con consulta , digamos, { location: "RU" } , entonces nuestro cliente no recibirá dicha solicitud, porque su consulta { location: "UK" } .


Una consulta puede incluir un número ilimitado de propiedades. Por ejemplo, puede especificar lo siguiente


 { location: "UK", type: "managers" } 

Luego, además de una coincidencia de consulta completa, también procesaremos con éxito las siguientes consultas:


 { location: "UK" } 

o


 { type: "managers" } 

Proveedor


Creación


Para crear un proveedor (así como para crear un cliente), necesita el proveedor y el transporte.


Instalación


 # Install provider npm install ceres.provider --save # Install transport npm install ceres.provider.node.ws --save 

Creación


 import Transport, { ConnectionParameters } from 'ceres.provider.node.ws'; import Provider from 'ceres.provider'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ port: 3005 })); // Create provider const provider: Provider = new Provider(transport); 

Desde el momento en que se crea el proveedor, puede aceptar conexiones de clientes.


Eventos


Además del cliente, el proveedor puede "escuchar" los mensajes y generarlos.


Escuchando


 // Subscribe to event provider.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }); 

Generar


 provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' })); 

Consultas


Naturalmente, el proveedor puede (y debe) "escuchar" las solicitudes


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, clientID: string, callback: (error: Error | null, results : any ) => any) { console.log(`Request from client ${clientId} was gotten.`); // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; provider.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

Solo hay una diferencia con el cliente, el proveedor además del cuerpo de la solicitud recibirá un ID de cliente único, que se asigna automáticamente a todos los clientes conectados.


Ejemplo


De hecho, realmente no quiero aburrirlo con extractos de la documentación, estoy seguro de que será más fácil y más interesante para usted ver un pequeño fragmento de código.


Puede instalar fácilmente el ejemplo de chat descargando las fuentes y realizando un par de acciones simples


Instalación y lanzamiento del cliente


 cd chat/client npm install npm start 

El cliente estará disponible en http: // localhost: 3000 . Abra inmediatamente un par de pestañas con el cliente para ver la "comunicación".


Instalación y lanzamiento del proveedor (servidor)


 cd chat/server npm install ts-node ./server.ts 

Estoy seguro de que está familiarizado con el paquete ts-node , pero si no, le permite ejecutar archivos TS. Si no desea instalar, simplemente compile el servidor y luego ejecute el archivo JS.


 cd chat/server npm run build node ./build/server/server.js 

Que? De nuevo?


Anticipando preguntas sobre por qué demonios inventar otra bicicleta, porque hay tantas soluciones ya resueltas, desde protobuf hasta hardcore joynr de BMW, solo puedo decir que fue interesante para mí. Todo el proyecto se realizó únicamente por iniciativa personal sin ningún tipo de apoyo, en mi tiempo libre del trabajo.


Es por eso que sus comentarios son de particular valor para mí. En un intento por motivarte de alguna manera, puedo prometer que por cada estrella en Github, acariciaré al hámster (lo cual, por decirlo suavemente, no me gusta). Para el tenedor, uhhh, le rascaré su pussiko ... brrrr.


El hámster no es mío, el hámster del hijo .


Además, en un par de semanas, el proyecto se pondrá a prueba a mis antiguos colegas (que mencioné al principio de la publicación y que estaban interesados ​​en cuál era la versión alfa). El objetivo es depurar y ejecutar múltiples componentes. Realmente espero que funcione.


Enlaces y paquetes


El proyecto está alojado por dos repositorios.


  • fuentes ceres : ceres.provider, ceres.consumer y todos los transportes disponibles en la actualidad.
  • fuentes del generador de protocolo ceres.protocol

NPM siguientes paquetes disponibles



Bueno y ligero.

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


All Articles