実践が示すように、問題の大部分は解決策自体ではなく、システムのコンポーネント間の通信がどのように発生するかによって発生します。 システムのコンポーネント間の通信に混乱がある場合、個々のコンポーネントをうまく書き込もうとしないため、システム全体が失敗します。
ご注意 自転車の中。
問題または問題の説明
しばらく前に、CRM、ERMシステム、デリバティブなどの喜びを大衆にもたらすプロジェクトにたまたま取り組みました。 さらに、同社は、レジスター用のソフトウェアからコールセンターまで、オペレーターを最大200人までレンタルできる可能性のある、包括的な製品を発行しました。
私自身は、コールセンターのフロントエンドアプリケーションに取り組んでいました。
オペレーターのアプリケーションに流入するのは、すべてのシステムコンポーネントからの情報であることは容易に想像できます。 そして、それが単一のオペレーターではなく、マネージャーと管理者であるという事実を考慮すると、アプリケーションがどれだけのコミュニケーションと情報を「消化」して相互に関連させるべきか想像できます。
プロジェクトが既に開始され、それ自体が非常に安定して動作しているとき、システムの透明性の問題が完全な成長の中で生じました。
これがポイントです。 多くのコンポーネントがあり、それらはすべてデータソースで動作します。 ただし、これらのコンポーネントのほとんどすべては、かつてスタンドアロン製品として記述されていました。 つまり、システム全体の要素としてではなく、販売の個別の決定としてです。 その結果、単一の(システム)APIはなく、それらの間の通信に関する共通の標準もありません。
説明します。 一部のコンポーネントはJSONを送信し、「誰か」はキーを含む行を送信します。値は内部にあり、「誰か」は一般にバイナリを送信し、必要な処理を行います。 しかし、コールセンターの最終アプリケーションでは、すべてを取得して、何らかの方法で処理する必要がありました。 最も重要なことは、データ形式/構造が変更されたことを認識できるリンクがシステムにないことです。 一部のコンポーネントが昨日JSONを送信し、今日はバイナリを送信することに決めた場合、誰もこれを見ることができません。 最終アプリケーションのみが予想どおりにクラッシュし始めます。
コンポーネント間で「統一された通信言語」が存在しないことは深刻な問題につながることがすぐに明らかになりました(私はデザイン段階で問題について話していたので、私の周りではなく)。
最も単純なケースは、クライアントがデータセットの変更を要求した場合です。 たとえば、商品/サービスのデータベースを操作するためのコンポーネントを「保持」する若者にタスクを取り消します。 彼は仕事をし、新しいデータセットを実装します。 しかし、更新の翌日...ああ...コールセンターのアプリケーションは、予期したとおりに動作しなくなります。
あなたはおそらくすでに推測しました。 私たちのヒーローは、データセットだけでなく、彼のコンポーネントがシステムに送信するデータ構造も変更しました。 その結果、コールセンターアプリケーションはこのコンポーネントを使用できなくなり、他の依存関係がチェーンに沿って飛ぶようになります。
彼らは私たちが実際に何をしたいかについて考え始めました。 その結果、潜在的なソリューションについて次の要件を策定しました。
何よりもまず 、データ構造の変更は、システムで直ちに「強調表示」する必要があります。 誰かがどこかで変更を行い、これらの変更がシステムが期待するものと互換性がない場合、変更されたコンポーネントのテスト段階でエラーが発生するはずです。
二番目 。 データ型はコンパイル中だけでなく、実行時もチェックする必要があります。
3番目 。 完全に異なるスキルレベルを持つ多数の人々がコンポーネントで作業するため、記述言語はよりシンプルなはずです。
4番目 。 決定がどうであれ、それを扱うのは可能な限り便利であるべきです。 可能であれば、IDEは可能な限り強調表示する必要があります。
最初の考えは、protobufを実装することでした。 シンプルで読みやすく、簡単。 厳密なデータ入力。 医者が注文したもののようです。 しかし、残念ながら、すべてのprotobuf構文が単純に見えるわけではありません。 さらに、コンパイルされたプロトコルでさえ追加のライブラリを必要としましたが、Javascriptはprotobufによってサポートされておらず、コミュニティ作業の結果でした。 一般的に、彼らは拒否しました。
次に、プロトコルをJSONで記述するというアイデアが浮上しました。 さて、どれほど簡単ですか?
さて、それから私は辞めた。 そして、この投稿は、私の出発後に誰も特に問題に特に対処し始めなかったので、完了できたでしょう。
しかし、コンポーネント間の通信の問題が再びその潜在能力を最大限に発揮したいくつかの個人プロジェクトを考えて、私は自分でアイデアの実装を開始することにしました。 以下で説明します。
それで、私はあなたの注意にceresプロジェクトを提示します。
- プロトコルジェネレーター
- プロバイダー
- クライアント
- トランスポートの実装
プロトコル
タスクは次のようにすることでした:
- システムでメッセージ構造を設定するのは簡単でした。
- すべてのメッセージフィールドのデータ型を判別するのは簡単でした。
- 補助エンティティを定義して参照することができました。
- そしてもちろん、これらすべてがIDEによって強調表示されるように
完全に自然な方法で、純粋なJavascriptではなく、Typescriptがプロトコルの変換先の言語として選択されたと思います。 つまり、プロトコルジェネレーターは、JSONをTypescriptに変換するだけです。
システムで利用可能なメッセージを説明するには、JSONとは何かを知るだけです。 誰も問題はないと確信しています。
Hello Worldの代わりに、私はそれほどハックされていない例を提供します-チャット。
{ "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" }
すべてがとてつもなくシンプルです。 NewMessageイベントとUsersListUpdatedイベントがいくつかあります。 いくつかのUsersListおよびAddUserResultリクエストも同様です。 さらに2つのエンティティがあります。ChatMessageとUserです。
ご覧のとおり、説明は非常に透明で理解しやすいものです。 ルールについて少し。
- JSONのオブジェクトは、生成されたプロトコルのクラスになります
- プロパティ値は、データ型定義またはクラス(エンティティ)への参照です
- 生成されたプロトコルの観点からネストされたオブジェクトは、「ネストされた」クラスになります。つまり、ネストされたオブジェクトは、親のすべてのプロパティを継承します。
あとは、プロトコルを生成して使用を開始するだけです。
npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r
その結果、Typescriptで生成されたプロトコルを取得します。 接続して使用します:
そのため、プロトコルはすでに開発者に何かを提供しています:
- IDEは、プロトコルに含まれるものを強調表示します。 IDEは、予想されるすべてのプロパティも強調表示します。
- データ型に何か問題があるかどうかを確実に伝えるTypescript。 もちろん、これは開発段階で行われますが、プロトコル自体はすでに実行時にデータ型をチェックし、違反が検出された場合は例外をスローします
- 一般に、検証は忘れることができます。 プロトコルは必要なすべてのチェックを行います。
- 生成されたプロトコルは、追加のライブラリを必要としません。 彼が働く必要があるすべては、彼はすでに含まれています。 そして、それは非常に便利です。
はい、生成されたプロトコルのサイズは、控えめに言っても驚くかもしれません。 ただし、生成されたプロトコルファイルが適している縮小を忘れないでください。
メッセージを「パック」して送信できます
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
ここで予約することが重要です。パケットはバイトの配列になります。これは、トラフィック負荷の観点から見て非常に適切で正しいものです。同じJSONの「コスト」を送信すると、もちろん高価になります。 ただし、プロトコルには1つの秘trickがあります。デバッグモードでは、読み取り可能なJSONが生成されるため、開発者はトラフィックを「調べ」て何が起こるかを確認できます。
これは実行時に直接行われます。
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
サーバー(または他の受信者)で、メッセージを簡単に解凍できます。
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 }
このプロトコルは、すべての主要なデータタイプをサポートしています。
種類 | 値 | 説明 | サイズ、バイト |
---|---|---|---|
utf8String | UTF8エンコードされた文字列 | x | |
asciiString | アスキー文字列 | 1文字-1バイト | |
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 | |
ブール値 | 1 |
プロトコル内では、これらのデータ型はプリミティブと呼ばれます。 ただし、プロトコルのもう1つの機能は、独自のデータ型(「追加のデータ型」と呼ばれる)を追加できることです。
たとえば、 ChatMessageにはdatetimeデータ型のフィールドが作成されていることにお気づきでしょう。 アプリケーションレベルでは、このタイプはDateに対応し、プロトコル内ではuint32として保存(および送信)されます。
プロトコルにタイプを追加するのは非常に簡単です。 たとえば、 電子メールのデータ型が必要な場合は、プロトコルの次のメッセージについて言います。
{ "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" }
あなたがする必要があるのは、メールタイプの定義を書くことだけです。
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); }, } };
以上です。 プロトコルを生成することにより、新しい電子メールデータタイプのサポートが得られます 。 間違ったアドレスでエンティティを作成しようとすると、エラーが発生します
const user: Protocol.User = new Protocol.User({ nickname: 'Brad', email: 'not_valid_email' }); console.log(user);
ああ...
Error: Cannot create class of "User" due error(s): - Property "email" has wrong value; validation was failed with value "not_valid_email".
そのため、プロトコルは単に「不正な」データをシステムに許可しません。
新しいデータ型を定義するときに、いくつかの重要なプロパティを指定したことに注意してください。
- binaryType-データの保存、エンコード、デコードに使用されるプリミティブデータ型への参照。 この場合、アドレスがASCII文字列であることを示します。
- tsTypeは、Javascriptタイプへの参照です。つまり、Javascript環境でのデータタイプの表現方法です。 この場合、 文字列について話しています
- また、プロトコルの生成時にのみ新しいデータ型を定義する必要があることにも注意してください。 出力では、新しいデータ型を既に含む生成されたプロトコルを取得します。
ここで、すべてのプロトコル機能に関する詳細情報をceres.protocolで見つけることができます。
プロバイダーと顧客
概して、プロトコル自体を使用して通信を整理できます。 ただし、ブラウザーとnodejsについて話している場合は、プロバイダーとクライアントを使用できます。
お客様
作成
クライアントを作成するには、クライアントとトランスポートが必要です。
設置
# Install consumer (client) npm install ceres.consumer --save # Install transport npm install ceres.consumer.browser.ws --save
作成
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);
クライアントとプロバイダーは、プロトコル専用に設計されています。 つまり、プロトコル(ceres.protocol)でのみ機能します。
イベント
クライアントが作成された後、開発者はイベントをサブスクライブできます
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}`); });
クライアントは、メッセージデータが完全に正しい場合にのみイベントハンドラーを呼び出すことに注意してください。 つまり、アプリケーションは不正なデータから保護され、 NewMessageイベントハンドラーは常にProtocol.Events.NewMessageのインスタンスを引数として呼び出されます。
当然、クライアントはイベントを生成できます。
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}`); });
イベント名はどこにも指定せず、プロトコルからクラスへの参照を使用するか、そのインスタンスを渡すだけです。
タイプ{ [key: string]: string }
単純なオブジェクトを2番目の引数として指定することにより、限られた受信者グループにメッセージを送信することもできます。 ceres内では、このオブジェクトはqueryと呼ばれます 。
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}`); });
したがって、 { location: "UK" }
追加で指定することにより、自分の位置がUKであると特定した顧客のみがこのメッセージを受信するようになります。
クライアント自体を特定のクエリに関連付けるには、 refメソッドを呼び出すだけです。
consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); });
クライアントをqueryに接続した後、彼は「個人」または「グループ」メッセージを受信する機会があります。
お問い合わせ
リクエストもできます
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}`); });
2番目の引数として期待される結果( Protocol.Responses.UsersList )を指定することに注意する価値があります。つまり、応答がUsersListのインスタンスである場合にのみ、要求が正常に完了します。つかまえる 繰り返しますが、これにより、誤ったデータを処理することがなくなります。
クライアント自身も、リクエストを処理できる人と話すことができます。 これを行うには、リクエストの「責任者」として自分自身を「識別する」だけです。
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"`); });
オプションで、3番目の引数として、クライアントの識別に使用できるクエリオブジェクトを指定できることに注意してください。 したがって、誰かがquery 、例えば{ location: "RU" }
でクエリを送信した場合、彼のクエリ{ location: "UK" }
ため、クライアントはそのようなリクエストを受信しません。
クエリには、無制限の数のプロパティを含めることができます。 たとえば、次を指定できます
{ location: "UK", type: "managers" }
次に、完全なクエリ一致に加えて、次のクエリも正常に処理します。
{ location: "UK" }
または
{ type: "managers" }
プロバイダー
作成
プロバイダーを作成する(およびクライアントを作成する)には、プロバイダーとトランスポートが必要です。
設置
# Install provider npm install ceres.provider --save # Install transport npm install ceres.provider.node.ws --save
作成
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);
プロバイダーが作成された瞬間から、クライアントからの接続を受け入れることができます。
イベント
クライアントだけでなく、プロバイダーはメッセージを「リッスン」してメッセージを生成できます。
聞く
// Subscribe to event provider.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); });
生成する
provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' }));
お問い合わせ
当然、プロバイダーはリクエストを「リッスン」できます(する必要があります)
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"`); });
クライアントとの違いは1つだけです。プロバイダーは、リクエスト本文に加えて、接続されているすべてのクライアントに自動的に割り当てられる一意のclientIdを受け取ります。
例
実際、ドキュメンテーションからの抜粋で退屈させたくはありません。短いコードを見る方が簡単で面白いと思います。
ソースをダウンロードしていくつかの簡単な手順を実行するだけで、チャットの例を簡単にインストールできます。
クライアントのインストールと起動
cd chat/client npm install npm start
クライアントはhttp:// localhost:3000で利用可能になります。 クライアントでいくつかのタブをすぐに開いて、「通信」を確認します。
プロバイダー(サーバー)のインストールと起動
cd chat/server npm install ts-node ./server.ts
ts-nodeパッケージに精通しているはずですが、そうでない場合は、TSファイルを実行できます。 インストールしない場合は、サーバーをコンパイルしてからJSファイルを実行します。
cd chat/server npm run build node ./build/server/server.js
なに? 再び?!
なぜ地獄が別の自転車を発明するのかについての質問を予想しています。プロトブフからBMWのハードコアジョイナーに至るまでの非常に多くのソリューションがすでに解決されているため、私はそれが私にとって興味深いとしか言えません。 プロジェクト全体は、彼の仕事からの余暇に、サポートなしで個人的なイニシアチブのみで行われました。
それがあなたのフィードバックが私にとって特に価値がある理由です。 どういうわけかあなたをやる気にさせるために、私はgithubのすべての星について、私はハムスターをstrokeでることを約束することができます(私は控えめに言って嫌いです)。 フォークについては、うーん、私は彼のプッシコをスクラッチします... brrrr。
ハムスターは私のものではなく、息子のハムスターです。
さらに、数週間のうちに、プロジェクトは私の以前の同僚(投稿の冒頭で述べたが、アルファ版が何であるかに興味を持っている人)にテストを行う予定です。 目標は、複数のコンポーネントでデバッグおよび実行することです。 うまくいくことを本当に願っています。
リンクとパッケージ
プロジェクトは2つのリポジトリに収容されています。
- ceresのソースコード:ceres.provider、ceres.consumer、および現在利用可能なすべてのトランスポート。
- ceres.protocolプロトコルジェネレーターのソース
NPM次のパッケージが利用可能
- ceres.protocolプロトコルジェネレーター
- ceres.providerプロバイダー
- ceres.consumerの顧客
- 長いポーリングに基づくプロバイダーのceres.provider.node.longpollトランスポート
- Web Socketに基づくプロバイダーのceres.provider.node.wsトランスポート
- ロングポーリングに基づくクライアントのceres.consumer.browser.longpollトランスポート
- Web Socketに基づくクライアントのceres.consumer.browser.wsトランスポート
良くて軽い。