Semacam Kekacauan

Seperti yang diperlihatkan oleh praktik, sebagian besar masalah muncul bukan karena solusi itu sendiri, tetapi karena bagaimana komunikasi antara komponen-komponen sistem terjadi. Jika ada kekacauan dalam komunikasi antara komponen-komponen sistem, maka, karena Anda tidak mencoba menulis masing-masing komponen dengan baik, sistem secara keseluruhan akan gagal.


Perhatian Di dalam sepeda.


Masalah atau pernyataan masalah


Beberapa waktu yang lalu, kebetulan bekerja pada sebuah proyek untuk sebuah perusahaan yang membawa ke massa seperti CRM, sistem ERM dan turunannya. Selain itu, perusahaan mengeluarkan produk yang agak komprehensif dari perangkat lunak untuk register kas ke call-center dengan kemungkinan menyewa operator dalam jumlah hingga 200 jiwa.


Saya sendiri mengerjakan aplikasi front-end untuk call-center.


Mudah dibayangkan bahwa informasi dari semua komponen sistem yang mengalir ke aplikasi operator. Dan jika kita memperhitungkan fakta bahwa itu bukan operator tunggal, tetapi juga manajer dan administrator, maka Anda dapat membayangkan berapa banyak komunikasi dan informasi yang harus "dicerna" aplikasi dan saling berhubungan satu sama lain.


Ketika proyek sudah diluncurkan dan bahkan bekerja cukup stabil untuk dirinya sendiri, masalah transparansi sistem muncul dalam semua pertumbuhannya.


Ini intinya. Ada banyak komponen dan mereka semua bekerja dengan sumber data mereka. Tetapi hampir semua komponen ini pernah ditulis sebagai produk mandiri. Artinya, bukan sebagai elemen dari sistem keseluruhan, tetapi sebagai keputusan terpisah untuk dijual. Akibatnya, tidak ada API (sistem) tunggal dan tidak ada standar komunikasi umum di antara mereka.


Saya akan jelaskan. Beberapa komponen mengirim JSON, “seseorang” mengirim baris dengan kunci: nilai di dalam, “seseorang” mengirim biner secara umum dan melakukan apa pun yang Anda inginkan dengannya. Tapi, dan aplikasi terakhir untuk call-center harus mendapatkan semuanya dan memprosesnya entah bagaimana. Yah dan yang paling penting, tidak ada tautan dalam sistem yang dapat mengenali bahwa format / struktur data telah berubah. Jika beberapa komponen mengirim JSON kemarin, dan hari ini memutuskan untuk mengirim biner - tidak ada yang akan melihat ini. Hanya aplikasi terakhir yang akan mulai macet seperti yang diharapkan.


Segera menjadi jelas (bagi orang-orang di sekitar saya, bukan untuk saya, karena saya berbicara tentang masalah pada tahap desain) bahwa tidak adanya "bahasa komunikasi terpadu" antara komponen menyebabkan masalah serius.


Kasus paling sederhana adalah ketika klien diminta untuk mengubah beberapa dataset. Mereka menulis tugas kepada pemuda yang “memegang” komponen untuk bekerja dengan basis data barang / jasa, misalnya. Dia melakukan pekerjaannya, mengimplementasikan dataset baru, dan baginya, brengsek, semuanya bekerja. Tapi, sehari setelah pembaruan ... oh ... aplikasi di pusat panggilan tiba-tiba mulai berfungsi tidak seperti yang mereka harapkan darinya.


Anda mungkin sudah menebak. Pahlawan kami tidak hanya mengubah dataset, tetapi juga struktur data yang dikirim komponennya ke sistem. Akibatnya, aplikasi call-center sama sekali tidak dapat bekerja dengan komponen ini, dan ada dependensi lain terbang di sepanjang rantai.


Mereka mulai berpikir tentang apa yang sebenarnya ingin kami dapatkan di jalan keluar. Sebagai hasilnya, kami merumuskan persyaratan berikut untuk solusi potensial:


Pertama dan terpenting: setiap perubahan dalam struktur data harus segera "disorot" dalam sistem. Jika seseorang telah membuat perubahan di suatu tempat dan perubahan ini tidak sesuai dengan apa yang diharapkan sistem, kesalahan harus terjadi pada tahap pengujian komponen, yang telah diubah.


Yang kedua . Tipe data harus diperiksa tidak hanya selama kompilasi, tetapi juga saat dijalankan.


Yang ketiga . Karena sejumlah besar orang dengan tingkat keterampilan yang sangat berbeda bekerja pada komponen, bahasa deskripsi harus lebih sederhana.


Keempat . Apa pun solusinya, harus senyaman mungkin untuk bekerja dengannya. Jika memungkinkan, IDE harus menyoroti sebanyak mungkin.


Pikiran pertama adalah menerapkan protobuf. Sederhana, mudah dibaca dan mudah. Pengetikan data yang ketat. Sepertinya itu yang diperintahkan dokter. Namun sayang, tidak semua sintaksis protobuf tampak sederhana. Selain itu, bahkan protokol yang dikompilasi memerlukan perpustakaan tambahan, tetapi Javascript tidak didukung oleh protobuf dan merupakan hasil kerja komunitas. Secara umum, mereka menolak.


Kemudian muncul ide untuk menggambarkan protokol di JSON. Nah, seberapa mudah?


Baiklah, lalu saya berhenti. Dan dalam hal ini pos ini bisa diselesaikan, karena setelah kepergian saya tidak ada orang lain yang mulai berurusan dengan masalah tersebut.


Namun, mengingat beberapa proyek pribadi di mana masalah komunikasi antar komponen kembali ke potensi penuh, saya memutuskan untuk mulai menerapkan ide sendiri. Apa yang akan dibahas di bawah ini.


Jadi, saya sajikan kepada Anda proyek ceres , yang meliputi:


  • generator protokol
  • penyedia
  • klien
  • implementasi transportasi

Protokol


Tugasnya adalah membuatnya:


  • mudah untuk mengatur struktur pesan dalam sistem.
  • mudah untuk menentukan tipe data dari semua bidang pesan.
  • adalah mungkin untuk mendefinisikan entitas bantu dan merujuknya.
  • dan tentu saja, agar semua ini disorot oleh IDE

Saya berpikir bahwa dengan cara yang sepenuhnya alami, sebagai bahasa di mana protokol dikonversi, Typecript dipilih bukan Javascript murni. Yaitu, yang dilakukan oleh generator protokol adalah mengubah JSON menjadi Typefon.


Untuk menggambarkan pesan yang tersedia di sistem, Anda hanya perlu tahu apa itu JSON. Dengan itu, saya yakin tidak ada yang punya masalah.


Alih-alih Hello World, saya menawarkan contoh yang tidak kurang basi - obrolan.


{ "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" } 

Semuanya sangat sederhana. Kami memiliki beberapa acara NewMessage dan UsersListUpdated; serta beberapa permintaan UsersList dan AddUserResult. Ada dua entitas lagi: ChatMessage dan Pengguna.


Seperti yang Anda lihat, uraiannya cukup transparan dan dapat dimengerti. Sedikit tentang aturannya.


  • Objek di JSON akan menjadi kelas dalam protokol yang dibuat
  • Nilai properti adalah definisi tipe data atau referensi ke kelas (entitas)
  • Objek bersarang dari sudut pandang protokol yang dihasilkan akan menjadi kelas "bersarang", yaitu, objek bersarang akan mewarisi semua properti orang tua mereka.

Sekarang yang perlu Anda lakukan adalah membuat protokol untuk mulai menggunakannya.


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

Sebagai hasilnya, kita mendapatkan protokol yang dihasilkan oleh Scripteks. Kami terhubung dan menggunakan:


gambar

Jadi, protokol sudah memberikan sesuatu kepada pengembang:


  • IDE menyoroti apa yang kita miliki dalam protokol. IDE juga menyoroti semua properti yang diharapkan.
  • Naskah, yang pasti akan memberi tahu kami jika ada sesuatu yang salah dengan tipe data. Tentu saja, ini dilakukan pada tahap pengembangan, tetapi protokol itu sendiri sudah akan menjalankan-waktu memeriksa tipe data dan melemparkan pengecualian jika pelanggaran terdeteksi
  • Secara umum, Anda bisa melupakan validasi. Protokol akan melakukan semua pemeriksaan yang diperlukan.
  • Protokol yang dihasilkan tidak memerlukan pustaka tambahan. Semua yang dia butuhkan untuk bekerja, dia sudah mengandung. Dan itu sangat nyaman.

Ya, ukuran protokol yang dihasilkan mungkin mengejutkan Anda, untuk sedikitnya. Tapi, jangan lupa tentang minifikasi, yang file protokol yang dihasilkan cocok dengan baik.

Sekarang kita dapat "mengepak" pesan dan mengirim


 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 

Penting untuk melakukan reservasi di sini, paket akan menjadi array byte, yang sangat baik dan benar dari sudut pandang beban lalu lintas, karena mengirim JSON "biaya" yang sama, tentu saja, lebih mahal. Namun, protokol memiliki satu fitur - dalam mode debug akan menghasilkan JSON yang dapat dibaca sehingga pengembang dapat "melihat" lalu lintas dan melihat apa yang terjadi.


Ini dilakukan langsung saat run-time.


 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 

Di server (atau penerima lainnya), kami dapat dengan mudah membongkar pesan:


 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 } 

Protokol mendukung semua tipe data utama:


JenisNilai-nilaiDeskripsiUkuran, byte
utf8StringString yang dikodekan UTF8x
asciiStringstring ascii1 karakter - 1 byte
int8-128 hingga 1271
int16-32768 hingga 327672
int32-2147483648 hingga 21474836474
uint80 hingga 2551
uint160 hingga 655352
uint320 hingga 42949672954
float321.2x10 -38 hingga 3.4x10 384
float645.0x10 -324 hingga 1.8x10 3088
boolean1

Dalam protokol, tipe data ini disebut primitif. Namun, fitur lain dari protokol ini adalah Anda dapat menambahkan tipe data Anda sendiri (disebut "tipe data tambahan").


Misalnya, Anda mungkin sudah memperhatikan bahwa ChatMessage memiliki bidang yang dibuat dengan tipe data datetime . Pada tingkat aplikasi - jenis ini sesuai dengan Tanggal , dan di dalam protokol disimpan (dan dikirim) sebagai uint32 .


Menambahkan tipe Anda ke protokol cukup sederhana. Misalnya, jika kita ingin memiliki tipe data email , katakan untuk pesan berikut dalam protokol:


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

Yang perlu Anda lakukan adalah menulis definisi untuk jenis email.


 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); }, } }; 

Itu saja. Dengan membuat protokol, kami mendapatkan dukungan untuk tipe data email baru. Saat kami mencoba membuat entitas dengan alamat yang salah, kami mendapatkan kesalahan


 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". 

Jadi, protokol tidak memungkinkan data "buruk" masuk ke sistem.


Harap perhatikan, saat mendefinisikan tipe data baru, kami menetapkan beberapa properti utama:


  • binaryType - referensi ke tipe data primitif yang harus digunakan untuk menyimpan, menyandikan / mendekode data. Dalam kasus ini, kami menunjukkan bahwa alamat tersebut adalah string ascii.
  • tsType adalah referensi ke tipe Javascript, yaitu, bagaimana tipe data harus diwakili dalam lingkungan Javascript. Dalam hal ini kita berbicara tentang string
  • Perlu juga dicatat bahwa kita perlu mendefinisikan tipe data baru hanya pada saat menghasilkan protokol. Pada output, kami mendapatkan protokol yang dihasilkan yang sudah berisi tipe data baru.

Anda dapat melihat informasi terperinci tentang semua fitur protokol di sini ceres.protocol .

Penyedia dan pelanggan


Pada umumnya, protokol itu sendiri dapat digunakan untuk mengatur komunikasi. Namun, jika kita berbicara tentang browser dan nodejs, maka penyedia dan klien tersedia.


Pelanggan


Ciptaan


Untuk membuat klien, Anda perlu klien dan transportasi.


Instalasi


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

Ciptaan


 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); 

Klien, serta penyedia, dirancang khusus untuk protokol. Artinya, mereka hanya akan bekerja dengan protokol (ceres.protocol).

Acara


Setelah klien dibuat, pengembang dapat berlangganan acara


 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}`); }); 

Harap dicatat bahwa klien akan memanggil event handler hanya jika data pesan sepenuhnya benar. Dengan kata lain, aplikasi kita dilindungi terhadap data yang tidak benar dan pengendali event NewMessage akan selalu dipanggil dengan instance dari Protokol . Acara.NewMessage sebagai argumen.


Secara alami, klien dapat menghasilkan acara.


 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}`); }); 

Perhatikan bahwa kami tidak menentukan nama acara di mana pun, kami cukup menggunakan tautan ke kelas dari protokol, atau meneruskan turunannya.


Kami juga dapat mengirim pesan ke grup penerima terbatas dengan menetapkan objek tipe sederhana { [key: string]: string } sebagai argumen kedua. Di dalam ceres, objek ini disebut kueri .


 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}`); }); 

Dengan demikian, dengan tambahan menunjukkan { location: "UK" } , kami dapat memastikan bahwa hanya pelanggan yang telah mengidentifikasi posisi mereka sebagai Inggris akan menerima pesan ini.


Untuk mengaitkan klien itu sendiri dengan permintaan tertentu, Anda hanya perlu memanggil metode referensi:


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

Setelah kami menghubungkan klien dengan permintaan , ia memiliki kesempatan untuk menerima pesan "pribadi" atau "grup".


Pertanyaan


Kami juga dapat membuat permintaan


 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}`); }); 

Patut diperhatikan bahwa sebagai argumen kedua kami menentukan hasil yang diharapkan ( Protocol.Responses.UsersList ), yang berarti bahwa permintaan kami akan berhasil diselesaikan hanya jika responsnya adalah instance dari UsersList , dalam semua kasus lain kami akan "jatuh" ke menangkap Sekali lagi, ini memastikan kita dari memproses data yang salah.


Klien sendiri juga dapat berbicara dengan mereka yang dapat memproses permintaan. Untuk melakukan ini, Anda hanya perlu "mengidentifikasi" diri Anda sebagai "bertanggung jawab" untuk permintaan tersebut.


 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"`); }); 

Catatan, secara opsional, sebagai argumen ketiga, kita dapat menentukan objek permintaan yang dapat digunakan untuk mengidentifikasi klien. Dengan demikian, jika seseorang mengirim kueri dengan kueri , katakanlah, { location: "RU" } , maka klien kami tidak akan menerima permintaan seperti itu, karena kueri { location: "UK" } .


Kueri dapat menyertakan jumlah properti yang tidak terbatas. Misalnya, Anda dapat menentukan yang berikut ini


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

Kemudian, selain pencocokan kueri lengkap, kami juga akan berhasil memproses kueri berikut:


 { location: "UK" } 

atau


 { type: "managers" } 

Penyedia


Ciptaan


Untuk membuat penyedia (juga untuk membuat klien), Anda memerlukan penyedia dan transportasi.


Instalasi


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

Ciptaan


 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); 

Dari saat penyedia dibuat, ia dapat menerima koneksi dari klien.


Acara


Seperti halnya klien, penyedia dapat "mendengarkan" pesan dan menghasilkannya.


Mendengarkan


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

Hasilkan


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

Pertanyaan


Secara alami, penyedia dapat (dan harus) "mendengarkan" permintaan


 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"`); }); 

Hanya ada satu perbedaan dari klien, penyedia selain badan permintaan akan menerima clientId unik, yang ditugaskan secara otomatis ke semua klien yang terhubung.


Contoh


Bahkan, saya benar-benar tidak ingin membuat Anda bosan dengan kutipan dari dokumentasi, saya yakin akan lebih mudah dan lebih menarik bagi Anda untuk hanya melihat sepotong kode pendek.


Anda dapat dengan mudah menginstal contoh obrolan dengan mengunduh sumber dan melakukan beberapa tindakan sederhana


Instalasi dan peluncuran klien


 cd chat/client npm install npm start 

Klien akan tersedia di http: // localhost: 3000 . Buka segera beberapa tab dengan klien untuk melihat "komunikasi".


Instalasi dan peluncuran penyedia (server)


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

Saya yakin Anda terbiasa dengan paket ts-node , tetapi jika tidak, ini memungkinkan Anda untuk menjalankan file TS. Jika Anda tidak ingin menginstal, cukup kompilasi server, dan kemudian jalankan file JS.


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

Apa? Lagi ?!


Mengantisipasi pertanyaan tentang mengapa harus menciptakan sepeda lain, karena ada begitu banyak solusi yang telah dikerjakan, dari protobuf hingga hardcore joynr dari BMW, saya hanya dapat mengatakan bahwa itu menarik bagi saya. Seluruh proyek dilakukan semata-mata atas inisiatif pribadi tanpa dukungan, di waktu luang saya dari pekerjaan.


Itulah sebabnya umpan balik Anda sangat berharga bagi saya. Dalam upaya memotivasi Anda, saya bisa berjanji bahwa untuk setiap bintang di github, saya akan membelai hamster (yang, secara halus, saya tidak suka). Untuk garpu, uhhh, aku akan menggaruk pussiko-nya ... brrrr.


Hamster bukan milikku, hamster putra .


Selain itu, dalam beberapa minggu proyek ini akan diuji untuk mantan kolega saya (yang saya sebutkan di awal posting dan yang tertarik pada apa versi alfa itu). Tujuannya adalah debugging dan berjalan pada banyak komponen. Saya sangat berharap ini berhasil.


Tautan dan paket


Proyek ini diselenggarakan oleh dua repositori


  • sumber ceres : ceres.provider, ceres.consumer dan semua transportasi yang tersedia saat ini.
  • sumber generator protokol ceres.protocol

Paket-paket NPM berikut tersedia



Bagus dan ringan.

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


All Articles