предисловие
В последнее время, в части связи в реальном времени при построении двух систем, подытожу то, что я узнал.
Это серия статей, в основном задуманных в настоящее время в четырех частях.
- Объясните Websocket простыми словами (1) Протокол Websocket
- Объясните Websocket простыми словами (2) Распределенный кластер Websocket
- Подробное объяснение Websocket (три) Websocket подканала (анализ исходного кода socket.io и ws-wrapper)
текст
Вот простая диаграмма архитектуры игрушки, которую я создаю. Часть связи в реальном времени извлекается как узел Websocket для формирования простой распределенной системы, а затем связь между кластерами Websocket и связь между узлом Websocket и узлом Restful API осуществляется через Pub/Sub Redis (например, как пользователь, вызывающий узел Restful API).После того, как API публикует статью, он уведомляет Websocket о необходимости передать красную точку нового сообщения во внешний интерфейс).
Яма распределенных системЛевоухая мышь: Из практики Amazon рассказ о сложностях распределенных систем
В этой статье в основном представлено распределенное кластерное решение Websocket, и, наконец, будет запущенная демонстрация.
Распределенное кластерное решение Websocket
В этом блоге мы в конечном итоге хотим создать WebsocketкластерДля общения с клиентами в режиме реального времени, например, в чатах. Конечно, мы можем построить его с помощью простой демонстрации.ОдинСервер Websocket и разрешить всем клиентам подключаться к этой машине, но что, если количество взаимодействий в этом чате очень велико? Например, в прямом эфире Douyu я пошел в Douyu, чтобы увидеть запрос, и из именования мы видим, что он установил ws-соединение, называемоеdanmuproxy.douyu.com
,Как показано ниже.
Итак, вопрос в том, если я использую только один сервер, как я могу поддерживать этот чат, к которому одновременно могут присоединиться 100 000 человек? Очевидно, нам нужно решение типаСбалансируйте нагрузку трафика на разные серверы и предоставьте каждому серверу механизм связи для синхронизации сообщений.(В противном случае пользователь A подключен к серверу A, а пользователь B сталкивается с сервером B. Когда они отправляют сообщения, другая сторона не может их получить).
На самом деле, из имени на картинке выше вы знаете, что Доую связан с этим.danmuproxy.douyu.com
серединаproxy
Можно грубо предположить, что они также распределяли трафик.
Кластер веб-сокетов
Поскольку это отличается от балансировки нагрузки обычных HTTP-серверов, в предыдущем разделе также упоминалось, что этим серверам Websocket необходимо обмениваться информацией (конечно, то же самое верно и для серверов, которым необходимо выполнять совместное использование сеансов). Это означает, что взаимодействие клиента с сервером Websocketсостояние(с сохранением состояния), нам нужно хранить в памяти данные подключения каждого клиента. И когда мы хотим достичьраспределенный, нам нужно поделиться этой информацией между машинами, поэтому нам нуженPublish/Subscribe broker(На самом деле брокер выучил это, когда пошел в школу, чтобы преподавать архитектуру проектирования программного обеспечения, но в то время это было слишком ново для понимания). Вот пример.
Предположим, что теперь мы используем Redis в качестве нашего решения, тогда у нас теперь есть три сервера Websocket.WS1
,WS2
иWS3
. Затем к каждому серверу подключались по три пользователя.WS1
Один из пользователей на машине отправляет сообщение в чат. В логике вашего сервера Websocket вы сначала сохраняете сообщение в базе данных для сохранения (например, исторические сообщения), а затем сохраняете сообщение в соответствии с идентификатором канала. вещи проталкиваются в канал этого чата (реализация канала Websocket будет подробно описана в следующей статье), мы предполагаем, что этот channelId называется "☆Мир".
Теперь у вас есть данные, надежно сохраненные в БД, и вы публикуете событие в своемPub/Sub broker(канал Redis), чтобы уведомить другие заинтересованные стороны (другие серверы Websocket или API и т. д.). Таким образом, два других сервера доWS2
иWS3
Поскольку они заинтересованы в этой части, они также прослушивают этот канал Redis через сценарий, они будут уведомлены, а затем каждый сервер будет запрашивать запрос к БД, чтобы получить обновление, а затем отправить сообщение на соответствующий канал на Веб-сокет.
Это то, что вы можете увидеть, используя Pub/Sub Brooker для реализации масштабируемого кластера Websocket.
Отсюда вы также можете увидеть преимущества кластеризации, высокой масштабируемости и высокой доступности.
выполнить
В этой реализации используется один из моих высокопроизводительных внутренних серверов Alibaba Cloud, относительно недорогой студенческий сервер Alibaba Cloud за 9 юаней и Redis на высокопроизводительном сервере.
Балансировка нагрузки Nginx
Сначала настройте Nginx для балансировки нагрузки.На следующем рисунке показана моя конфигурация, но это всего лишь демонстрация, которая не связана с wss.
Реализация на стороне сервера
код находится вgithubначальство.
Демонстрационный код тоже очень короткий
const WebSocket = require('ws');
const publicIp = require('public-ip');
const uuidv1 = require('uuid/v1');
const redis = require("redis");
const config = require('./config');
const sub = redis.createClient(config.DB.REDIS_PORT, config.DB.REDIS_HOST);
const pub = redis.createClient(config.DB.REDIS_PORT, config.DB.REDIS_HOST);
if (config.DB.REDIS_PASSWORD) {
sub.auth(config.DB.REDIS_PASSWORD);
pub.auth(config.DB.REDIS_PASSWORD);
}
const wss = new WebSocket.Server({ port: 2333 });
const ip2name = {
'47.94.233.234': '梁王的高配据点',
'115.28.68.89': '梁王的9块服务器',
}
let sockets = {};
wss.on('connection', function connection(ws) {
const uuid = uuidv1();
ws.uuid = uuid;
sockets[uuid] = ws;
ws.on('message', function incoming(message) {
// publish消息给其他服务器
pub.publish('channel', `${ws.uuid}>${message}`);
console.log(`publish to channel: ${ws.uuid}>${message}`)
// 向本服务器的socket广播
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(`来自${ws.from || '???'}的用户${ws.uuid}发送了: ${message}`);
}
});
});
publicIp.v4().then(ip => {
console.log(ip);
ws.from = ip2name[ip] ? ip2name[ip] : '未知';
ws.send(`你连接的服务器为${ws.from}`);
});
});
// 监听其他服务器发送的消息
sub.on('message', function(channel, message) {
console.log(`channel ${channel}, ${message}`)
if (channel == 'channel')
{
var messageArr = message.split('>');
var uuid = messageArr[0]
var wsFrom = sockets[uuid];
var content = messageArr[1];
// 如果socket是非本服务器的
if(!wsFrom) {
wss.clients.forEach(function each(client) {
client.send(`来自其他服务器的用户${uuid}发送了: ${content}`);
});
}
}
});
sub.subscribe('channel');
Эффект
Вы можете попробовать это в консоли со следующим кодом, сервер может быть отключен позже.
var socket = new WebSocket('ws://websocket-demo.lwio.me');
// Listen for messages
socket.addEventListener('message', function (event) {
console.log('收到了', event.data);
});
// socket.send('keke')
постскриптум
Обновление от 1 апреля, Ма Йе, Alibaba Cloud сегодня звонила в полицию.Вы можете видеть, что мой Redis напрямую открыт для общедоступной сети, и это даст мне волну, верно? После учебы я склонил голову перед старшим братом Синьанем.
Использованная литература: