Wie die Praxis zeigt, entsteht ein großer Teil der Probleme nicht aufgrund der Lösungen selbst, sondern aufgrund der Art und Weise, wie die Kommunikation zwischen den Komponenten des Systems erfolgt. Wenn die Kommunikation zwischen den Komponenten des Systems durcheinander ist, fällt das gesamte System aus, da Sie nicht versuchen, die einzelnen Komponenten gut zu schreiben.
Achtung Im Fahrrad.
Problem oder Problemstellung
Vor einiger Zeit arbeitete es an einem Projekt für ein Unternehmen, das Massen wie CRM, ERM-Systeme und Derivate in die Massen bringt. Darüber hinaus hat das Unternehmen ein ziemlich umfassendes Produkt von Software für Registrierkassen an Callcenter herausgegeben, mit der Möglichkeit, Betreiber in Höhe von bis zu 200 Seelen zu mieten.
Ich selbst habe an einer Front-End-Anwendung für Call-Center gearbeitet.
Es ist leicht vorstellbar, dass Informationen aus allen Systemkomponenten in die Anwendung des Bedieners fließen. Und wenn wir die Tatsache berücksichtigen, dass es sich nicht um einen einzelnen Bediener, sondern auch um einen Manager und einen Administrator handelt, können Sie sich vorstellen, wie viel Kommunikation und Informationen die Anwendung „verdauen“ und miteinander in Beziehung setzen sollte.
Als das Projekt bereits gestartet wurde und sogar für sich selbst recht stabil funktionierte, trat das Problem der Systemtransparenz in seinem gesamten Wachstum auf.
Das ist der Punkt. Es gibt viele Komponenten, die alle mit ihren Datenquellen arbeiten. Aber fast alle diese Komponenten wurden einmal als eigenständige Produkte geschrieben. Das heißt, nicht als Element des Gesamtsystems, sondern als separate Verkaufsentscheidungen. Infolgedessen gibt es keine einzige (System-) API und keine gemeinsamen Kommunikationsstandards zwischen ihnen.
Ich werde es erklären. Einige Komponenten senden JSON, "jemand" sendet Zeilen mit Schlüssel: Wert im Inneren, "jemand" sendet im Allgemeinen Binärdateien und macht damit, was Sie wollen. Aber und die endgültige Bewerbung für das Callcenter musste alles bekommen und irgendwie verarbeiten. Nun und vor allem gab es keine Verknüpfung im System, die erkennen konnte, dass sich das Datenformat / die Datenstruktur geändert hat. Wenn eine Komponente gestern JSON gesendet hat und heute beschlossen hat, Binärdateien zu senden, wird dies niemand sehen. Nur die endgültige Anwendung stürzt wie erwartet ab.
Es wurde schnell klar (für die um mich herum, nicht für mich, da ich in der Entwurfsphase über das Problem gesprochen habe), dass das Fehlen einer „einheitlichen Kommunikationssprache“ zwischen den Komponenten zu ernsthaften Problemen führt.
Der einfachste Fall ist, wenn der Client aufgefordert wurde, einen Datensatz zu ändern. Sie schreiben die Aufgabe an den jungen Mann ab, der beispielsweise die Komponente für die Arbeit mit Datenbanken von Waren / Dienstleistungen „hält“. Er macht seine Arbeit, implementiert einen neuen Datensatz und für ihn, Arschloch, funktioniert alles. Aber am Tag nach dem Update ... oh ... beginnt die Anwendung im Callcenter plötzlich nicht mehr so zu funktionieren, wie sie es erwarten.
Sie haben es wahrscheinlich schon erraten. Unser Held hat nicht nur den Datensatz geändert, sondern auch die Datenstruktur, die seine Komponente an das System sendet. Infolgedessen kann die Call-Center-Anwendung einfach nicht mehr mit dieser Komponente arbeiten, und andere Abhängigkeiten verlaufen entlang der Kette.
Sie begannen darüber nachzudenken, was wir eigentlich raus wollen. Infolgedessen haben wir die folgenden Anforderungen für eine mögliche Lösung formuliert:
In erster Linie: Jede Änderung der Datenstruktur sollte sofort im System "hervorgehoben" werden. Wenn jemand irgendwo Änderungen vorgenommen hat und diese Änderungen nicht mit den Erwartungen des Systems kompatibel sind, sollte in der Phase des Komponententests ein Fehler auftreten, der geändert wurde.
Der zweite . Datentypen sollten nicht nur während der Kompilierung, sondern auch zur Laufzeit überprüft werden.
Der dritte . Da eine große Anzahl von Personen mit völlig unterschiedlichen Fähigkeiten an Komponenten arbeiten, sollte die Beschreibungssprache einfacher sein.
Viertens . Unabhängig von der Lösung sollte es so bequem wie möglich sein, damit zu arbeiten. Wenn möglich, sollte die IDE so viel wie möglich hervorheben.
Der erste Gedanke war, Protobuf zu implementieren. Einfach, lesbar und leicht. Strikte Dateneingabe. Es scheint das zu sein, was der Arzt befohlen hat. Leider schien nicht jede Protobuf-Syntax einfach zu sein. Darüber hinaus erforderte selbst ein kompiliertes Protokoll eine zusätzliche Bibliothek, aber Javascript wurde von protobuf nicht unterstützt und war das Ergebnis von Community-Arbeit. Im Allgemeinen lehnten sie ab.
Dann kam die Idee auf, das Protokoll in JSON zu beschreiben. Nun, wie viel einfacher?
Nun, dann höre ich auf. Und diesbezüglich hätte dieser Beitrag abgeschlossen werden können, da sich nach meiner Abreise niemand mehr besonders intensiv mit dem Problem befasste.
Angesichts einiger persönlicher Projekte, bei denen das Problem der Kommunikation zwischen Komponenten wieder voll ausgeschöpft wurde, beschloss ich, die Idee selbst umzusetzen. Was wird unten diskutiert.
Daher präsentiere ich Ihnen das ceres- Projekt, das Folgendes umfasst:
- Protokollgenerator
- Anbieter
- der Kunde
- Durchführung von Transporten
Protokoll
Die Aufgabe war es, es so zu machen, dass:
- Es war einfach, die Nachrichtenstruktur im System festzulegen.
- Es war einfach, den Datentyp aller Nachrichtenfelder zu bestimmen.
- es war möglich, Hilfsentitäten zu definieren und auf diese zu verweisen.
- und natürlich, damit all dies von der IDE hervorgehoben wird
Ich denke, dass auf ganz natürliche Weise Typescript als die Sprache gewählt wurde, in die das Protokoll konvertiert wird, nicht reines Javascript. Das heißt, alles, was der Protokollgenerator tut, ist, JSON in Typescript umzuwandeln.
Um die im System verfügbaren Nachrichten zu beschreiben, müssen Sie nur wissen, was JSON ist. Ich bin mir sicher, dass niemand Probleme damit hat.
Anstelle von Hello World biete ich ein nicht weniger abgedroschenes Beispiel an - 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" }
Alles ist unglaublich einfach. Wir haben einige NewMessage- und UsersListUpdated-Ereignisse. sowie einige UsersList- und AddUserResult-Anforderungen. Es gibt zwei weitere Entitäten: ChatMessage und User.
Wie Sie sehen können, ist die Beschreibung ziemlich transparent und verständlich. Ein wenig über die Regeln.
- Ein Objekt in JSON wird zu einer Klasse im generierten Protokoll
- Der Eigenschaftswert ist eine Datentypdefinition oder ein Verweis auf eine Klasse (Entität).
- Verschachtelte Objekte werden aus Sicht des generierten Protokolls zu "verschachtelten" Klassen, dh verschachtelte Objekte erben alle Eigenschaften ihrer Eltern.
Jetzt müssen Sie nur noch ein Protokoll erstellen, um es zu verwenden.
npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r
Als Ergebnis erhalten wir ein Typescript-generiertes Protokoll. Wir verbinden und nutzen:

Das Protokoll gibt dem Entwickler also bereits etwas:
- Die IDE hebt hervor, was wir im Protokoll haben. Die IDE hebt auch alle erwarteten Eigenschaften hervor.
- Typoskript, das uns sicherlich sagt, ob etwas mit Datentypen nicht stimmt. Dies geschieht natürlich in der Entwicklungsphase, aber das Protokoll selbst überprüft bereits zur Laufzeit die Datentypen und löst eine Ausnahme aus, wenn ein Verstoß festgestellt wird
- Im Allgemeinen können Sie die Validierung vergessen. Das Protokoll führt alle erforderlichen Überprüfungen durch.
- Das generierte Protokoll erfordert keine zusätzlichen Bibliotheken. Alles, was er zum Arbeiten braucht, enthält er bereits. Und es ist sehr praktisch.
Ja, die Größe des generierten Protokolls kann Sie, gelinde gesagt, überraschen. Vergessen Sie jedoch nicht die Minimierung, für die sich die generierte Protokolldatei gut eignet.
Jetzt können wir die Nachricht "packen" und senden
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();
Es ist wichtig, hier eine Reservierung vorzunehmen. Das Paket besteht aus einem Array von Bytes, was unter dem Gesichtspunkt der Verkehrslast sehr gut und korrekt ist, da das Senden derselben JSON- "Kosten" natürlich teurer ist. Das Protokoll verfügt jedoch über eine Funktion: Im Debug-Modus wird lesbarer JSON generiert, sodass der Entwickler den Datenverkehr „betrachten“ und sehen kann, was passiert.
Dies erfolgt direkt zur Laufzeit.
import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() });
Auf dem Server (oder einem anderen Empfänger) können wir die Nachricht einfach entpacken:
import * as Protocol from '../../protocol/protocol.chat'; const smth = Protocol.parse(packet); if (smth instanceof Error) {
Das Protokoll unterstützt alle wichtigen Datentypen:
Typ | Werte | Beschreibung | Größe, Bytes |
---|
utf8String | | UTF8-codierte Zeichenfolge | x |
asciiString | | ASCII-Zeichenfolge | 1 Zeichen - 1 Byte |
int8 | -128 bis 127 | | 1 |
int16 | -32768 bis 32767 | | 2 |
int32 | -2147483648 bis 2147483647 | | 4 |
uint8 | 0 bis 255 | | 1 |
uint16 | 0 bis 65535 | | 2 |
uint32 | 0 bis 4294967295 | | 4 |
float32 | 1,2 x 10 -38 bis 3,4 x 10 38 | | 4 |
float64 | 5,0 × 10 –324 bis 1,8 × 10 308 | | 8 |
Boolescher Wert | | | 1 |
Innerhalb des Protokolls werden diese Datentypen als primitiv bezeichnet. Ein weiteres Merkmal des Protokolls ist jedoch, dass Sie Ihre eigenen Datentypen hinzufügen können (sogenannte "zusätzliche Datentypen").
Zum Beispiel haben Sie wahrscheinlich bereits bemerkt, dass ChatMessage ein Feld mit einem Datetime- Datentyp erstellt hat. Auf Anwendungsebene entspricht dieser Typ dem Datum und wird im Protokoll als uint32 gespeichert (und gesendet).
Das Hinzufügen Ihres Typs zum Protokoll ist ganz einfach. Wenn wir beispielsweise einen E-Mail- Datentyp haben möchten, sagen wir für die folgende Nachricht im Protokoll:
{ "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" }
Sie müssen lediglich eine Definition für den E-Mail-Typ schreiben.
export const AdvancedTypes: { [key:string]: any} = { email: {
Das ist alles. Durch das Generieren des Protokolls erhalten wir Unterstützung für den neuen E-Mail- Datentyp. Wenn wir versuchen, eine Entität mit der falschen Adresse zu erstellen, wird eine Fehlermeldung angezeigt
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".
Das Protokoll lässt also einfach keine "schlechten" Daten in das System zu.
Bitte beachten Sie, dass wir beim Definieren eines neuen Datentyps einige Schlüsseleigenschaften angegeben haben:
- binaryType - Ein Verweis auf einen primitiven Datentyp, der zum Speichern, Codieren / Decodieren von Daten verwendet werden soll. In diesem Fall geben wir an, dass die Adresse eine ASCII-Zeichenfolge ist.
- tsType ist eine Referenz auf den Javascript-Typ, dh wie der Datentyp in der Javascript-Umgebung dargestellt werden soll. In diesem Fall handelt es sich um eine Zeichenfolge
- Es ist auch erwähnenswert, dass wir einen neuen Datentyp nur zum Zeitpunkt der Generierung des Protokolls definieren müssen. Am Ausgang erhalten wir ein generiertes Protokoll, das bereits einen neuen Datentyp enthält.
Detaillierte Informationen zu allen Protokollfunktionen finden Sie hier ceres.protocol .
Anbieter und Kunde
Im Großen und Ganzen kann das Protokoll selbst zum Organisieren der Kommunikation verwendet werden. Wenn es sich jedoch um den Browser und die NodeJS handelt, sind der Anbieter und der Client verfügbar.
Kunde
Schöpfung
Um einen Client zu erstellen, benötigen Sie den Client und den Transport.
Installation
Schöpfung
import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer';
Sowohl der Client als auch der Anbieter sind speziell für das Protokoll konzipiert. Das heißt, sie funktionieren nur mit dem Protokoll (ceres.protocol).
Ereignisse
Nachdem der Client erstellt wurde, kann der Entwickler Ereignisse abonnieren
import * as Protocol from '../../protocol/protocol.chat'; import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer';
Bitte beachten Sie, dass der Client den Ereignishandler nur aufruft, wenn die Nachrichtendaten vollständig korrekt sind. Mit anderen Worten, unsere Anwendung ist vor falschen Daten geschützt und der NewMessage- Ereignishandler wird immer mit einer Instanz von Protocol.Events.NewMessage als Argument aufgerufen.
Natürlich kann der Client Ereignisse generieren.
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}`); });
Beachten Sie, dass wir nirgendwo Ereignisnamen angeben, sondern entweder entweder einen Link zur Klasse aus dem Protokoll verwenden oder eine Instanz davon übergeben.
Wir können eine Nachricht auch an eine begrenzte Gruppe von Empfängern senden, indem wir als zweites Argument ein einfaches Objekt vom Typ { [key: string]: string }
angeben. Innerhalb von ceres wird dieses Objekt als Abfrage bezeichnet .
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}`); });
Durch die zusätzliche Angabe von { location: "UK" }
können wir sicher sein, dass nur Kunden, die ihre Position als UK identifiziert haben, diese Nachricht erhalten.
Um den Client selbst einer bestimmten Abfrage zuzuordnen , müssen Sie nur die ref- Methode aufrufen:
consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); });
Nachdem wir den Client mit der Abfrage verbunden haben , hat er die Möglichkeit, "persönliche" oder "Gruppen" -Nachrichten zu empfangen.
Anfragen
Wir können auch Anfragen stellen
consumer.request( new Protocol.Requests.GetUsers(),
Es ist zu beachten, dass wir als zweites Argument das erwartete Ergebnis angeben ( Protocol.Responses.UsersList ). Dies bedeutet, dass unsere Anforderung nur dann erfolgreich abgeschlossen wird, wenn die Antwort eine Instanz von UsersList ist. In allen anderen Fällen werden wir "fallen" fangen Dies versichert uns wiederum vor der Verarbeitung falscher Daten.
Der Kunde selbst kann auch mit denen sprechen, die Anfragen bearbeiten können. Dazu müssen Sie sich nur als "verantwortlich" für die Anfrage "identifizieren".
function processRequestGetUsers(request: Protocol.Requests.GetUsers, callback: (error: Error | null, results : any ) => any) {
Beachten Sie, dass wir optional als drittes Argument ein Abfrageobjekt angeben können, mit dem der Client identifiziert werden kann. Wenn also jemand eine Anfrage mit der Anfrage { location: "RU" }
sendet, erhält unser Client eine solche Anfrage nicht, da seine Anfrage { location: "UK" }
.
Eine Abfrage kann eine unbegrenzte Anzahl von Eigenschaften enthalten. Beispielsweise können Sie Folgendes angeben
{ location: "UK", type: "managers" }
Zusätzlich zu einer vollständigen Abfrageübereinstimmung werden wir dann auch die folgenden Abfragen erfolgreich verarbeiten:
{ location: "UK" }
oder
{ type: "managers" }
Anbieter
Schöpfung
Um einen Anbieter zu erstellen (sowie einen Kunden zu erstellen), benötigen Sie den Anbieter und den Transport.
Installation
Schöpfung
import Transport, { ConnectionParameters } from 'ceres.provider.node.ws'; import Provider from 'ceres.provider';
Ab dem Moment, in dem der Anbieter erstellt wird, kann er Verbindungen von Clients akzeptieren.
Ereignisse
Neben dem Client kann der Anbieter Nachrichten "abhören" und generieren.
Zuhören
Generieren
provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' }));
Anfragen
Natürlich kann (und sollte) der Anbieter Anfragen "abhören"
function processRequestGetUsers(request: Protocol.Requests.GetUsers, clientID: string, callback: (error: Error | null, results : any ) => any) { console.log(`Request from client ${clientId} was gotten.`);
Es gibt nur einen Unterschied zum Client. Der Anbieter erhält zusätzlich zum Anforderungshauptteil eine eindeutige clientId , die automatisch allen verbundenen Clients zugewiesen wird.
Beispiel
Tatsächlich möchte ich Sie nicht mit Auszügen aus der Dokumentation langweilen. Ich bin sicher, dass es für Sie einfacher und interessanter sein wird, nur ein kurzes Codefragment zu sehen.
Sie können das Chat-Beispiel einfach installieren, indem Sie die Quellen herunterladen und einige einfache Aktionen ausführen
Client-Installation und Start
cd chat/client npm install npm start
Der Client ist unter http: // localhost: 3000 verfügbar. Öffnen Sie sofort einige Registerkarten mit dem Client, um die "Kommunikation" anzuzeigen.
Installation und Start des Providers (Servers)
cd chat/server npm install ts-node ./server.ts
Ich bin sicher, dass Sie mit dem ts-node- Paket vertraut sind , aber wenn nicht, können Sie damit TS-Dateien ausführen. Wenn Sie nicht installieren möchten, kompilieren Sie einfach den Server und führen Sie die JS-Datei aus.
cd chat/server npm run build node ./build/server/server.js
Was? Schon wieder ?!
Ich kann nur sagen, dass es für mich interessant war, Fragen zu antizipieren, warum zum Teufel ein anderes Fahrrad erfunden werden sollte, weil es so viele bewährte Lösungen gibt, angefangen beim Protobuf bis hin zum Hardcore-Joynr von BMW. Das gesamte Projekt wurde ausschließlich auf persönliche Initiative ohne Unterstützung in meiner Freizeit von der Arbeit durchgeführt.
Deshalb ist Ihr Feedback für mich von besonderem Wert . Um dich irgendwie zu motivieren, kann ich versprechen, dass ich für jeden Stern auf Github den Hamster streicheln werde (was ich, gelinde gesagt, nicht mag). Für die Gabel, ähhh, werde ich seine Pussiko kratzen ... brrrr.
Der Hamster gehört nicht mir, dem Hamster des Sohnes .
Darüber hinaus wird das Projekt in ein paar Wochen meinen ehemaligen Kollegen zum Testen angeboten (die ich zu Beginn des Beitrags erwähnt habe und die an der Alfa-Version interessiert waren). Das Ziel ist das Debuggen und Ausführen mehrerer Komponenten. Ich hoffe wirklich, dass es funktioniert.
Links und Pakete
Das Projekt wird von zwei Repositorys gehostet
- ceres- Quellen: ceres.provider, ceres.consumer und alle heute verfügbaren Transporte.
- ceres.protocol Protokollgeneratorquellen
NPM folgende Pakete verfügbar
Gut und leicht.