Single Page Apps and Realtime APIs
About me
Software Engineer at Mathspace:
Main author of SocketCluster:
Twitter: https://twitter.com/jgrosdubois
Jonathan Gros-Dubois
What is realtime?
What problem does it solve?
Protocol comparison
- HTTP follows a request-response mode of interaction - The client initiates the connection.
- The server can only respond to requests from the client - It cannot initiate.
- With WebSockets, the client initiates the connection, but afterwards, either party is free to send a message to the other.
- The connection is kept alive.
- After the initial handshake, raw messages can be sent using WebSocket frames - These are lightweight - Unlike HTTP requests, they carry no headers.
HTTP polling hacks
There are ways to emulate WebSockets using HTTP long polling.
SockJS
These solutions are great at first but things can get difficult when you need to scale beyond a single process/host.
Emulating a single realtime connection over multiple HTTP requests has the following shortcomings:
- You lose the in-order message delivery guarantee which a single TCP connection would otherwise provide.
- Each new HTTP request contains extra header information which is not needed - This will consume extra CPU and memory.
- In a multi-process or multi-host setup, some requests could be sent to the wrong process, so you have to use sticky load balancers to route requests to the correct server. This has scalability and security implications.
Performance comparison
Reactive/declarative programming and live binding of data in views
Renders to =>
Basic example (PolymerJS):
Declarative over imperative means that you don't need to get references to particular elements and explicitly render stuff inside them.
You declare the relationship between your views and your data and the rest is automatic. Data is free to flow throughout your views. Less control logic.
On the front-end, data binding allows you to relinquish unecessary control
Too much control can be dangerous
With Realtime APIs, you should also relinquish some control
Writing too much control logic on the server is an anti-pattern.
Because WebSockets are bidirectional, you may be tempted to get references to specific client sockets when you want to provide them with data.
The most common question from new users
of SocketCluster who are running their code on
multiple processes goes like this:
"I want to send a message to a specific user from the server side. How can I get a reference to a
socket which is hosted on a different worker process?"
The problem
A naive
solution
Naive solution at scale
Good patterns for realtime
The server should regulate data flow, not dictate it. That means avoiding doing lookups to get a hold of specific sockets! Managing propertyName=>socketID mappings is hard work... Even if you've only running your code on a single process, you have to remember to cleanup the mappings when sockets leave.
The solution is to give more control to the consumer of the data (the front-end) - Pub/Sub is a good, scalable way to achieve that. With a middleware/authorization layer, you can enforce access control.
SocketCluster lets you live bind directly to server-side data in realtime.
You can use it with any database you like.
A REST-like pattern for realtime APIs
socket.emit('get', {type: 'Product', id: 123, field: 'price'}, errorAndDataHandlerFn);
socket.emit('set', {type: 'Product', id: 123, field: 'price', value: 99.95}, errorHandlerFn);
socket.emit('delete', {type: 'Product', id: 123}, errorHandlerFn);
var productChannel = socket.subscribe('Product/123/price');
productChannel.watch(channelDataHandler);
// Realtime equivalent to REST's GET method
// Equivalent to REST's POST and UPDATE methods - You can also separate this
// into 'set' and 'update' if you like
// Equivalent to REST's DELETE method
// This is the one which gives us realtime updates. In practice, you could use
// REST over HTTP instead of all the others and only use sockets for this one; that
// way the realtime update can be a progressive enhancement for newer browsers only.
On the server-side
scServer.on('connection', function (socket) {
socket.on('get', function (query, callback) {
var deepKey = [query.type, query.id];
if (query.field) {
deepKey.push(query.field);
}
scServer.global.get(deepKey, callback);
});
socket.on('set', function (query, callback) {
var deepKey = [query.type, query.id];
if (query.field) {
deepKey.push(query.field);
}
scServer.global.set(deepKey, query.value, function (err) {
if (!err) {
var channelName;
if (query.field) {
scServer.global.publish(query.type + '/' + query.id + '/' + query.field, query.value);
}
// TODO: Also publish to higher levels in hierarchy: query.type + '/' + query.id and just query.type channels
}
callback(err);
});
});
});
Here we are using SC's internal 'global' hierarchical data store to hold the data in memory (not persistent - So only for demo purposes) - You can use any database/datastore you like.
Try it yourself
Single Page Apps and Realtime APIs
By grosjona
Single Page Apps and Realtime APIs
- 8,805