Comme le montre la pratique, une grande partie des problèmes ne se posent pas à cause des solutions elles-mêmes, mais à cause de la façon dont la communication entre les composants du système se produit. S'il y a un gâchis dans la communication entre les composants du système, alors, comme vous n'essayez pas de bien écrire les composants individuels, le système dans son ensemble échouera.
Attention À l'intérieur du vélo.
Problème ou énoncé du problème
Il y a quelque temps, il est arrivé de travailler sur un projet pour une entreprise qui apporte aux masses des délices tels que le CRM, les systèmes ERM et les dérivés. En outre, la société a publié un produit assez complet, du logiciel pour les caisses enregistreuses au centre d'appels, avec la possibilité de louer des opérateurs jusqu'à 200 âmes.
J'ai moi-même travaillé sur une application front-end de call-center.
Il est facile d’imaginer que ce sont les informations de tous les composants du système qui circulent dans l’application de l’opérateur. Et si nous prenons en compte le fait qu'il ne s'agit pas d'un seul opérateur, mais également d'un gestionnaire et d'un administrateur, alors vous pouvez imaginer la quantité de communication et d'informations que l'application doit «digérer» et relier les unes aux autres.
Lorsque le projet était déjà lancé et fonctionnait même de manière assez stable pour lui-même, le problème de la transparence du système se posait dans toute sa croissance.
Voilà le point. Il existe de nombreux composants et ils fonctionnent tous avec leurs sources de données. Mais presque tous ces composants ont déjà été écrits en tant que produits autonomes. Ce n'est pas en tant qu'élément du système global, mais en tant que décisions distinctes à vendre. En conséquence, il n'y a pas d'API (système) unique et pas de normes de communication communes entre elles.
Je vais vous expliquer. Certains composants envoient du JSON, «quelqu'un» envoie des lignes avec la clé: value inside, «quelqu'un» envoie le binaire en général et faites ce que vous voulez avec. Mais, et la demande finale pour le centre d'appels devait obtenir tout cela et le traiter d'une manière ou d'une autre. Eh bien et surtout, il n'y avait aucun lien dans le système qui pouvait reconnaître que le format / la structure des données avait changé. Si un composant a envoyé JSON hier et a décidé aujourd'hui d'envoyer du binaire - personne ne le verra. Seule l'application finale commencera à planter comme prévu.
Il est vite devenu évident (pour ceux qui m'entouraient, pas pour moi, car j'avais parlé du problème au stade de la conception) que l'absence d'un «langage de communication unifié» entre les composants entraînait de graves problèmes.
Le cas le plus simple est lorsque le client a demandé de modifier un ensemble de données. Ils annulent la tâche au jeune homme qui «détient» le composant pour travailler avec des bases de données de biens / services, par exemple. Il fait son travail, implémente un nouvel ensemble de données, et pour lui, connard, tout fonctionne. Mais, le lendemain de la mise à jour ... oh ... l'application du centre d'appels commence soudainement à ne pas fonctionner comme prévu.
Vous l'avez probablement déjà deviné. Notre héros a changé non seulement l'ensemble de données, mais aussi la structure de données que son composant envoie au système. En conséquence, l'application de centre d'appels n'est tout simplement plus en mesure de travailler avec ce composant, et d'autres dépendances volent le long de la chaîne.
Ils ont commencé à réfléchir à ce que nous voulons en fait sortir. En conséquence, nous avons formulé les exigences suivantes pour une solution potentielle:
D'abord et avant tout: tout changement dans la structure des données doit être immédiatement «mis en évidence» dans le système. Si quelqu'un a apporté des modifications quelque part et que ces modifications sont incompatibles avec les attentes du système, une erreur doit se produire lors de la phase de test des composants, qui a été modifiée.
Le deuxième . Les types de données doivent être vérifiés non seulement lors de la compilation, mais également lors de l'exécution.
Le troisième . Étant donné qu'un grand nombre de personnes ayant des niveaux de compétence complètement différents travaillent sur des composants, le langage de description devrait être plus simple.
Quatrièmement . Quelle que soit la solution, il devrait être aussi pratique que possible de travailler avec elle. Si possible, l'IDE doit mettre en évidence autant que possible.
La première pensée a été d'implémenter protobuf. Simple, lisible et facile. Saisie stricte des données. Il semble que ce soit ce que le médecin a ordonné. Mais hélas, toute la syntaxe du protobuf ne semblait pas simple. De plus, même le protocole compilé nécessitait une bibliothèque supplémentaire, mais Javascript n'était pas pris en charge par protobuf et était le résultat d'un travail communautaire. En général, ils ont refusé.
Puis l'idée est venue de décrire le protocole en JSON. Eh bien, combien plus facile?
Eh bien, je quitte. Et à ce sujet, ce poste aurait pu être achevé, car après mon départ, personne d'autre n'a commencé à traiter le problème de manière particulièrement étroite.
Cependant, étant donné quelques projets personnels où la question de la communication entre les composants a de nouveau atteint son plein potentiel, j'ai décidé de commencer à mettre en œuvre l'idée par moi-même. Ce qui sera discuté ci-dessous.
Je présente donc à votre attention le projet ceres , qui comprend:
- générateur de protocole
- fournisseur
- le client
- mise en place de transports
Protocole
La tâche était de faire en sorte que:
- il était facile de définir la structure des messages dans le système.
- il était facile de déterminer le type de données de tous les champs de message.
- il a été possible de définir des entités auxiliaires et de s'y référer.
- et bien sûr, pour que tout cela soit mis en évidence par l'IDE
Je pense que d'une manière tout à fait naturelle, en tant que langue dans laquelle le protocole est converti, Typescript a été choisi non pas en pur Javascript. Autrement dit, tout ce que fait le générateur de protocole est de transformer JSON en Typescript.
Pour décrire les messages disponibles dans le système, il vous suffit de savoir ce qu'est JSON. Avec qui, je suis sûr que personne n'a de problème.
Au lieu de Hello World, je vous propose un exemple non moins galvaudé - le 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" }
Tout est incroyablement simple. Nous avons quelques événements NewMessage et UsersListUpdated; ainsi que quelques requêtes UsersList et AddUserResult. Il existe deux autres entités: ChatMessage et User.
Comme vous pouvez le voir, la description est assez transparente et compréhensible. Un peu sur les règles.
- Un objet en JSON deviendra une classe dans le protocole généré
- La valeur de la propriété est une définition de type de données ou une référence à une classe (entité)
- Du point de vue du protocole généré, les objets imbriqués deviendront des classes "imbriquées", c'est-à-dire que les objets imbriqués hériteront de toutes les propriétés de leurs parents.
Il ne vous reste plus qu'à générer un protocole pour commencer à l'utiliser.
npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r
En conséquence, nous obtenons un protocole généré par Typescript. Nous connectons et utilisons:

Ainsi, le protocole donne déjà quelque chose au développeur:
- L'IDE met en évidence ce que nous avons dans le protocole. L'IDE met également en évidence toutes les propriétés attendues.
- Type de script, qui nous dira certainement si quelque chose ne va pas avec les types de données. Bien sûr, cela se fait au stade du développement, mais le protocole lui-même vérifiera déjà les types de données au moment de l'exécution et lèvera une exception si une violation est détectée
- En général, vous pouvez oublier la validation. Le protocole fera toutes les vérifications nécessaires.
- Le protocole généré ne nécessite aucune bibliothèque supplémentaire. Tout ce dont il a besoin pour travailler, il le contient déjà. Et c'est très pratique.
Oui, la taille du protocole généré peut vous surprendre, c'est le moins qu'on puisse dire. Mais n'oubliez pas la minification à laquelle le fichier de protocole généré se prête bien.
Maintenant, nous pouvons "emballer" le message et l'envoyer
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();
Il est important de faire une réservation ici, le paquet sera un tableau d'octets, ce qui est très bon et correct du point de vue de la charge de trafic, car envoyer les mêmes "coûts" JSON, bien sûr, plus chers. Cependant, le protocole a une fonctionnalité - en mode débogage, il générera un JSON lisible afin que le développeur puisse «regarder» le trafic et voir ce qui se passe.
Cela se fait directement au moment de l'exécution.
import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() });
Sur le serveur (ou tout autre destinataire), nous pouvons facilement décompresser le message:
import * as Protocol from '../../protocol/protocol.chat'; const smth = Protocol.parse(packet); if (smth instanceof Error) {
Le protocole prend en charge tous les principaux types de données:
Tapez | Les valeurs | La description | Taille, octets |
---|
utf8String | | Chaîne codée UTF8 | x |
asciiString | | chaîne ascii | 1 caractère - 1 octet |
int8 | -128 à 127 | | 1 |
int16 | -32768 à 32767 | | 2 |
int32 | -2147483648 à 2147483647 | | 4 |
uint8 | 0 à 255 | | 1 |
uint16 | 0 à 65535 | | 2 |
uint32 | 0 à 4294967295 | | 4 |
float32 | 1,2x10 -38 à 3,4x10 38 | | 4 |
float64 | 5,0x10 -324 à 1,8x10 308 | | 8 |
booléen | | | 1 |
Dans le protocole, ces types de données sont appelés primitifs. Cependant, une autre caractéristique du protocole est qu'il vous permet d'ajouter vos propres types de données (appelés "types de données supplémentaires").
Par exemple, vous avez probablement déjà remarqué que ChatMessage a un champ créé avec un type de données datetime . Au niveau de l'application - ce type correspond à Date , et à l'intérieur du protocole est stocké (et envoyé) en tant que uint32 .
Ajouter votre type au protocole est assez simple. Par exemple, si nous voulons avoir un type de données e-mail , disons pour le message suivant dans le protocole:
{ "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" }
Tout ce que vous avez à faire est d'écrire une définition du type d'e-mail.
export const AdvancedTypes: { [key:string]: any} = { email: {
C’est tout. En générant le protocole, nous obtenons la prise en charge du nouveau type de données de messagerie . Lorsque nous essayons de créer une entité avec la mauvaise adresse, nous obtenons une erreur
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".
Ainsi, le protocole n'autorise tout simplement pas les «mauvaises» données dans le système.
Notez que lors de la définition d'un nouveau type de données, nous avons spécifié quelques propriétés clés:
- binaryType - une référence à un type de données primitif qui doit être utilisé pour stocker, encoder / décoder des données. Dans ce cas, nous indiquons que l'adresse est une chaîne ascii.
- tsType est une référence au type Javascript, c'est-à-dire comment le type de données doit être représenté dans l'environnement Javascript. Dans ce cas, nous parlons de chaîne
- Il convient également de noter que nous devons définir un nouveau type de données uniquement au moment de la génération du protocole. En sortie, nous obtenons un protocole généré qui contient déjà un nouveau type de données.
Vous pouvez voir des informations détaillées sur toutes les fonctionnalités du protocole ici ceres.protocol .
Fournisseur et client
Dans l'ensemble, le protocole lui-même peut être utilisé pour organiser la communication. Cependant, si nous parlons du navigateur et de nodejs, le fournisseur et le client sont disponibles.
Client
La création
Pour créer un client, vous avez besoin du client et du transport.
L'installation
La création
import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer';
Le client, ainsi que le fournisseur, sont conçus spécifiquement pour le protocole. Autrement dit, ils ne fonctionneront qu'avec le protocole (ceres.protocol).
Les événements
Une fois le client créé, le développeur peut s'abonner aux événements
import * as Protocol from '../../protocol/protocol.chat'; import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer';
Veuillez noter que le client n'appellera le gestionnaire d'événements que si les données du message sont complètement correctes. En d'autres termes, notre application est protégée contre les données incorrectes et le gestionnaire d' événements NewMessage sera toujours appelé avec une instance de Protocol.Events.NewMessage comme argument.
Naturellement, le client peut générer des événements.
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}`); });
Notez que nous ne spécifions aucun nom d'événement nulle part, nous utilisons simplement un lien vers la classe à partir du protocole, ou passons une instance de celui-ci.
Nous pouvons également envoyer un message à un groupe limité de destinataires en spécifiant un simple objet de type { [key: string]: string }
comme deuxième argument. Dans ceres, cet objet est appelé requête .
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}`); });
Ainsi, en indiquant en plus { location: "UK" }
, nous pouvons être sûrs que seuls les clients qui ont identifié leur position comme UK recevront ce message.
Pour associer le client lui-même à une requête spécifique, il suffit d'appeler la méthode ref :
consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); });
Après avoir connecté le client à la requête , il a la possibilité de recevoir des messages "personnels" ou "de groupe".
Demandes
Nous pouvons également faire des demandes
consumer.request( new Protocol.Requests.GetUsers(),
Il convient de noter qu'en tant que deuxième argument, nous spécifions le résultat attendu ( Protocol.Responses.UsersList ), ce qui signifie que notre demande ne sera menée à bien que si la réponse est une instance de UsersList , dans tous les autres cas, nous «tomberons» sur attraper Encore une fois, cela nous empêche de traiter des données incorrectes.
Le client lui-même peut également parler à ceux qui peuvent traiter les demandes. Pour ce faire, il vous suffit de vous "identifier" comme "responsable" de la demande.
function processRequestGetUsers(request: Protocol.Requests.GetUsers, callback: (error: Error | null, results : any ) => any) {
Notez, facultativement, que le troisième argument, nous pouvons spécifier un objet de requête qui peut être utilisé pour identifier le client. Ainsi, si quelqu'un envoie une requête avec une requête , disons { location: "RU" }
, alors notre client ne recevra pas une telle requête, car sa requête { location: "UK" }
.
Une requête peut inclure un nombre illimité de propriétés. Par exemple, vous pouvez spécifier les éléments suivants
{ location: "UK", type: "managers" }
Ensuite, en plus d'une correspondance de requête complète, nous traiterons également avec succès les requêtes suivantes:
{ location: "UK" }
ou
{ type: "managers" }
Fournisseur
La création
Pour créer un fournisseur (ainsi que pour créer un client), vous avez besoin du fournisseur et du transport.
L'installation
La création
import Transport, { ConnectionParameters } from 'ceres.provider.node.ws'; import Provider from 'ceres.provider';
À partir du moment où le fournisseur est créé, il peut accepter les connexions des clients.
Les événements
En plus du client, le fournisseur peut "écouter" les messages et les générer.
Écoute
Générer
provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' }));
Demandes
Naturellement, le fournisseur peut (et devrait) «écouter» les demandes
function processRequestGetUsers(request: Protocol.Requests.GetUsers, clientID: string, callback: (error: Error | null, results : any ) => any) { console.log(`Request from client ${clientId} was gotten.`);
Il n'y a qu'une seule différence par rapport au client, le fournisseur en plus du corps de la demande recevra un clientId unique, qui est attribué automatiquement à tous les clients connectés.
Exemple
En fait, je ne veux vraiment pas vous ennuyer avec des extraits de la documentation, je suis sûr qu'il sera plus facile et plus intéressant pour vous de simplement voir un court morceau de code.
Vous pouvez facilement installer l'exemple de discussion en téléchargeant les sources et en effectuant quelques actions simples
Installation et lancement du client
cd chat/client npm install npm start
Le client sera disponible sur http: // localhost: 3000 . Ouvrez immédiatement quelques onglets avec le client pour voir la "communication".
Installation et lancement du fournisseur (serveur)
cd chat/server npm install ts-node ./server.ts
Je suis sûr que vous connaissez le paquet ts-node , mais sinon, il vous permet d'exécuter des fichiers TS. Si vous ne souhaitez pas installer, compilez simplement le serveur, puis exécutez le fichier JS.
cd chat/server npm run build node ./build/server/server.js
Quoi? Encore?!
Anticipant les questions sur pourquoi diable inventer une autre moto, car il y a tellement de solutions éprouvées autour, à partir de protobuf et se terminant par le joynr hardcore de BMW, je peux seulement dire que c'était intéressant pour moi. L'ensemble du projet a été réalisé uniquement sur une initiative personnelle sans aucun soutien, dans mon temps libre du travail.
C'est pourquoi vos commentaires sont particulièrement précieux pour moi. Dans une tentative de vous motiver d'une manière ou d'une autre, je peux vous promettre que pour chaque étoile sur github, je vais caresser le hamster (ce qui, pour le dire doucement, je n'aime pas). Pour la fourchette, euhhh, je vais gratter son pussiko ... brrrr.
Le hamster n'est pas le mien, le hamster du fils .
De plus, dans quelques semaines, le projet sera testé pour mes anciens collègues (que j'ai mentionnés au début de l'article et qui étaient intéressés par la version alfa). L'objectif est le débogage et l'exécution sur plusieurs composants. J'espère vraiment que ça marche.
Liens et packages
Le projet est hébergé par deux référentiels
- sources de ceres : ceres.provider, ceres.consumer et tous les transports disponibles aujourd'hui.
- sources du générateur de protocole ceres.protocol
NPM packages suivants disponibles
Bon et léger.