Pull to refresh
0
Scorocode
Готовый облачный backend для любых приложений

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

Reading time 6 min
Views 8.8K


Недавно мы добавили поддержку 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, разработчики могут не тратить время на реализацию серверной логики, а сконцентрироваться на других частях приложения.

Демо: Ссылка
Исходники: Ссылка
Tags:
Hubs:
+3
Comments 14
Comments Comments 14

Articles

Information

Website
scorocode.ru
Registered
Employees
11–30 employees
Location
Россия