Короткий ответ: если вы используете браузерный WebSocket или нормальную серверную библиотеку, вручную маскировать и демаскировать фреймы не нужно. Это делает реализация протокола. Ручная маскировка нужна только если вы пишете низкоуровневую реализацию WebSocket-протокола сами. По правилу протокола клиент маскирует фреймы, которые отправляет серверу, а сервер при получении их демаскирует
Главная формула простая: каждый байт payload XOR-ится с байтом 4-байтового masking key
Что такое маскировка фрейма
WebSocket передает данные фреймами. У фрейма есть заголовок и payload. Когда клиент отправляет фрейм серверу, payload должен быть замаскирован 4-байтовым ключом. Сервер берет этот ключ и восстанавливает исходные данные
Это не шифрование. Маскировка не защищает данные от чтения как TLS. Для защиты канала используйте wss://. Маскировка — часть WebSocket-протокола, связанная с безопасностью взаимодействия клиента и посредников
Алгоритм XOR
Для каждого байта:
decoded[i] = encoded[i] ^ maskingKey[i % 4]
И наоборот:
encoded[i] = decoded[i] ^ maskingKey[i % 4]
Операция XOR обратима: если применить тот же ключ второй раз, получится исходный байт
Пример на JavaScript
function applyMask(payload, maskingKey) {
const result = new Uint8Array(payload.length);
for (let i = 0; i < payload.length; i++) {
result[i] = payload[i] ^ maskingKey[i % 4];
}
return result;
}
const text = new TextEncoder().encode("hello");
const key = new Uint8Array([1, 2, 3, 4]);
const masked = applyMask(text, key);
const unmasked = applyMask(masked, key);
console.log(new TextDecoder().decode(unmasked));
Ожидаемый результат:
hello
Этот пример показывает механику. Он не является полноценной реализацией WebSocket-фрейминга, потому что настоящий фрейм еще содержит FIN, opcode, длину payload, mask bit и сам masking key
Где это делать не нужно
В браузере:
const socket = new WebSocket("wss://example.com/ws");
socket.send("hello");
Вы не видите masking key и не должны его задавать. Браузер сам сформирует правильный фрейм
В Node.js с библиотекой ws:
socket.send("hello");
Библиотека тоже сама работает с фреймами
Если вы вручную “замаскируете” строку перед socket.send, библиотека замаскирует уже испорченные данные как обычный payload. Сервер получит не то, что вы ожидаете
Где это нужно
Ручная маскировка нужна, если вы:
- пишете свой WebSocket-клиент поверх TCP;
- разбираете raw-фреймы для обучения;
- реализуете сервер без готовой WebSocket-библиотеки;
- пишете протокольный тестер.
В прикладной веб-разработке почти всегда нужно использовать библиотеку, а не реализовывать RFC руками
Частые ошибки
Первая ошибка — думать, что маскировка равна шифрованию. Нет. Для безопасности используйте TLS и wss://
Вторая ошибка — маскировать данные перед отправкой через браузерный API. Браузер сделает маскировку сам
Третья ошибка — забыть i % 4 и использовать один байт ключа для всего payload
Четвертая ошибка — ожидать, что серверные фреймы к клиенту будут маскированы так же, как клиентские. В стандартной схеме маскировка обязательна именно для клиента
Самопроверка
Возьмите строку hello, закодируйте ее в байты, примените applyMask, затем примените applyMask еще раз с тем же ключом. Если вернулась строка hello, принцип демаскировки понятен
После этого вернитесь к обычному WebSocket API и не используйте эту функцию в прикладном коде, если только вы не пишете низкоуровневый протокол
Что почитать дальше по WebSocket
Если нужен общий маршрут по теме, откройте рубрику WebSocket. Для соседних задач пригодятся эти разборы:
- Django: как подружить Celery с WebSocket
- Django: как сохранить биржевые данные, полученные по WebSocket
- FastAPI WebSocket: уведомления в браузер
- Fedora 43: как установить WebSocket сервер