-
Notifications
You must be signed in to change notification settings - Fork 8
/
arch.txt
executable file
·165 lines (121 loc) · 28.2 KB
/
arch.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
АРХИТЕКТУРА ILAB3.0
(введение + обсуждение)
Микросервисная архитектура
Основная идея ILAB3 - микросервисная архитектура сервера, когда для решения задачи она разбивается не просто на модули в рамках единого приложения, а на несколько отдельных процессов, которые как-то между собой взаимодействуют. Удобство - такой архитектуры в лёгкой расширяемости (модули можно подключать и отключать на лету) и большей безопасности (если падает с ошибкой один процесс, другие остаются жить). В случае ILAB речь идёт о node-процессах (т.е. отдельных экземпляров node исполняющих разные скрипты), взаимодействующих (в основном) по json-prc протоколу.
Сокеты
Для того чтобы с процессом можно было общаться, в нём делается сокет-сервер (имееются ввиду т.н. беркли-сокеты по которым просто передаются байты данных, вроде как по com-порту; не путать с вебсокетами). Это значит что такой сервер начинает слушать некий порт, к которому могут подключаться внешние клиенты. После подключения между ними устанавливается канал связи, по которому передаются какие-то данные. В нашем случае по этому каналу в обе стороны передаются json-структуры, для чего используется класс JsonSocket.
Класс JsonSocket
Вся работа с сокетами скрыта в классе JsonSocket (описан в файле jsonsocket.js). По сути этот класс - обёртка над сокетом, для передачи по нему не байтов, а сразу json-обьектов и мы бы могли подписаться на событие не просто прихода байтов, а на событие "json", означающее "нам прислали новый json-обьект".
Для того чтобы подключиться к сокет-серверу пишем
var socket = new JsonSocket(7000, '127.0.0.1');
что означает что мы хотим подключиться к 7000-му порту на машине c ip=127.0.0.1
Чтобы послать такому серверу обьект jobj пишем что-то вроде
socket.write(jobj); // так отправляем
Чтобы подписаться на приход данных от сервера пишем:
socket.on('json', function (json) { console.log(json); } ) ; // так слушаем.
Чтобы самому создать сервер, слушающий jsonsocket
server = net.createServer // создаём обычный сервер-сокет (апишный метод node.js)
и в реакции на его событие 'connection' (подключение нового клиента) создавать
jsocket = new JsonSocket(socket)
теперь через jsocket можно отправлять данные клиенту по write и слушать данные от него подписываясь на событие "json".
На самом деле нам не обязательно знать как как он устроен, потому что их использование спрятано внутри кода сервисов и проксей. Это так, для понимания общих принципов. Например, чтобы понимать, что так можно передавать только обьекты, сериализуемые в json.
Процесс управляемый по json-rpc
Если наш процесс создал jsonsocket-сервер, он может слушать от внешних клиентов какие-то сообщения, уведомления, команды. Основная идея json-rpc - это Remote Procedure Call, т.е способ сказать серверу "выполни ка у себя процедуру Beep(20,5)" послав ему json-обьект вида { type:"method", name:"beep", args: [20,5] }. Если мы хотим ещё и получить какой-нибудь ответ от сервера, к сообщению нужно добавить уникальный id, а сервер после успешного выполнения вернёт что-то с этим же id (по id мы понимаем, на какой именно запрос это был ответ). Например
Вызываем метод получения списка юзеров со статусом "admin", отправляя:
{ type:"method", name:"getUsers", args: ["admin"], id:10 }.
Получаем в ответ
{"result": ["Вася", "Петя", "Маша"], "error": null, "id":10}
Отдельный бонус в том, что такие методы можно вызывать действительно удалённо, т.е. передавая запрос не только от одного процесса к другому на одном компе, а удалённым серверам (если у них, конечно, открыты соответствующие порты, а ваш провайдер разрешает по этим портам общаться; у меня на работе, скажем, все порты кроме 80 и 443 для http закрыты злым админом; зато дома весь парк машин подключенных к роутеру видят порты друг друга). Это, в частности, позволяет делать масштабируемую систему, разнося её сервисы по разным машинам.
Подробнее - https://ru.wikipedia.org/wiki/JSON-RPC
Прокси-обьекты
Всё сказанное выше не имело прямого отношения к ILAB3 :-) Точнее, это конечно происходит там внутри, но прелесть в том, что ilab будет за вас формировать json-пакеты и гонять их по сокетам.
Вам нужно будет написать только что-то типа
proxyForMyRemoteService.Beep(20,5)
чтобы заставить ваш удалённый сервис 20 секунд пищать на частоте 5 мГц )) (если у него, конечно, реализована такая функция).
Что такое в данном proxyForMyRemoteService? Это такой обьект-посредник (фасад?) для общения с удалённым сервером, поддерживающем наш json-rpc протокол; такие посредники у нас называются прокси.
В модуле ServiceProxy (или его вариациях, я написал ещё его вариант на промисах) и описан класс таких обьектов, делающих за нас нудную работу. Использование:
var ServiceProxy = useModule('ServiceProxy'); // подключение модуля
var proxy = new ServiceProxy(); // создание объекта
proxy.attach(port, host, callback); // подключение к сервису, работающему на машине с адресом host, слушающей порт port, в callback - функция которая выполняется когда подключение успешно завершено и можно начинать работать.
proxy.beep(10,5) ; // одна эта строка формирует json-пакет и шлёт его по сокету
Это собственно главное. Если вас интересует ответ вызыванного метода, нам нужно как-то прописать callback, который вызовется когда этот ответ придёт. Пока предлагается такой синтаксис
proxy.getUsers("admin", function(result) { myusers = result; } ).
Т.е. идея передавать callback после списка аргументов функции.
Ещё у меня есть реализация на прокси:
var ServiceProxyPromises = useModule('ServiceProxyPromises');
var proxyP = new ServiceProxyPromises();
proxyP.attach(port, host, function () {
proxyP.getUsers("admin").then(
function(result) { console.log("Штук юзеров: " + result.length); },
);
});
Если вы понимаете зачем нужны прокси, вы оцените прелесть подхода, для остальных он может показаться сложным и проще пользоваться просто колбэками.
https://github.com/AlfLearn/ILAB-3.0/tree/master/Modules - здесь в файлах "примеры использования ServiceProxy.txt", "ServiceProxyPromises.js" и "ServiceProxy.js" мои версии прокси-обьектов (это форк ilab3, да).
Сервисы
Сервисом мы будем называть процесс, который умеет понимать команды по нашему json-prc и с которым можно ощаться через наши proxy-обьекты. В принципе можно написать хоть произвольное node-приложение, лишь бы оно реализовало протокол. Но удобнее использовать готовый класс сервиса, который из коробки умеет не только слушать json-команды, но и интерпретировать их как вызов своих собственных методов.
Базовый класс сервиса описан в модуле Service.js
Для того чтобы написать свой сервис, надо унаследовать его от Service. Как-то так:
function myService(port){
this.users = [];
var self = this;
// это публичная функция:
this.getUsers = function(status) {
var result = [];
for(var i=0; i<self.users.length; i++)
if(self.users[i].status==status) result.push(self.users[i]);
return result;
};
return Service.call(this, port, "myService");
}
Inherit(myService, Service, {
... тут какие-то внутренние методы сервиса
}
Обращаю внимание - публичные функции (т.е. доступные для rpc-вызовов) прописываются как явные свойства обьекта сервиса (а не его его прототипа). Т.е. только собственные методы обьекта сервиса (те которые hasOwnProperty) автоматически попадают в публичный интерфейс обьекта (upd - ещё туда не попадают функции, имена которых начинаются с "_", по принятному соглашению для именования как бы приватных свойств объектов).
Всё, теперь вы умеете создавать сервисы. Вы можете написать и запустить несколько разных node-скриптов реализующих в себе функционал сервисов, научить их обмениваться информацией и командами.
Но пока это всё ещё зоопарк сервисов, каждый из которых во-первых, нужно запускать вручную, во-вторых - чтобы один процесс обратился к другому он должен знать его порт. В этом нет ничего крамольного, например, mondodb слушает стандартный для него порт 27100; но мы-то знаем, что сервисов напишем много и разных и резервировать под каждый номера портов как-то неправильно, лучше же выдавать их динамически?
Потому автоматизируем управление "зоопарком".
Менеджер сервисов
Это такой сервис, который управляет другими сервисами. А сам он является сервисом потому, что им можно удалённо управлять - т.е. вызывать у него команды по созданию, остановке, выяснению статуса и прочему контролю над другими сервисами.
Менеджер сервисов при создании других сервисов передаёт им (через командную строку) номер порта, на котором они будут работать. Например, его просят сделать "userService", он ищет модуль с таким именем, запускает сервис, выдавая ему очередной номер порта (скажем, 4997) и запоминает всю информацию об этом сервисе (номер порта, pid для удаления процесса и т.п.).
Теперь другим процессам, чтобы подключиться к модулю userService не обязательно знать его порт. Достаточно знать только порт менеджера сервисов, создать прокси для общения с ним и спросить у него - "а скажи мне номер порта для userService". А дальше уже по полученному порту создавать прокси для общения уже с userService.
Менеджер сервисов объявлен в ServicesManager.js , но по состоянию на 5.07.2016 удалённо управлять им ещё нельзя :-)
Также в todo кому-то - автозагрузка сервисов по списку из config-файла.
Менеджер "узлов"
NodesManagerService.js - похож на менеджер сервисов, только управляет не сервисами, а "узлами сервера". "Узлы" (nodes) - это что-то вроде серверов, конечные пользователи сервисов. Например, KLAB - это "узел" (может называть его как-то вроде subserver?). На практике в WM "узлы" обычно описывают поддомены сайта, реализующие разные функционал. Хотя для локальных копий ILAB, скажем стоящих на ноуте и управляющих 3d-принтером, "узел", наверное, сможет означать и отдельную единицу внешнего оборудования. Отличие "узла" от "сервиса" примерно такое, как exe-шника от dll-ки (на вашем компьютере могут исполняться много разных программ, контролирумых виндой, которые могут использовать общие dll-библиотеки).
Самый главный скрипт - rootService.js
Практически ничего не делает. Его единственная задача - запустить сервисы и узлы сервера. Ну, хорошо бы только те, что указаны в config-е, хотя и не обязательно (это может быть в конфигах самих запускаемых им модулей). По-возможности сам он должен никода не падать, но если это всё же случится - перезапускаться обратно средствами ОС.
Фреймы
Фреймы - это запущенные экземпляры node.exe. Если в винде посмотреть через диспетчер задач на запущенные node-процессы, мы увидим что все сервисы - это на самом деле "node ServiceFrame.js", исполняющие каждый свой сервис. А все "узлы" - это "node NodeFrame.js", обсулуживащие каждый свой, эм, поддомен.
Это означает, что технически ваш сервис "myService" - это экземляр ServiceFrame.js в глубинах которого сделано что-то вроде require("myService"), а узел "myNode" - соответственно исполняемый NodeFrame.js в глубинах которого - require("myMode").
Для вас это означает, что вам (в коде вашего сервиса ил узла) доступен глобальный обьект Frame, в котором уже хранятся некоторые переменные окружения. Например, жизненно необходимый для общения с другими сервисами Frame.servicesManagerPort , через который вы можете общаться с менеджером сервисов.
Как начать писать?
Написать свой сервис и узел. Серьёзно, напишите сервис показывающий текущее время и узел, который будет спрашивать у него это время через прокси и выводить в консоль. А потом, когда мы поймём, как делать узлы веб-сервера - ещё и узел с веб-интерфейсом.
Структура папок сервера ILAB3.0
Сейчас это выглядит так:
Modules\ - библиотечные функции и классы
Nodes\ - узлы
Services\ - сервисы
Storage\ - папка для хранения данных (база данных в файликах)
RootService.js - главный скрипт, который нужно запускать
________________________________________
ВНИМАНИЕ, дальнеший текст уже отчасти мои домыслы
Возможно у разработчика ILAB иное видение.
Здесь я (AlfLearn) описываю то, как сам решал бы эту задачу
Возможно, здесь будет обсуждение.
Роутинг, веб-сервер, статические и динамические ресурсы
Внешний мир обычно смотрит на сервер через 80-й порт и http-протокол. Когда браузер стучится на сервер, он по умолчанию пытается подключиться к его 80-му порту. И это порт один на всех. А "узлов" у нас много, что делать? Распределить возможные запросы по разным узлам; так, всё идущее на http://klab.web-manufacture.net/ будет перенаправляться узлу KLAB, а всё с http://sandbox.web-manufacture.net/ - видимо на узел "sandbox".
Также каждый такой узел-поддомен может хотеть иметь свой "сайт" - набор html-страничек, файловую систему - то что называется "статический контент". Для этого у каждого узла может быть своя папка которая отображается (maping) на систему папок сайта. Этим занимается сервер статических файлов - т.е. когда придёт запрос "klab.web-manufacture.net/editors/supereditor.html", а с klab у нас сопоставлена папка "D:\MyDocuments\klab\" он должен будет отдать файл "D:\MyDocuments\klab\editors/supereditor.html".
Различие статики и динамики. Статичный контент сайта (отдельного узла) меняется редко и представляет собой просто содержимое какой-то папки. Чтобы его отдать, вобщем-то даже участие узла не требуется, обычно это просто сайт для работы с этим узлом уже через динамические запросы.
Динамические данные перенаправляются роутингом уже к "узлам", потому что только они знают как на них отвечать. Потому если вы послали, скажем ajax-запрос GET на "klab.web-manufacture.net/api/users?status=10", роутер уже должен перенаправить такой запрос узлу KLAB (в виде json-пакета с полем werb:"get" и телом запроса), дождаться ответа от него и вернуть веб-пользователю.
Как различать статические и динамические ресурсы? Если запрос похож на имя файла, скорее всего хотят статику (но если такой файл таки не найден, можно попробовать узнать его уже динамически). Если запрос содержит "?" знак, скорее всего это динамика. Но по-хорошему, лучше просто прописать в конфиге шаблон запросов, трактуемых как динамические (например, всё начинающееся с "api/"), а остальное обрабатывать как статику.
В принципе, всеми вопросами отдачи по http-во внешний мир может заниматься один сервис - "вебсервер". Каждый узел, желающий отдавать динамический контент, видимо, должен будет создать в себе нечто (тоже сервис?), слушающее запросы от веб-сервера, что скорее всего будет оформлено в виде класса ServerNode, т.е. "серверного узла", который из коробки будет уметь слушать json-представление http-запросов и как-то отвечать на них.
По-видимому, этим должны заниматься FilesService.js, RoutingService.js, StaticService.js, но их устройство меня несколько пугает и я не уверен, что они рабочие, потому что тупо скорпированы в Server3 из ILAB-master :-) (по состоянию на 5.07.2016)
Ещё есть проблема вебсокетов. В общем случае я не представляю как их данные транслировать узлам. В частном случае - у веб-сервера может быть socket.io и все коннекты на разные поддомены от разных юзеров мы будем транслировать в json-rpc. Так что для узлов это будет только один из видов сообщений.
В некоторых случаях (личные папки, группы юзеров), право на просмотр какого-то фрагмента даже статического контента может определяться профилем юзера. Потому можно или каждый раз спрашивать у узла, разрешено ли показать данный ресурс (папку или файл), прежде чем отдавать статику. Или определить хотя бы шаблон таких запросов, для которых требуются такие подтверждения (скажем, всё начинающееся с "klab.web-manufacture.net/users" требует подтверждения, остальное - нет).
Потому переходим к вопросу авторизации.
Авторизация, права доступа
Авторизация. Если кому-то где-то нужно ограничить доступ к ресурсам узла, нужно проверять токен, приходящий в куки сессии. Токен выдаётся при авторизации (если кто не знает, это такой разовый пароль доступа, который вам выдают на время сессии за правильно введённый постоянный пароль и даже незарегистрированным юзерам, просто чтобы узнавать их, когда они заходят с другой страницы того же браузера). Куки свои для каждого домена, потому на каждом придётся авторизоваться отдельно.
Сервис авторизации плотно связан с сервисом веб-сервера, потому что при успешной авторизации веб-сервер должен будет записать токен в куки (а доступ к ним имеет только веб-сервер). Возможно его даже можно сделать частью веб-сервера.
Редактируемые папки юзеров и git
Редактируемые папки - это та часть статического контента, которую может редактировать данный юзер в зависимости от своих прав. Права определяются сервисом авторизации (т.е. при выполнении опасного действия от имени юзера, узел должен спросить, а имеет ли юзер с таким логином и токеном такие права), хотя конечное решение всё равно за узлом.
Манипуляции с git - это просто надстройка над редактором файлов и папок. В простейшем случае она означает "я тут внёс несколько изменений, давайте сохраним мою версию", т.е. сделаем save состояния папки, как делаем сейвы в компьютерных играх. Тут можно пофантазировать над коллективным редактированием, но такие вещи лучше всё же делать в личных папках, отдельных для каждого юзера; которые можно защитить хотя бы от редактирования другими, а по желанию - и от просмотра.