Websocket technology is often told by horror stories, for example, that it is not supported by web browsers, or that providers / admins block websocket traffic - therefore, it cannot be used in applications. On the other hand, developers do not always represent in advance the pitfalls that the websocket technology has, like any other technology. As for imaginary restrictions, I’ll say at once that 96.8% of web browsers support websocket technology today. You can say that the remaining 3.2% is a lot, it is millions of users. I completely agree with you. Only everything is known in comparison. The same XmlHttpRequest, which everyone has been using in Ajax technology for several years now, 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 smaller) has long stopped anyone using the Ajax technology. So now use a fallback on long polling does not make any sense. At least because web browsers that do not support websocket are the same web browsers that do not support XmlHttpRequest, and in reality no fallback will occur.
The second horror story, about the ban on the websocket from the providers or administrators of corporate networks is also unjustified, since now everyone is using the https protocol, and it is impossible to understand that the websocket connection is open (without hacking https).
As for the real restrictions and ways to overcome them, I will tell in this post, on the example of developing a web-admin application.
So, the WebSocket object in the web browser has, frankly, a very laconic set of methods: send () and close (), as well as methods addEventListener (), removeEventListener () and dispatchEvent () inherited from the EventTarget object. Therefore, the developer must solve several problems using libraries (as a rule) or independently (almost impossible).
')
Let's start with the most understandable task. Periodically there is a disconnection from the server. Reconnecting is easy. But if you remember that at this time, messages from both the client and the server continue to go, everything becomes immediately and much more complicated. In general, a message may be lost if a mechanism for acknowledging a received message is not provided, or delivered repeatedly (even many times) if a confirmation mechanism is provided for, but the failure occurred just after it was received and before the message was acknowledged.
If you need guaranteed delivery of messages and / or delivery of messages without duplicates, then there are special protocols for implementation of this, for example AMQP and MQTT, which work with the websocket transport. But today we will not consider them.
Most websocket libraries support a programmer-transparent connection to the server. Using such a library is always more reliable than developing your own implementation.
Next, you need to implement the infrastructure for sending and receiving asynchronous messages. To use for this "bare" event handler onmessage without additional binding ungrateful occupation. Such an infrastructure can be, for example, remote procedure call (RPC). The json-rpc specification, specifically for working with the websocket transport, introduced an id, which allows you to match the remote procedure call by the client with a response message from the web server. I would prefer this protocol to all other possibilities, however 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 a disconnect between the client and the server periodically occurs. If we have a little power of one server, we can raise several more servers. In this case, after breaking the connection, 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 still run into system performance, since the node.js capabilities by the number of simultaneously open websocket connections (this should not be confused with speed) are significantly lower than specialized servers such as message queues and brokers. And the need for cross-sharing between all instances of websocket servers across a cluster of redis servers, 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, for example AMQP and MQTT, which work, including with the websocket transport. But today we will not consider them.
As you can see from the list of listed tasks, it’s extremely hard to work with when working with websocket, and even impossible if you need to scale the solution to multiple 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 solely fallback to obsolete modes of transport, since today this functionality is not relevant, and libraries that implement broader functionality tend to implement fallback to obsolete modes of transport.
I'll start with the most popular library - socket.io. Now you can hear the opinion, most likely a fair one, that this library is slow and expensive in terms of resources. Most likely it is, and it works slower than the native websocket. However, today it is the most developed library by its means. And, once again, when working with websocket, the main deterrent is not speed, but the number of simultaneously open connections with unique clients. And this question is better solved already by making client connections to specialized servers.
So, soket.io implements reliable recovery when the connection to the server is broken and scaling using the server or the redis server cluster. socket.io, in fact, implements its own individual messaging protocol, which allows you to implement the exchange of messages between the client and the server without reference to a specific programming language.
An interesting possibility of socket.io is confirmation of event handling, in which an arbitrary object can be returned from the server to the client, which allows for the implementation of a remote procedure call (although it does not comply with the json-rpc standard).
Also, I tentatively considered two more interesting libraries, which I will briefly discuss below.
Library faye
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. Attempting to find a way to implement RPC did not succeed, as 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 a cluster was deployed, it was necessary to launch a rather complex infrastructure of brokers and workers.
We now turn to the implementation of RPC on socket.io. As I said above, this library already has the ability to exchange objects between the client and the 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 consider each of the parts in relation to a specific application. To build the admin, I used the react-admin
github.com/marmelab/react-admin library. Data exchange with the server in this library is implemented using a data provider, which has a very convenient scheme, almost a peculiar standard. For example, to get the list, the method is called:
dataProvider( 'GET_LIST', ' ', { pagination: { page: {int}, perPage: {int} }, sort: { field: {string}, order: {string} }, filter: { Object } }
This method returns an object in an asynchronous response:
{ data: [ ], total: }
Currently there is an impressive number of implementations of react-admin data providers for various servers and frameworks (for example, firebase, spring boot, graphql, etc.). In the case of RPC, the implementation turned out to be the most concise, since the object is transmitted 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, on the server side had to do a little more work. To organize the comparison of functions that handle a remote call, a router was developed, similar to the express.js router. Only instead of the middleware signature (req, res, next), 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 to the Admin component:
import React from 'react'; import { Admin, Resource, EditGuesser } from 'react-admin'; import UserList from './UserList'; import dataProvider from './wsProvider'; const App = () => <Admin dataProvider={dataProvider}> <Resource name="users" list={UserList} edit={EditGuesser} /> </Admin>; export default App;
useful links
1.
www.infoq.com/articles/Web-Sockets-Proxy-Serversapapacy@gmail.com
July 14, 2019