Объясните Websocket простыми словами (2) Распределенный кластер Websocket

Redis внешний интерфейс сервер WebSocket

предисловие

В последнее время, в части связи в реальном времени при построении двух систем, подытожу то, что я узнал.

Это серия статей, в основном задуманных в настоящее время в четырех частях.

текст

Вот простая диаграмма архитектуры игрушки, которую я создаю. Часть связи в реальном времени извлекается как узел 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 напрямую открыт для общедоступной сети, и это даст мне волну, верно? После учебы я склонил голову перед старшим братом Синьанем.

Использованная литература:

Scaling WebSockets