Node.js developer tools. Remote procedure call on web sockets
Horror stories often tell about websocket technology, for example, that it is not supported by web browsers, or that providers / admins suppress websocket traffic - therefore it cannot be used in applications. On the other hand, developers do not always foresee the pitfalls that websocket technology has, like any other technology. As for the alleged limitations, I’ll say right away that 96.8% of web browsers support websocket technology today. You can say that 3.2% remaining overboard is a lot, these are millions of users. I completely agree with you. Only everything is known in comparison. The same XmlHttpRequest, which everyone has been using in Ajax for many years, supports 97.17% of web browsers (not much more, right?), And fetch in general, 93.08% of web browsers. Unlike websocket, such a percentage (and earlier it was even less) has long stopped anyone using Ajax technology. So using fallback on long polling currently makes no sense. If only because web browsers that do not support websocket are the same web browsers that do not support XmlHttpRequest, and in reality no fallback will happen.
The second horror story about banning on websocket from providers or admins of corporate networks is also unreasonable, since now everyone uses the https protocol, and it is impossible to understand that websocket connection is open (without breaking https).
As for the real limitations and ways to overcome them, I will tell in this post, on the example of developing the web admin area of the application.
So, the WebSocket object in the web browser has, frankly, a very concise set of methods: send () and close (), as well as the addEventListener (), removeEventListener () and dispatchEvent () methods inherited from the EventTarget object. Therefore, the developer must use libraries (usually) or independently (almost impossible) to solve several problems.
Let's start with the most understandable task. The connection to the server is interrupted periodically. Reconnecting is easy enough. But if you remember that messages from both the client and the server continue to go at this time, everything becomes immediately and much more complicated. In general, a message can be lost if a confirmation mechanism for the received message is not provided, or re-delivered (even multiple times) if the confirmation mechanism is provided, but the failure occurred just at the moment after receipt and before the message was confirmed.
If you need guaranteed message delivery and / or message delivery without takes, then there are special protocols for implementing this, for example, AMQP and MQTT, which work with websocket transport. But today we will not consider them.
Most libraries for working with websocket support transparent for the programmer reconnecting to the server. Using such a library is always more reliable than developing your implementation.
Next, you need to implement the infrastructure for sending and receiving asynchronous messages. To do this, use the “naked” onmessage event handler without additional binding, a thankless task. Such an infrastructure can be, for example, remote procedure call (RPC). The id id was introduced into the json-rpc specification, specifically for working with the websocket transport, which allows you to map the remote procedure call by the client to the response message from the web server. I would prefer this protocol to all other possibilities, but so far I have not found a successful implementation of this protocol for the server part on node.js.
And finally, you need to implement scaling. Recall that the connection between the client and the server periodically occurs. If the power of one server is not enough for us, we can raise several more servers. In this case, after the connection is disconnected, the connection to the same server is not guaranteed. Typically, a redis server or a cluster of redis servers is used to coordinate multiple websocket servers.
And, unfortunately, sooner or later we will run into system performance anyway, since the capabilities of node.js in the number of websocket connections open at the same time (do not confuse this with speed) are significantly lower than with specialized servers such as message queues and brokers. And the need for cross-exchange between all instances of websocket servers through a redis server cluster, after some critical point, will not give a significant increase in the number of open connections. The way to solve this problem is to use specialized servers, such as AMQP and MQTT, which work, including with websocket transport. But today we will not consider them.
As you can see from the list of listed tasks, cycling while working with websocket is extremely time-consuming, and even impossible if you need to scale the solution to several websocket servers.
Therefore, I propose to consider several popular libraries that implement work with websocket.
I will immediately exclude from consideration those libraries that implement only fallback on obsolete modes of transport, since today this functionality is not relevant, and libraries that implement wider functionality, as a rule, also implement fallback on obsolete modes of transport.
I'll start with the most popular library - socket.io. Now you can hear the opinion, most likely fair, that this library is slow and expensive in terms of resources. Most likely it is, and it works slower than native websocket. However, today it is the most developed library by its means. And, once again, when working with websocket, the main limiting factor is not speed, but the number of simultaneously open connections with unique clients. And this question is best solved already by making connections with clients to specialized servers.
So, soket.io implements reliable recovery when disconnecting from the server and scaling using a server or a cluster of redis servers. socket.io, in fact, implements its own individual messaging protocol, which allows you to implement messaging between the client and server without being tied to a specific programming language.
An interesting feature of socket.io is the confirmation of event processing, in which an arbitrary object can be returned from the server to the client, which allows for remote procedure calls (although it does not comply with the json-rpc standard).
Also, preliminary, I examined two more interesting libraries, which I will briefly discuss below.
Faye library faye.jcoglan.com. It implements the bayeux protocol, which was developed in the CometD project and implements the subscription / distribution of messages to message channels. This project also supports scaling using a server or a cluster of redis servers. An attempt to find a way to implement RPC was unsuccessful because it did not fit into the bayeux protocol scheme.
In the socketcluster socketcluster.io project , the emphasis is on scaling the websocket server. At the same time, the websocket server cluster is not created on the basis of the redis server, as in the first two mentioned libraries, but on the basis of node.js. In this regard, when deploying the cluster, it was necessary to launch a rather complex infrastructure of brokers and workers.
Now let's move on to the implementation of RPC on socket.io. As I said above, this library has already implemented the ability to exchange objects between the client and server:
This is the general scheme. Now we will consider each of the parts in relation to a specific application. To build the admin panel, I used the react-admin library github.com/marmelab/react-admin . Data exchange with the server in this library is implemented using a data provider, which has a very convenient scheme, almost a kind of standard. For example, to get a list, the method is called:
This method in an asynchronous response returns an object:
There are currently an impressive number of react-admin data provider implementations for various servers and frameworks (e.g. firebase, spring boot, graphql, etc.). In the case of RPC, the implementation turned out to be the most concise, since the object is transferred in its original form to the emit function call:
Unfortunately, a little more work had to be done on the server side. To organize the mapping of functions that handle the remote call, a router similar to express.js was developed. Only instead of the middleware (req, res, next) signature the implementation relies on the signature (socket, payload, callback). As a result, we all got the usual code:
Details of the implementation of the router can be found in the project repository.
All that remains is to assign a provider for the Admin component:
Useful Links
1. www.infoq.com/articles/Web-Sockets-Proxy-Servers
apapacy@gmail.com
July 14, 2019
The second horror story about banning on websocket from providers or admins of corporate networks is also unreasonable, since now everyone uses the https protocol, and it is impossible to understand that websocket connection is open (without breaking https).
As for the real limitations and ways to overcome them, I will tell in this post, on the example of developing the web admin area of the application.
So, the WebSocket object in the web browser has, frankly, a very concise set of methods: send () and close (), as well as the addEventListener (), removeEventListener () and dispatchEvent () methods inherited from the EventTarget object. Therefore, the developer must use libraries (usually) or independently (almost impossible) to solve several problems.
Let's start with the most understandable task. The connection to the server is interrupted periodically. Reconnecting is easy enough. But if you remember that messages from both the client and the server continue to go at this time, everything becomes immediately and much more complicated. In general, a message can be lost if a confirmation mechanism for the received message is not provided, or re-delivered (even multiple times) if the confirmation mechanism is provided, but the failure occurred just at the moment after receipt and before the message was confirmed.
If you need guaranteed message delivery and / or message delivery without takes, then there are special protocols for implementing this, for example, AMQP and MQTT, which work with websocket transport. But today we will not consider them.
Most libraries for working with websocket support transparent for the programmer reconnecting to the server. Using such a library is always more reliable than developing your implementation.
Next, you need to implement the infrastructure for sending and receiving asynchronous messages. To do this, use the “naked” onmessage event handler without additional binding, a thankless task. Such an infrastructure can be, for example, remote procedure call (RPC). The id id was introduced into the json-rpc specification, specifically for working with the websocket transport, which allows you to map the remote procedure call by the client to the response message from the web server. I would prefer this protocol to all other possibilities, but so far I have not found a successful implementation of this protocol for the server part on node.js.
And finally, you need to implement scaling. Recall that the connection between the client and the server periodically occurs. If the power of one server is not enough for us, we can raise several more servers. In this case, after the connection is disconnected, the connection to the same server is not guaranteed. Typically, a redis server or a cluster of redis servers is used to coordinate multiple websocket servers.
And, unfortunately, sooner or later we will run into system performance anyway, since the capabilities of node.js in the number of websocket connections open at the same time (do not confuse this with speed) are significantly lower than with specialized servers such as message queues and brokers. And the need for cross-exchange between all instances of websocket servers through a redis server cluster, after some critical point, will not give a significant increase in the number of open connections. The way to solve this problem is to use specialized servers, such as AMQP and MQTT, which work, including with websocket transport. But today we will not consider them.
As you can see from the list of listed tasks, cycling while working with websocket is extremely time-consuming, and even impossible if you need to scale the solution to several websocket servers.
Therefore, I propose to consider several popular libraries that implement work with websocket.
I will immediately exclude from consideration those libraries that implement only fallback on obsolete modes of transport, since today this functionality is not relevant, and libraries that implement wider functionality, as a rule, also implement fallback on obsolete modes of transport.
I'll start with the most popular library - socket.io. Now you can hear the opinion, most likely fair, that this library is slow and expensive in terms of resources. Most likely it is, and it works slower than native websocket. However, today it is the most developed library by its means. And, once again, when working with websocket, the main limiting factor is not speed, but the number of simultaneously open connections with unique clients. And this question is best solved already by making connections with clients to specialized servers.
So, soket.io implements reliable recovery when disconnecting from the server and scaling using a server or a cluster of redis servers. socket.io, in fact, implements its own individual messaging protocol, which allows you to implement messaging between the client and server without being tied to a specific programming language.
An interesting feature of socket.io is the confirmation of event processing, in which an arbitrary object can be returned from the server to the client, which allows for remote procedure calls (although it does not comply with the json-rpc standard).
Also, preliminary, I examined two more interesting libraries, which I will briefly discuss below.
Faye library faye.jcoglan.com. It implements the bayeux protocol, which was developed in the CometD project and implements the subscription / distribution of messages to message channels. This project also supports scaling using a server or a cluster of redis servers. An attempt to find a way to implement RPC was unsuccessful because it did not fit into the bayeux protocol scheme.
In the socketcluster socketcluster.io project , the emphasis is on scaling the websocket server. At the same time, the websocket server cluster is not created on the basis of the redis server, as in the first two mentioned libraries, but on the basis of node.js. In this regard, when deploying the cluster, it was necessary to launch a rather complex infrastructure of brokers and workers.
Now let's move on to the implementation of RPC on socket.io. As I said above, this library has already implemented the ability to exchange objects between the client and server:
import io from 'socket.io-client';
const socket = io({
path: '/ws',
transports: ['websocket']
});
const remoteCall = data =>
new Promise((resolve, reject) => {
socket.emit('remote-call', data, (response) => {
if (response.error) {
reject(response);
} else {
resolve(response);
}
});
});
const server = require('http').createServer();
const io = require('socket.io')(server, { path: '/ws' });
io.on('connection', (socket) => {
socket.on('remote-call', async (data, callback) => {
handleRemoteCall(socket, data, callback);
});
});
server.listen(5000, () => {
console.log('dashboard backend listening on *:5000');
});
const handleRemoteCall = (socket, data, callback) => {
const response =...
callback(response)
}
This is the general scheme. Now we will consider each of the parts in relation to a specific application. To build the admin panel, I used the react-admin library github.com/marmelab/react-admin . Data exchange with the server in this library is implemented using a data provider, which has a very convenient scheme, almost a kind of standard. For example, to get a list, the method is called:
dataProvider(
‘GET_LIST’,
‘имя коллекции’,
{
pagination: {
page: {int},
perPage: {int}
},
sort: {
field: {string},
order: {string}
},
filter: {
Object
}
}
This method in an asynchronous response returns an object:
{
data: [ коллекция объектов],
total: общее количество объектов в коллекции
}
There are currently an impressive number of react-admin data provider implementations for various servers and frameworks (e.g. firebase, spring boot, graphql, etc.). In the case of RPC, the implementation turned out to be the most concise, since the object is transferred in its original form to the emit function call:
import io from 'socket.io-client';
const socket = io({
path: '/ws',
transports: ['websocket']
});
export default (action, collection, payload = {}) =>
new Promise((resolve, reject) => {
socket.emit('remote-call', {action, collection, payload}, (response) => {
if (response.error) {
reject(response);
} else {
resolve(response);
}
});
});
Unfortunately, a little more work had to be done on the server side. To organize the mapping of functions that handle the remote call, a router similar to express.js was developed. Only instead of the middleware (req, res, next) signature the implementation relies on the signature (socket, payload, callback). As a result, we all got the usual code:
const Router = require('./router');
const router = Router();
router.use('GET_LIST', (socket, payload, callback) => {
const limit = Number(payload.pagination.perPage);
const offset = (Number(payload.pagination.page) - 1) * limit
return callback({data: users.slice(offset, offset + limit ), total: users.length});
});
router.use('GET_ONE', (socket, payload, callback) => {
return callback({ data: users[payload.id]});
});
router.use('UPDATE', (socket, payload, callback) => {
users[payload.id] = payload.data
return callback({ data: users[payload.id] });
});
module.exports = router;
const users = [];
for (let i = 0; i < 10000; i++) {
users.push({ id: i, name: `name of ${i}`});
}
Details of the implementation of the router can be found in the project repository.
All that remains is to assign a provider for the Admin component:
import React from 'react';
import { Admin, Resource, EditGuesser } from 'react-admin';
import UserList from './UserList';
import dataProvider from './wsProvider';
const App = () => ;
export default App;
Useful Links
1. www.infoq.com/articles/Web-Sockets-Proxy-Servers
apapacy@gmail.com
July 14, 2019