With help of WebSockets developer can implement a two-way Server<->Client communication, for example sent a notification from server to client when some task are finished on the server-side or when some data is changed etc.
WebSockets specification itself do not provide neither security nor data format recommendation for such communication, so in UnityBase we implement a simple JSON messaging protocol and secure it using build-in UnityBase auth mechanisms.
System requirements & limitations
You need a Windows8 & up or Windows Server 2012 & up on the server side.
All modern browser are support WebSockets on the client side. See the table
A single WebSocket connection take a 5Kb of memory. In case you need more when 50000 concurrent connection several instance of server & load balancing are required.
On the JsonMessagesProtocol#broadcast operation we discover a linear latency growing during increasing a number of active connection (the worst case are measured):
The JsonMessagesProtocol#sendCommand latency is practically independent of the number of connections and remains within 2-50ms., so our recommendation is to limit a JsonMessagesProtocol#broadcast usage if you expect more when 10000 concurrent WebSocket connections. Usage of JsonMessagesProtocol#sendCommand is not a bottleneck.
Quick example
Set up a Autotest
application and:
- see how we initialize server-side protocol in the
Autotest\models\TST\_wsThreadInitialization.js
- explore a pure HTML example
- see a entity
Autotest\models\TST\tst_maindata.js
for example of sending notification after entityupdate
/insert
Server-side
On the server side WebSockets implemented as a dedicated (compiled inside UB.exe
) service with its own threads pool.
By default UnityBase start a single thread for WebSocket (wsServer.threadPoolSize === 1), so code inside this
thread must be as fast as possible(like in node.js). Because of this we do not add a database (domain) support to
such threads. In case you need a database operations you must call a HTTP server. We plane to add a fast database (sqlite3
)
support for WebSocket threads in future.
Enable WebSocket support
To set up a WebSocket support on the server side you must add a wsServer
section to the server config (example):
"httpServer": {...},
"wsServer": {
"host": "127.0.0.1",
"port": "888",
"path": "ws"
},....
Because WebSockets is implemented over HTTP(S), you MUST configure a httpServer
also. In case you expect
many (more when 10000) concurrent connection tuning of HTTP.SYS is required (depend on your usage scenario).
See HTTPServer for details.
Add a protocol
The second step is to add a protocols (subprotocols
in term of WebSocket RFC) in the WebSocket threads.
UnityBase will call require('models\yourModel\_wsThreadInitialization.js')
for each domain
model during server startup. Inside this module you initialize a build-in {UB#getWSNotifier wsNotifier} protocol:
var wsNotifier = UB.getWSNotifier();
and assign handlers to it (if you need to receive web socket messages in server):
if (wsNotifier) {
console.debug('Start subscribing to wsNotifier tsts_* events');
wsNotifier.on('tst_echo', function (connection, params) {
connection.send({
command: 'tst_message', params: {from: connection.session.userID, message: params}
});
});
}
or create a your own protocol implementation - see for example how {UB#getWSNotifier UB.getWSNotifier()} is implemented.
Sending a message
You can send a message to the WebSocket connection either from WebSocket thread or from HTTP thread. In case of HTTP thread do not try to obtain a protocol instance during thread initialization - both HTTP & WebSocket thread pool are initialized in parallel. So you must obtain a protocol instance inside entity methods:
function SomeMethod(ctx){
var notifier = UB.getWSNotifier();
if (notifier) {
store.currentDataName = 'selectAfterInsert';
if (!store.eof) {
rowID = store.get('ID');
}
//send message to ALL connected sessions
notifier.broadcast('ub_entityModification', {action: 'insert', ID: rowID});
//Send to specific user
var userSessions = notifier.getUserSessions(Session.userID);
userSessions.forEach(function(sessionID){
notifier.sendCommand('test_command', sessionID, {action: 'inserted', ID: rowID});
});
}
}
WARNING To avoid conflicts with other models ALWAYS append a model prefix to your events name
//WRONG
notifier.sendCommand('hello', sessionID, {action: 'inserted', ID: rowID});
//GOOD (trs - is a model code here)
notifier.sendCommand('trs_hello', sessionID, {action: 'inserted', ID: rowID});
For a full list of available methods see {JsonMessagesProtocol}
Client side implementation
ub-core.js
package contain a client-side helper class UBNotifierWSProtocol. For a adminUI
instance of this
class is accessible via singleton $App.ubNotifier
Inside your model you can subscribe to events of $App.ubNotifier:
if ($App.ubNotifier.supported){
$App.ubNotifier.on('ubs_message', me.checkMessages.bind(me));
}
To check UnityBase server support a spacified WebSocket protocol (i.e WebSocket is supported by the server OS,
configured in the server config and protocol are registered) you must check a supportedWSProtocols
section
of getAppInfo
response.
On the client side HTML5 WebSocket can be used:
$App.connection.authorize().then(function(session){
$session = session;
$ws = new WebSocket('ws://' + location.host + '/autotest/ws?SESSION_SIGNATURE=' + session.signature(), 'ubNotifier');
$ws.onopen = function(e){
console.log('Connected to:', e.target.url, 'protocol:', e.target.protocol);
};
$ws.onmessage = function(e){
var msg;
try {
msg = JSON.parse(e.data)
} catch (err) {
console.error('Invalid command from server:', e.data);
}
var
command = msg.command,
params = msg.params;
if (command === 'accepted') {
console.log('Server accept connection with session ID=', params.connectionID);
} else if (command === 'tst_message') {
console.log('New message. ', params.from, 'said:', params.message);
} else if (command === 'error') {
console.error('Error. Details:', params.description);
} else {
console.error('Invalid server response. Command', command, 'is unknown');
}
};
$ws.onclose = function(e){
console.log('Connection closed from server. Code:', e.code, 'Reason:', e.reason);
}
}).done();
Now you can send a messages
$ws.send(JSON.stringify({command: 'yourCommand', params: {a: 12, b: 2}});
Application architecture tips
TODO
Написать о том, что
- вебсокетный поток должен работать бысто.
- нет гарантированной доставки.
Для нотификаций рекомендовать архитектуру:
- клиент залогинился в систему.
- клиент вызвает HTTP метод "дайВсеМоиСообщения" (сущность в БД)
- клиент создал вебсокет и ждет нотификаций
- если что то приизошло на сервере сервер кладет (модифицирует) сущность с сообщениями и шлет веб-сокет команду "новоеСообщение" (не факт что клиент онлайн сейчас)
- клиент получает команду "новоеСообщение" и вызвает HTTP метод "дайВсеМоиСообщения"
- клиент вызывает HTTP метод "яПонялСообщение": {ID: IDсообщения} - сервер помечает сообщение как полученное клиентом
Такая архитектура не ломает обычный HTTP мир. Если веб-сокеты не доступны клиент может вызывать HTTP метод "дайВсеМоиСообщения" периодически без получения команды.
Для гарантированной доставки уведомлений с подтверждением получения и сохранением истории уведомлений/факта прочтения использовать ГОТОВУЮ подсистему message Notification subsystem