WebSockets в Scorocode або чат своїми руками за 15 хвилин



Нещодавно ми додали підтримку WebSockets розробляється нами backend as a service Scorocode. Тепер ви можете повноцінно використовувати цю технологію при створенні додатків, що вимагають безпечного і універсального способу передачі даних.

У цій статті я розповім про внутрішню реалізації, з чим зіткнулися, як вирішували проблеми, а також наведу приклад простої програми, написаного з використанням технології і нашого сервісу.

Подробиці під катом.

Архітектура
При плануванні архітектури нам необхідно було домогтися можливості горизонтального масштабування простим додаванням машин в кластер. В основному розглядалися дві схеми:

1. Ноди використовують загальний брокер
В даній схемі ми можемо розгорнути необмежену кількість нод, які будуть використовувати загальний брокер повідомлень. В якості брокера може виступати Redis.

Плюси:

Основний плюс, як мені бачиться, в тому, що нам не потрібно винаходити свій велосипед для обміну повідомленнями між нодами, встановлюємо Redis, підключаємося, підписуємося на канал і працюємо.

Мінуси:

Якщо з масштабуванням самих нод все зрозуміло — досить просто додати додаткові машини, то з Redis все трохи складніше. Рано чи пізно ми досягнемо межі пропускної здатності Redis, і нам доведеться замислюватися і про масштабуванні самого брокера, і про відмовостійкості. У будь-якому випадку це спричинить за собою ускладнення загальної архітектури.

2. Ноди мають загальну шину для обміну системними повідомленнями
В даній схемі ми відмовляємося від використання додаткового ПЗ, і реалізовуємо спілкування між нашими примірниками програми через загальну шину. У такому вигляді наші ноди утворюють єдиний кластер.

Плюси:

Нам не потрібні додаткові залежності у вигляді окремого, спрощується архітектура і підтримка всієї інфраструктури.

Мінуси:

Нам доведеться винаходити свій протокол обміну системними повідомленнями між нодами, реалізовувати перепідключення при обриві з'єднання і багато іншого.

Вибір зроблений
Враховуючи, що при реплікації Redis ми отримаємо приблизно схожу схему з додатковим навантаженням у мережі при обміні повідомленнями між репліками, ми вирішили відмовитися від цього варіанту і реалізувати архітектуру на основі другої схеми.

Для обміну повідомленнями між нодами вирішили використовувати ZeroMQ або Nanomsg. Дані бібліотеки представляють з себе високорівневу абстракцію для обміну повідомленнями між процесами, нодами, кластерами, додатками. При цьому вам не потрібно турбуватися за стан з'єднання, обробку помилок і т. д. Все це вже реалізовано всередині. Ми зупинилися на Nanomsg.

Балансування навантаження здійснюємо з допомогою Nginx. При підключенні клієнта балансувальник перенаправляє його на один з микросервисов, який взаємодіє з іншими, утворюючи єдиний кластер.

Тести показали, що дана схема чудово справляється з поставленими завданнями.

В результаті ми отримали:

1) Окремий микросервис для роботи з WebSocket написаний на Go.
2) Просте масштабування додаванням нсд.
3) Відсутність залежностей.

Приклад використання WebSocket
Один з найпоширеніших прикладів використання WebSocket — чат. Нижче буде описано приклад створення найпростішого чату, з використанням Scorocode, React і WebSockets.

Наша сторінка чату:

<!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>

Розіб'ємо наш чат на три складові: каркас чату, список учасників та історія чату.

Почнемо з каркаса чату:

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;


Список користувачів:

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>
)
}
}


І компонент, що відображає історію листування:

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;


Область застосування WebSockets досить широка. Це може бути і оновлення контенту в режимі реального часу, розподілені обчислення, взаємодія frontend'а c API, різні інтерактивні сервіси та багато іншого. З урахуванням досить простий інтеграції з платформою Scorocode, розробники можуть не витрачати час на реалізацію внутрішньої логіки, а концентруватися на інших частинах програми.

Демо: Посилання
Джерело: Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.