Long polling
Это способ, при котором мы с клиента отправляем запрос на сервер, который висит ровно до тех пор, пока сервер не ответит на него. Если время ожидания закончилось, то мы с клиента отправляем запрос заново.
Если сервер нам что-нибудь вернёт, то мы опять отправляем запрос на получение данных с сервера.
Этот способ самый простой и требует от нас просто постоянно запрашивать данные с клиента на сервер
Тут пользователь отправляет get-запрос, но мы не возвращаем ему ответ (ответ мы забиндили в событие). Взамен мы ждём, когда другой участник чата отправит сообщение и уже только после этого событие в get-запросе вызываем, после чего всем участникам чата возвращается ответ
На сервере мы имеем:
- пост-запрос, который вызывает функцию внутри гет-запроса
- гет-запрос, который хранит в себе ивент, срабатываемый, когда на сервер отправляют запрос с данными
const express = require('express');
const cors = require('cors');
const events = require('events');
const PORT = 5000;
// инициализируем эмиттер событий
const emitter = new events.EventEmitter();
const app = express();
app.use(cors());
app.use(express.json());
// будет возвращать новые сообшения
app.get('/get-messages', (req, res) => {
// если пользователь отправил сообщение, то остальных пользователей чата нужно осведомить о доставке сообщения
emitter.once('newMessage', (message) => {
// всем пользователям, у кого висит подключение, отправяем сообщение
res.json(message);
});
});
// будет
app.post('/new-messages', (req, res) => {
const message = req.body;
// тут мы вывзаем событие из get, после того, как мы получили новое сообщение
emitter.emit('newMessage', message);
res.status(200);
});
// прослушиваем порт
app.listen(PORT, () => console.log(`server started on PORT ${PORT}`));
На клиенте нам нужно написать функцию, которая будет отправлять постоянно запрос на получение данных subscribe()
и функцию, которая будет отправлять эти данные sendMessage()
. Уже только тогда после отправки сообщения триггернётся гет на сервере и отдаст сообщения
import React, { useEffect, useState } from 'react';
import './styles.css';
import axios from 'axios';
const LongPulling = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
useEffect(() => {
subscribe();
}, []);
const subscribe = async () => {
try {
// тут мы получаем ответ от сервера и связь пропадает
const { data } = await axios.get('http://localhost:5000/get-messages');
setMessages((prev) => [data, ...prev]);
// тут мы переоформляем подписку, чтобы связь не пропадала
await subscribe();
} catch (e) {
// если произойдёт ошибка, то нам нужно будет просто переоформить подписку
setTimeout(() => {
subscribe();
}, 500);
}
};
const sendMessage = async () => {
await axios.post('http://localhost:5000/new-messages', {
message: value,
id: Date.now(),
});
};
return (
<div>
<div>
<h2>LongPulling</h2>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div className='message' key={mess.id}>
{mess.message}
</div>
))}
</div>
</div>
</div>
);
};
export default LongPulling;
И теперь в двух разных браузерах мы реализовали чат в реальном времени
event soursing (server sent events)
Второй варинат взаимодействия - это event soursing, который подразумевает под собой, чтобы было установлено постоянно одностороннее соединение с сервера к клиенту.
Клиент только получает ответы от сервера на изменённые данные и больше ничего не происходит.
Строится данный подход на базе обычного https
На сервере мы создаём специальный заголовок, который определит, что наши запросы имеют постоянный характер и создаём многоповторный ивент, который будет записывать ответ специальным образом
const express = require('express');
const cors = require('cors');
const events = require('events');
const PORT = 5000;
const emitter = new events.EventEmitter();
const app = express();
app.use(cors());
app.use(express.json());
app.get('/connect', (req, res) => {
// тут мы задаём заголовок, что связь у нас будет постоянная
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
// этот же ивент может срабатывать множество раз, поэтому меняем once на on
emitter.on('newMessage', (message) => {
// тут мы обязательно оборачиваем строку в такой шаблон, чтобы она принялась классом EventSource
res.write(`data: ${JSON.stringify(message)} \n\n`);
});
});
app.post('/new-messages', (req, res) => {
const message = req.body;
emitter.emit('newMessage', message);
res.status(200);
});
app.listen(PORT, () => console.log(`server started on PORT ${PORT}`));
На клиенте нужно поменять функцию subscribe()
, которая будет работать с классом EventSource
, который, в свою очередь, уже будет отслеживать получение сообщений
import React, { useEffect, useState } from 'react';
import './styles.css';
import axios from 'axios';
const EventSourcing = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
useEffect(() => {
subscribe();
}, []);
const subscribe = async () => {
// создаём ивентсурс с ссылкой на коннекшн-контроллер
const eventSource = new EventSource(`http://localhost:5000/connect`);
// тут мы каждый раз при получении сообщения выполняем действие
eventSource.onmessage = function (event) {
const message = JSON.parse(event.data);
setMessages((prev) => [message, ...prev]);
};
};
const sendMessage = async () => {
await axios.post('http://localhost:5000/new-messages', {
message: value,
id: Date.now(),
});
};
return (
<div>
<div>
<h2>EventSourcing</h2>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div className='message' key={mess.id}>
{mess.message}
</div>
))}
</div>
</div>
</div>
);
};
export default EventSourcing;
И наш чат всё так же продолжает работать, но теперь посредством передачи сырых данных
WebSockets
Вебсокеты - это технология, которая позволяет устанавливать между клиентом и сервером постоянную (дуплексную) связь для обмена данными
Вебсокеты используются, например, в чатах для того, чтобы сообщение приходило сразу без потребности в перезагрузке страницы. Или в новостях, чтобы их быстро обновлять.
Это самый мощный способ организовать взаимодействие между клиентом и сервером и требует поднятия отдельного вебсокет-сервера
Изначально нам нужно поднять отдельный сервер вебсокетов
Далее мы описываем события, при которых будут срабатывать сокеты.
Изначально WebSocket создаётся для одного человека на одно подключение и сообщение по-умолчанию будет передаваться только самому пользователю. Чтобы оно отправлялось сразу обоим людям в чате, нужно создать широковещатель broadcastMessage()
.
const ws = require('ws');
// запускаем сервер вебсокетов
const wss = new ws.Server(
{
port: 5000,
},
() => console.log(`Server started on 5000`)
);
// при подлючении сокета
wss.on('connection', function connection(ws) {
// при отпрвке сообщения
ws.on('message', function (message) {
// мы получаем сообщение с клиента
message = JSON.parse(message);
// и при разных событиях в сообщении (есть событие подключения и просто отправки сообщения), будем выполнять "разные" действия
switch (message.event) {
case 'message':
broadcastMessage(message);
break;
case 'connection':
broadcastMessage(message);
break;
}
});
});
// распространяем сообщение по пользователям
function broadcastMessage(message, id) {
// перебираем всех клиентов
wss.clients.forEach((client) => {
// каждый клиент является вебсокетом и можно каждому отправить сообщение
client.send(JSON.stringify(message));
});
}
На клиенте нужно уже будет описать подключение к сокетам и описать реакции сокета на его разные состояния (ошибка, подключение и так далее)
import React, { useEffect, useRef, useState } from 'react';
import axios from 'axios';
const WebSock = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
const [connected, setConnected] = useState(false);
const [username, setUsername] = useState('');
// чтобы не потерять сокет при перерендере, присваиваем его в реф
const socket = useRef();
function connect() {
// присваиваем сюда сокет
socket.current = new WebSocket('ws://localhost:5000');
// при открытии сокета
socket.current.onopen = () => {
setConnected(true);
// сообщение о подключении пользователя к сокетам
const message = {
event: 'connection',
username,
id: Date.now(),
};
// отправит сообщение на сервер
socket.current.send(JSON.stringify(message));
console.log('Socket подключен');
};
// при получении сообщения от сокета
socket.current.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [message, ...prev]);
};
// при закрытии сокета
socket.current.onclose = () => {
console.log('Socket закрыт');
};
// при ошибке в сокете
socket.current.onerror = () => {
console.log('Socket произошла ошибка');
};
}
const sendMessage = async () => {
const message = {
username,
message: value,
id: Date.now(),
event: 'message',
};
socket.current.send(JSON.stringify(message));
setValue('');
};
if (!connected) {
return (
<div className='center'>
<div className='form'>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type='text'
placeholder='Введите ваше имя'
/>
<button onClick={connect}>Войти</button>
</div>
</div>
);
}
return (
<div className='center'>
<div>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div key={mess.id}>
{mess.event === 'connection' ? (
<div className='connection_message'>
Пользователь {mess.username} подключился
</div>
) : (
<div className='message'>
{mess.username}. {mess.message}
</div>
)}
</div>
))}
</div>
</div>
</div>
);
};
export default WebSock;