В одном из наших проектов, мы использовали IPC (inter-process communication) на сокетах. Довольно большой проект, торгового бота, где были множество модулей которые взаимодействовали друг с другом. По мере роста сложности стал вопрос о мониторинге, что происходит в микросервисах. Мы решили создать свое приложение для отслеживания, потока данных на всего двух библиотеках react и redoor. Я хотел бы поделиться с вами нашим подходом.
Микросервисы обмениваются между собой JSON объектами, с двумя полями: имя и данные. Имя - это идентификатор какому сервису предназначается объект и поле данные - полезная нагрузка. Пример:
{ name:'ticket_delete', data:{id:1} }
Поскольку сервис довольно сырой и протоколы менялись каждую неделю, так что мониторинг должен быть максимально простым и модульным. Соответственно, в приложении каждый модуль должен отображать предназначаемые ему данные и так добавляя, удаляя данные мы должны получить набор независимых модулей для мониторинга процессов в микросервисах.
И так начнем.Для примера сделаем простейшее приложение и веб сервер. Приложение будет состоять из трех модулей. На картинке они обозначены пунктирными линиями. Таймер, статистика и кнопки управления статистикой.
Создадим простой Web Socket сервер.
/** src/ws_server/echo_server.js */const WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8888 });function sendToAll( data) { let str = JSON.stringify(data); wss.clients.forEach(function each(client) { client.send(str); });}// Отправляем данные каждую секундуsetInterval(e=>{ let d = new Date(); let H = d.getHours(); let m = ('0'+d.getMinutes()).substr(-2); let s = ('0'+d.getSeconds()).substr(-2); let time_str = `${H}:${m}:${s}`; sendToAll({name:'timer', data:{time_str}});},1000);
Сервер каждую секунду формирует строку с датой и отправляет всем подключившимся клиентам. Открываем консоль и запускаем сервер:
node src/ws_server/echo_server.js
Теперь перейдем к проекту приложения. Для сборки и отладки будем использовать rollup конфигурация ниже.
rollup.config.js
import serve from 'rollup-plugin-serve';import babel from '@rollup/plugin-babel';import { nodeResolve } from '@rollup/plugin-node-resolve';import commonjs from '@rollup/plugin-commonjs';import hmr from 'rollup-plugin-hot'import postcss from 'rollup-plugin-postcss';import autoprefixer from 'autoprefixer'import replace from '@rollup/plugin-replace';const browsers = [ "last 2 years", "> 0.1%", "not dead"]let is_production = process.env.BUILD === 'production';const replace_cfg = { 'process.env.NODE_ENV': JSON.stringify( is_production ? 'production' : 'development' ), preventAssignment:false,}const babel_cfg = { babelrc: false, presets: [ [ "@babel/preset-env", { targets: { browsers: browsers }, } ], "@babel/preset-react" ], exclude: 'node_modules/**', plugins: [ "@babel/plugin-proposal-class-properties", ["@babel/plugin-transform-runtime", { "regenerator": true }], [ "transform-react-jsx" ] ], babelHelpers: 'runtime'}const cfg = { input: [ 'src/main.js', ], output: { dir:'dist', format: 'iife', sourcemap: true, exports: 'named', }, inlineDynamicImports: true, plugins: [ replace(replace_cfg), babel(babel_cfg), postcss({ plugins: [ autoprefixer({ overrideBrowserslist: browsers }), ] }), commonjs({ sourceMap: true, }), nodeResolve({ browser: true, jsnext: true, module: false, }), serve({ open: false, host: 'localhost', port: 3000, }), ],} ;export default cfg;
Точка входа нашего проекта main.js
создадим
его.
/** src/main.js */import React, { createElement, Component, createContext } from 'react';import ReactDOM from 'react-dom';import {Connect, Provider} from './store'import Timer from './Timer/Timer'const Main = () => ( <Provider> <h1>ws stats</h1> <Timer/> </Provider>);const root = document.body.appendChild(document.createElement("DIV"));ReactDOM.render(<Main />, root);
Теперь создадим стор для нашего проекта
/** src/store.js */import React, { createElement, Component, createContext } from 'react';import createStoreFactory from 'redoor';import * as actionsWS from './actionsWS'import * as actionsTimer from './Timer/actionsTimer'const createStore = createStoreFactory({Component, createContext, createElement});const { Provider, Connect } = createStore( [ actionsWS, // websocket actions actionsTimer, // Timer actions ]);export { Provider, Connect };
Прежде чем создавать модуль таймера нам надо получать данные от сервера. Создадим акшнес файл для работы с сокетом.
/** src/actionsWS.js */export const __module_name = 'actionsWS'let __emit;// получаем функцию emit от redoorexport const bindStateMethods = (getState, setState, emit) => { __emit = emit};// подключаемся к серверуlet wss = new WebSocket('ws://localhost:8888')// получаем все сообщения от сервера и отправляем их в поток redoorwss.onmessage = (msg) => { let d = JSON.parse(msg.data); __emit(d.name, d.data);}
Здесь надо остановиться поподробнее. Наши сервисы отправляют данные в виде объекта с полями: имя и данные. В библиотеке redoor можно так же создавать потоки событий в которые мы просто передаем данные и имя. Выглядит это примерно так:
+------+ | emit | --- events --+--------------+----- ... ------+------------->+------+ | | | v v v +----------+ +----------+ +----------+ | actions1 | | actions2 | ... | actionsN | +----------+ +----------+ +----------+
Таким образом каждый модуль имеет возможность "слушать" свои события и по надобности и чужие тоже.
Теперь создадим собственно сам модуль таймера. В папке
Timer
создадим два файла Timer.js
и
actionsTimer.js
/** src/Timer/Timer.js */import React from 'react';import {Connect} from '../store'import s from './Timer.module.css'const Timer = ({timer_str}) => <div className={s.root}> {timer_str}</div>export default Connect(Timer);
Здесь все просто, таймер берет из глобального стейта
timer_str
который обновляется в
actionsTimer.js
. Функция Connect
подключает модуль к redoor.
/** src/Timer/actionsTimer.js */export const __module_name = 'actionsTimer'let __setState;// получаем метод для обновления стейтаexport const bindStateMethods = (getState, setState) => { __setState = setState;};// инициализируем переменную таймераexport const initState = { timer_str:''}// "слушаем" поток событий нам нужен "timer"export const listen = (name,data) =>{ name === 'timer' && updateTimer(data);}// обновляем стейт function updateTimer(data) { __setState({timer_str:data.time_str})}
В акшес файле, мы "слушаем" событие timer
таймера
(функция listen
) и как только оно будет получено
обновляем стейт и выводим строку с данными.
Подробнее о функциях redoor:
__module_name
- зарезервированная переменная нужна
просто для отладки она сообщает в какой модуль входят акшенсы.
bindStateMethods
- функция для получения
setState
, поскольку данные приходят асинхронно нам
надо получить в локальных переменных функцию обновления стейта.
initState
- функция или объект инициализации данных
модуля в нашем случае это timer_str
listen
- функция в которую приходят все события
сгенерированные redoor.
Готово. Запускаем компиляцию и открываем браузер по адресу
http://localhost:3000
npx rollup -c rollup.config.js --watch
Должны появиться часики с временем. Перейдём к более сложному.
По аналогии с таймером добавим еще модуль статистики. Для начала
добавим новый генератор данных в echo_server.js
/** src/ws_server/echo_server.js */...let g_interval = 1;// Данные статистикиsetInterval(e=>{ let stats_array = []; for(let i=0;i<30;i++) { stats_array.push((Math.random()*(i*g_interval))|0); } let data = { stats_array } sendToAll({name:'stats', data});},500);...
И добавим модуль в проект. Для этого создадим папку
Stats
в которой создадим Stats.js
и
actionsStats.js
/** src/Stats/Stats.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const Bar = ({h})=><div className={s.bar} style={{height:`${h}`px}}> {h}</div>const Stats = ({stats_array})=><div className={s.root}> <div className={s.bars}> {stats_array.map((it,v)=><Bar key={v} h={it} />)} </div></div>export default Connect(Stats);
/** src/Stats/actionsStats.js */export const __module_name = 'actionsStats'let __setState = null;export const bindStateMethods = (getState, setState, emit) => { __setState = setState;}export const initState = { stats_array:[],}export const listen = (name,data) =>{ name === 'stats' && updateStats(data);}function updateStats(data) { __setState({ stats_array:data.stats_array, })}
и подключаем новый модуль к стору
/** src/store.js */...import * as actionsStats from './Stats/actionsStats'const { Provider, Connect } = createStore( [ actionsWS, actionsTimer, actionsStats //<-- модуль Stats ]);...
В итоге мы должны получить это:
Как видите модуль Stats
принципиально не отличается
от модуля Timer
, только отображение не строки, а
массива данных. Что если мы хотим не только получать данные, но и
отправлять их на сервер? Добавим управление статистикой.
В нашем примере переменная g_interval это угловой коэффициент наклона нормировки случайной величины. Попробуем ей управлять с нашего приложения.
Добавим пару кнопок к графику статистики. Плюс будет увеличвать
значение interval
минус уменьшать.
/** src/Stats/Stats.js */...import Buttons from './Buttons' // импортируем модуль...const Stats = ({cxRun, stats_array})=><div className={s.root}> <div className={s.bars}> {stats_array.map((it,v)=><Bar key={v} h={it} />)} </div> <Buttons/> {/*Модуль кнопочки*/}</div>...
И сам модуль с кнопочками
/** src/Stats/Buttons.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const DATA_INTERVAL_PLUS = { name:'change_interval', interval:1}const DATA_INTERVAL_MINUS = { name:'change_interval', interval:-1}const Buttons = ({cxEmit, interval})=><div className={s.root}> <div className={s.btns}> <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_PLUS)}> plus </button> <div className={s.len}>interval:{interval}</div> <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_MINUS)}> minus </button> </div></div>export default Connect(Buttons);
Получаем панель с кнопочками:
И модифицируем actionsWS.js
/** src/actionsWS.js */...let wss = new WebSocket('ws://localhost:8888')wss.onmessage = (msg) => { let d = JSON.parse(msg.data); __emit(d.name, d.data);}// "слушаем" событие отправить данные на серверexport const listen = (name,data) => { name === 'ws_send' && sendMsg(data);}// отправляем данныеfunction sendMsg(msg) { wss.send(JSON.stringify(msg))}
Здесь мы в модуле Buttons.js
воспользовались
встроенной функции (cxEmit
) создания события в
библиотеке redoor. Событие ws_send
"слушает" модуль
actionsWS.js
. Полезная нагрузка data
-
это два объекта: DATA_INTERVAL_PLUS
и
DATA_INTERVAL_MINUS
. Таким образам если нажать кнопку
плюс на сервер будет отправлен объект {
name:'change_interval', interval:1 }
На сервере добавляем
/** src/ws_server/echo_server.js */...wss.on('connection', function onConnect(ws) { // "слушаем" приложение на событие "change_interval" // от модуля Buttons.js ws.on('message', function incoming(data) { let d = JSON.parse(data); d.name === 'change_interval' && change_interval(d); });});let g_interval = 1;// меняем интервалfunction change_interval(data) { g_interval += data.interval; // создаем событие, что интервал изменен sendToAll({name:'interval_changed', data:{interval:g_interval}});}...
И последний штрих необходимо отразить изменение интервала в
модуле Buttons.js. Для этого в actionsStats.js начнём слушать
событие "interval_changed
" и обновлять переменную
interval
/** src/Stats/actionsStats.js */...export const initState = { stats_array:[], interval:1 // добавляем переменную интервал}export const listen = (name,data) =>{ name === 'stats' && updateStats(data); // "слушаем" событие обновления интервала name === 'interval_changed' && updateInterval(data);}// обнавляем интервалfunction updateInterval(data) { __setState({ interval:data.interval, })}function updateStats(data) { __setState({ stats_array:data.stats_array, })}
Итак, мы получили три независимых модуля, где каждый модуль
следит только за своим событием и отображает только его. Что
довольно удобно когда еще не ясна до конца структура и протоколы на
этапе прототипирования. Надо только добавить, что поскольку все
события имеют сквозную структуру то надо четко придерживаться
шаблона создания события мы для себя выбрали такую: (MODULEN
AME)_(FUNCTION NAME)_(VAR NAME)
.
Надеюсь было полезно. Исходные коды проекта, как обычно, на гитхабе.