📜 ⬆️ ⬇️

WebSockets in Scorocode or chat with your own hands in 15 minutes



We recently added WebSockets support to our backend as a service Scorocode . Now you can fully use this technology when creating applications that require a safe and universal way to transfer data.

In this article I will talk about the internal implementation, with what they faced, how they solved the problems, and also give an example of a simple application written using technology and our service.
')
Details under the cut.

Architecture


When planning the architecture, we needed to achieve the possibility of horizontal scaling by simply adding machines to the cluster. Mainly two schemes were considered:

1. Nodes use a common broker


In this scheme, we can deploy an unlimited number of nodes that will use a common message broker. Redis can act as a broker.

Pros:

The main plus, as I see it, is that we don’t need to reinvent our bicycle for messaging between nodes, install Redis, connect, subscribe to the channel and work.

Minuses:

If everything is clear with scaling the nodes themselves - it is enough just to add additional machines, then with Redis everything is a bit more complicated. Sooner or later we will reach the Redis throughput limit, and we will have to think about the broker scaling and resiliency. In any case, this would entail the complication of the overall architecture.

2. Nodes have a common bus for exchanging system messages.


In this scheme, we refuse to use additional software, and implement communication between our application instances via a common bus. In this form, our nodes form a single cluster.

Pros:

We do not need additional dependencies in the form of separate software, simplifies the architecture and support of the entire infrastructure.

Minuses:

We will have to invent our own protocol for exchanging system messages between nodes, implement reconnection when the connection is broken, and much more.

Choice is made


Taking into account that during the replication of Redis, we will get approximately a similar scheme with additional load on the network when exchanging messages between replicas, we decided to abandon this option and implement the architecture based on the second scheme.

For messaging between nodes, we decided to use ZeroMQ or Nanomsg . These libraries are a high-level abstraction for exchanging messages between processes, nodes, clusters, applications. You do not need to worry about connection status, error handling, etc. All this is already implemented inside. We settled on Nanomsg.

Load balancing is done using Nginx. When a client connects, the balancer redirects it to one of the microservices, which interacts with the others, forming a single cluster.

Tests have shown that this scheme perfectly copes with the tasks.

As a result, we received:

1) Separate microservice for work with WebSocket written on Go.
2) Simple scaling by adding nodes.
3) No dependencies.

WebSocket usage example


One of the most common examples of using WebSocket is chat. Below will be described an example of creating a simple chat using Scorocode , React and WebSockets.

Our chat page:

<!doctype html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>My Chat</title> <link rel="stylesheet" type="text/css" href="dist/bundle.css"> </head> <body> <div id="app"></div> <script src="dist/bundle.js"></script> </body> </html> 

We divide our chat into three components: the chat frame, the list of participants and the chat history.

Let's start with the chat frame:

appView.js
 import React from 'react'; import UserList from './../components/userList' import History from './../components/history' //  SDK import Scorocode from './../scorocode.min' //  SDK Scorocode.Init({ ApplicationID: '<appId>', WebSocketKey: '<websocketKey>', JavaScriptKey: '<javascriptKey>' }); var WS = new Scorocode.WebSocket('scorochat'); class AppView extends React.Component{ constructor () { super(); this.state = { userList: {}, user: { id: '', name: '' }, history: [] }; } guid () { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } onOpen () { setInterval(() => { this.getUserList(); }, 10000); this.getUserList (); } onError (err) {} onClose () {} updateUserList (user) { let now = new Date().getTime(); let userList = this.state.userList; if (!userList[user.id]) { userList[user.id] = { name: user.name }; } userList[user.id].expire = now; for (let id in userList) { if (now - userList[id].expire > 10000) { delete userList[id]; } } this.setState({ userList: userList }); } getUserList () { var data = JSON.stringify({ cmd: 'getUserList', from: this.state.user, text: '' }); WS.send(data); } onMessage (data) { var result = JSON.parse(data); switch (result.cmd) { case 'message': let history = this.state.history.slice(); history.push(result); this.setState({history: history}); break; case 'getUserList': WS.send(JSON.stringify({ cmd: 'userList', from: this.state.user, text: '' })); break; case 'userList': this.updateUserList(result.from); break } } send (msg) { var data = JSON.stringify({ cmd: 'message', from: this.state.user, text: msg }); WS.send(data); } keyPressHandle(ev) { let value = ev.target.value; if (ev.charCode === 13 && !ev.shiftKey) { ev.preventDefault(); if (!ev.target.value) { return; } this.send(value); ev.target.value = ''; } } componentWillMount () { let userName = prompt('  ?'); userName = (userName || 'New User').substr(0, 30); this.setState({ user: { name: userName, id: this.guid() } }); } componentDidMount () { //    WS.on("open", this.onOpen.bind(this)); WS.on("close", this.onClose.bind(this)); WS.on("error", this.onError.bind(this)); WS.on("message", this.onMessage.bind(this)); } render () { return ( <div className="viewport"> <div className="header"> <h1>ScoroChat</h1> </div> <div className="main"> <div className="left_panel"> <UserList userList={this.state.userList}/> </div> <div className="content"> <div className="history"> <History history={this.state.history} /> </div> <div className="control"> <div className="container"> <textarea placeholder=" " onKeyPress={this.keyPressHandle.bind(this)}></textarea> </div> </div> </div> </div> </div> ) } } export default AppView; 


A list of users:

userList.js
 import React from "react"; var avatar = require('./../../img/avatar.png'); export default class UserList extends React.Component{ constructor(props){ super(props); } render () { const historyIds = Object.keys(this.props.userList); return ( <div id="members"> {historyIds.map((id) => { return ( <div className='userList' key={id}> <div className='userList_avatar'> <img src=http://{avatar} /> </div> <div className='userList_info'> <span>{this.props.userList[id].name}</span> </div> </div> ) })} </div> ) } } 


And the component that displays the history of correspondence:

history.js
 import React from 'react' var avatar = require('./../../img/avatar.png'); class History extends React.Component { constructor(props) { super(props); } getDate () { let dt = new Date(); return ('0' + dt.getHours()).slice(-2) + ':' + ('0' + dt.getMinutes()).slice(-2) + ' ' + ('0' + dt.getDate()).slice(-2) + '.' + ('0' + (dt.getMonth() + 1)).slice(-2) + '.' + dt.getFullYear(); } render () { return ( <div id="msgContainer" className="container"> {this.props.history.map((item, ind) => { return ( <div className="msg_container" key={ind}> <div className="avatar"> <img src=http://{avatar} /> </div> <div className="msg_content"> <div className="title"> <a className="author" href="javascript:void(0)">{item.from.name}</a> <span>{this.getDate()}</span> </div> <div className="msg_body"> <p>{item.text}</p> </div> </div> </div> ) })} </div> ); } componentDidUpdate() { var historyContainer = document.getElementsByClassName('history')[0]; var msgContainer = document.getElementById('msgContainer'); //   if (msgContainer.offsetHeight - (historyContainer.scrollTop + historyContainer.offsetHeight) < 200) { historyContainer.scrollTop = msgContainer.offsetHeight - historyContainer.offsetHeight; } } } export default History; 


The scope of WebSockets is quite wide. It can be real-time content updates, distributed computing, frontend interaction with the API, various interactive services and much more. Given the fairly simple integration with the Scorocode platform, developers can not waste time implementing server logic, but concentrate on other parts of the application.

Demo: Link
Sources: Link

Source: https://habr.com/ru/post/308068/


All Articles