Теория
Веб-сокеты - это протокол для двунаправленной связи между клиентом и сервером через интернет. Они позволяют установить постоянное соединение между браузером и сервером, который может использоваться для передачи данных в режиме реального времени.
Основные методы, используемые в веб-сокетах, включают:
WebSocket()
: конструктор объекта WebSocket, который создает новый экземпляр веб-сокета.send()
: метод, который отправляет данные на сервер через веб-сокет.close()
: метод, который закрывает веб-сокет.onopen()
: событие, которое происходит при открытии соединения с сервером.onmessage()
: событие, которое происходит при получении сообщения от сервера.onerror()
: событие, которое происходит при возникновении ошибки во время работы с веб-сокетами.
При использовании веб-сокетов, клиент и сервер устанавливают постоянное соединение. Как только соединение установлено, данные можно передавать в обоих направлениях без необходимости повторного подключения или перезагрузки страницы.
При передаче данных через веб-сокеты, данные могут быть закодированы в различных форматах, таких как JSON, XML или бинарный формат. Веб-сокеты также позволяют отправлять и получать файлы, аудио и видео потоки и другие типы данных в режиме реального времени.
Реализация
На стороне сервера создается объект rooms
, который содержит список всех комнат и пользователей в каждой комнате. При создании новой комнаты сервер проверяет, существует ли уже комната с таким же именем, и отправляет сообщение об ошибке, если комната уже существует. Если комната не существует, сервер создает новую комнату и добавляет создателя комнаты в список ее участников.
В обработчике для присоединения к комнате сервер проверяет, существует ли комната с указанным именем, и отправляет сообщение об ошибке, если комната не существует. Если комната существует, сервер добавляет пользователя в список ее участников и отправляет всем участникам комнаты сообщение о том, что новый пользователь присоединился к комнате.
Обработчик для отправки сообщения в комнату просто передает полученные данные всем участникам комнаты, используя метод io.to(roomName).emit()
.
Обработчик для отключения пользователя от чата удаляет пользователя из всех комнат и отправляет сообщение об уходе всем участникам соответствующих комнат.
server.js
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
// список комнат и пользователей в каждой комнате
const rooms = {};
app.use(express.static(__dirname + '/public'));
io.on('connection', socket => {
console.log('User connected');
// обработчик для создания комнаты
socket.on('create_room', roomName => {
console.log('Creating room', roomName);
if (!rooms[roomName]) {
// если комнаты не существует - создаем новую
rooms[roomName] = [socket.id];
socket.join(roomName);
socket.emit('created_room', { roomName, participants: [] });
} else {
// иначе отправляем сообщение об ошибке
socket.emit('error', 'Room already exists');
}
});
// обработчик для присоединения к комнате
socket.on('join_room', ({ roomName, username }) => {
console.log(`${username} is joining ${roomName}`);
if (rooms[roomName]) {
// если комната существует - добавляем пользователя в нее
rooms[roomName].push(socket.id);
socket.join(roomName);
const participants = rooms[roomName].map(id =>
io.sockets.connected[id].username
);
io.to(roomName).emit('joined_room', { roomName, participants });
socket.emit('room_joined', { roomName, username });
} else {
// иначе отправляем сообщение об ошибке
socket.emit('error', 'Room does not exist');
}
});
// обработчик для отправки сообщения в комнату
socket.on('send_message', ({ roomName, message }) => {
console.log(`${socket.username} sent a message to ${roomName}: ${message}`);
io.to(roomName).emit('new_message', { roomName, message, sender: socket.username });
});
// обработчик для отключения пользователя от чата
socket.on('disconnect', () => {
console.log(`User disconnected: ${socket.username}`);
// удаляем пользователя из всех комнат, в которых он находился
Object.keys(rooms).forEach(roomName => {
if (rooms[roomName].includes(socket.id)) {
rooms[roomName] = rooms[roomName].filter(id => id !== socket.id);
const participants = rooms[roomName].map(id =>
io.sockets.connected[id].username
);
io.to(roomName).emit('user_left', { roomName, participants, username: socket.username });
}
});
});
// добавляем имя пользователя к сокету
socket.on('set_username', username => {
socket.username = username;
console.log(`Username set: ${socket.username}`);
});
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Клиентский код использует две основные функции - joinRoom()
для присоединения к комнате и switchRoom()
для переключения на новую комнату. Когда пользователь присоединяется к комнате, он отправляет на сервер сообщение join_room
с именем комнаты и своим именем пользователя. Сервер добавляет пользователя в список участников комнаты и отправляет всем участникам комнаты сообщение joined_room
с списком всех участников. Каждый раз, когда пользователь отправляет сообщение в комнату, клиентский код отправляет на сервер сообщение send_message
, которое содержит текст сообщения и имя текущей комнаты. Сервер затем отправляет всем участникам комнаты сообщение new_message
, которое содержит текст сообщения, имя отправителя и имя текущей комнаты.
Когда пользователь покидает комнату или отключается от чата, клиент отправляет на сервер сообщение disconnect
, который удаляет пользователя из всех комнат и отправляет соответствующие сообщения об уходе. Каждый пользователь также может задать свое имя при входе в чат, используя сообщение set_username
.
client.js
const socket = io();
// обработчик для нажатия кнопки "Создать комнату"
document.getElementById('create-room').addEventListener('submit', (event) => {
event.preventDefault();
const roomName = document.getElementById('room-name').value;
socket.emit('create_room', roomName);
});
// обработчик для нажатия кнопки "Присоединиться к комнате"
document.getElementById('join-room').addEventListener('submit', (event) => {
event.preventDefault();
const roomName = document.getElementById('room-name').value;
const username = document.getElementById('username').value;
socket.emit('join_room', { roomName, username });
});
// обработчик для отправки сообщения в текущую комнату
document.getElementById('send-message').addEventListener('submit', (event) => {
event.preventDefault();
const message = document.getElementById('message').value;
socket.emit('send_message', { roomName: currentRoom, message });
});
// обработчик для получения сообщения от сервера о создании новой комнаты
socket.on('created_room', ({ roomName, participants }) => {
console.log(`Room created: ${roomName}`);
// добавляем комнату в список доступных комнат
const roomList = document.getElementById('room-list');
const newRoom = document.createElement('li');
newRoom.textContent = roomName;
newRoom.addEventListener('click', () => joinRoom(roomName));
roomList.appendChild(newRoom);
});
// обработчик для получения сообщения от сервера о присоединении к комнате
socket.on('room_joined', ({ roomName, username }) => {
console.log(`${username} joined ${roomName}`);
// переключаемся на новую комнату
switchRoom(roomName);
});
// обработчик для получения сообщения от сервера о присоединении нового пользователя к комнате
socket.on('joined_room', ({ roomName, participants }) => {
console.log(`Participants in ${roomName}: ${participants}`);
// отображаем список участников комнаты
const participantList = document.getElementById('participant-list');
participantList.innerHTML = '';
participants.forEach((username) => {
const newParticipant = document.createElement('li');
newParticipant.textContent = username;
participantList.appendChild(newParticipant);
});
});
// обработчик для получения нового сообщения от сервера
socket.on('new_message', ({ roomName, message, sender }) => {
console.log(`New message in ${roomName} from ${sender}: ${message}`);
// добавляем сообщение в чат
const chatBox = document.getElementById('chat-box');
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.innerHTML = `<span class="sender">${sender}</span>: ${message}`;
chatBox.appendChild(newMessage);
});
// обработчик для получения сообщения от сервера о том, что пользователь покинул комнату
socket.on('user_left', ({ roomName, participants, username }) => {
console.log(`${username} left ${roomName}`);
// отображаем список участников комнаты
const participantList = document.getElementById('participant-list');
participantList.innerHTML = '';
participants.forEach((username) => {
const newParticipant = document.createElement('li');
newParticipant.textContent = username;
participantList.appendChild(newParticipant);
});
});
// функция для переключения на новую комнату
function switchRoom(roomName) {
currentRoom = roomName;
console.log(`Switching to room: ${roomName}`);
// отображаем имя текущей комнаты
const currentRoomTitle = document.getElementById('current-room-title');
currentRoomTitle.textContent = roomName;
// очищаем чат
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML = '';
// запрашиваем список участников комнаты
socket.emit('get_participants', roomName);
}
// функция для присоединения к комнате
function joinRoom(roomName) {
console.log(`Joining room: ${roomName}`);
const username = document.getElementById('username').value;
socket.emit('join_room', { roomName, username });
}
// инициализация приложения
let currentRoom;
switchRoom('general');