Реализуется достаточно просто.
У вас один nodejs процесс слушает TCP порт 80. Браузер может создавать множество соединений с сервером, как короткоживущих, вроде GET/POST, так и долговременных, как WebSockets.
Websockets используют механизм апгрейда соединения, суть которого заключается в изменении самого протокола верхнего уровня при установлении TCP соединения. Простыми словами, сначала веб-сокеты цепляются по обычному HTTP, потом шлют заголовки
Upgrade: WebSocket
и
Connection: Upgrade
, после чего штаны (http) превращаются в элегантные брюки (websockets).
Поэтому один и тот же сервер может обслуживать и веб-сокеты и http. На этом заканчивается магия одновременных соединений со стороны браузере с одним и тем же сервером по разным протоколам. Дальше начинается магия того, как передаются данные с одного протокола (http) в веб-сокет.
Поскольку чат работает внутри одного процесса (это я нарочно упрощаю), то у процесса есть разделяемый
обработчик событий. Через него сообщения пересылаются между обработчиками разных протоколов.
В коде очень упрощенно это выглядит так:
// импортируем зависимости
import app from 'express';
import { createServer } from 'http';
import socketio from 'socket.io';
// наш http сервер
const app = express();
const http = createServer(app);
// наш websocket сервер
const io = socketio(http);
// глобальная шина сообщений
const messageBus = new EventEmitter();
// так мы получаем сообщение по http
app.get('/message', (req, res) => {
console.log('http message:', req.params);
// и пихаем его дальше по шине сообщений
messageBus.emit('message', req.params);
})
// когда установлено соединение,
// мы можем настроить подписку на сообщения
io.on('connection', (socket) => {
// соединение установлено!
console.log('Connected!');
// так мы подписываемся на сообщение
// приходящее через вебсокеты
socket.on('message', (message) => {
console.log('websockets message:', message);
// и шлем его дальше по шине сообщений
messageBus.emit('message', message);
});
// теперь мы подписываемся на любые сообщения
// вне зависимости от того, откуда они были получены,
// и пересылаем их всем через вебсокеты
messageBus.on('message', (message) => {
io.emit('message', message);
});
});
// слушаем по http порт 3000
http.listen(3000, () => {
console.log('запустились на порту 3000');
});
В реальной жизни вместо разделяемого объекта EventEmitter используется реальная шина сообщений на основе Redis Pub/Sub или очередей, вроде RabbitMQ. Такие штуки делаются для очень больших чатов, способных держать десятки и сотни тысяч человек онлайн. Например чаты в масштабах Youtube, Twitch и т.д.
Для небольших чатов до несколько десятков тысяч пользователей вполне достаточно одного хорошо написанного сервера на Go.
Nodejs не подходит для высоконагруженных проектов ввиду очевидно высокого уровня потребления ресурсов.