Русский
Русский
English
Статистика
Реклама

ReactRedoor IPC мониторинг

В одном из наших проектов, мы использовали 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).

Надеюсь было полезно. Исходные коды проекта, как обычно, на гитхабе.

Источник: habr.com
К списку статей
Опубликовано: 12.05.2021 18:04:50
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Node.js

Reactjs

Ipc

React

Webscoket

Nodejs

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru