|
|||||||
WebSocket
Время создания: 08.05.2018 12:06
Текстовые метки: websocket
Раздел: WebSocket
Запись: Velonski/mytetra-database/master/base/15257631962hvxz3dbdi/text.html на raw.githubusercontent.com
|
|||||||
|
|||||||
Протокол WebSocket (стандарт RFC 6455) предназначен для решения любых задач и снятия ограничений обмена данными между браузером и сервером. Он позволяет пересылать любые данные, на любой домен, безопасно и почти без лишнего сетевого трафика. Пример браузерного кода Для открытия соединения достаточно создать объект WebSocket, указав в нём специальный протокол ws.: var socket = new WebSocket("ws://javascript.ru/ws"); У объекта socket есть четыре коллбэка: один при получении данных и три – при изменениях в состоянии соединения: socket.onopen = function() { alert("Соединение установлено."); }; socket.onclose = function(event) { if (event.wasClean) { alert('Соединение закрыто чисто'); } else { alert('Обрыв соединения'); // например, "убит" процесс сервера } alert('Код: ' + event.code + ' причина: ' + event.reason); }; socket.onmessage = function(event) { alert("Получены данные " + event.data); }; socket.onerror = function(error) { alert("Ошибка " + error.message); }; Для посылки данных используется метод socket.send(data). Пересылать можно любые данные. Например, строку: socket.send("Привет"); …Или файл, выбранный в форме: socket.send(form.elements[0].file); Просто, не правда ли? Выбираем, что переслать, и socket.send(). Для того, чтобы коммуникация была успешной, сервер должен поддерживать протокол WebSocket. Чтобы лучше понимать происходящее – посмотрим, как он устроен. Установление WebSocket-соединения Протокол WebSocket работает над TCP. Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: «поддерживает ли сервер WebSocket?». Если сервер в ответных заголовках отвечает «да, поддерживаю», то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего. Установление соединения Пример запроса от браузера при создании нового объекта new WebSocket("ws://server.example.com/chat"): GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Origin: http://javascript.ru Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Описания заголовков: GET, Host Стандартные HTTP-заголовки из URL запроса Upgrade, Connection Указывают, что браузер хочет перейти на websocket. Origin Протокол, домен и порт, откуда отправлен запрос. Sec-WebSocket-Key Случайный ключ, который генерируется браузером: 16 байт в кодировке Base64. Sec-WebSocket-Version Версия протокола. Текущая версия: 13. Все заголовки, кроме GET и Host, браузер генерирует сам, без возможности вмешательства JavaScript. Такой XMLHttpRequest создать нельзя Создать подобный XMLHttpRequest-запрос (подделать WebSocket) невозможно, по одной простой причине: указанные выше заголовки запрещены к установке методом setRequestHeader. Сервер может проанализировать эти заголовки и решить, разрешает ли он WebSocket с данного домена Origin. Ответ сервера, если он понимает и разрешает WebSocket-подключение: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Здесь строка Sec-WebSocket-Accept представляет собой перекодированный по специальному алгоритму ключ Sec-WebSocket-Key. Браузер использует её для проверки, что ответ предназначается именно ему. Затем данные передаются по специальному протоколу, структура которого («фреймы») изложена далее. И это уже совсем не HTTP. Расширения и подпротоколы Также возможны дополнительные заголовки Sec-WebSocket-Extensions и Sec-WebSocket-Protocol, описывающие расширения и подпротоколы (subprotocol), которые поддерживает данный клиент. Посмотрим разницу между ними на двух примерах: Заголовок Sec-WebSocket-Extensions: deflate-frame означает, что браузер поддерживает модификацию протокола, обеспечивающую сжатие данных. Это говорит не о самих данных, а об улучшении способа их передачи. Браузер сам формирует этот заголовок. Заголовок Sec-WebSocket-Protocol: soap, wamp говорит о том, что по WebSocket браузер собирается передавать не просто какие-то данные, а данные в протоколах SOAP или WAMP («The WebSocket Application Messaging Protocol»). Стандартные подпротоколы регистрируются в специальном каталоге IANA. Этот заголовок браузер поставит, если указать второй необязательный параметр WebSocket: var socket = new WebSocket("ws://javascript.ru/ws", ["soap", "wamp"]); При наличии таких заголовков сервер может выбрать расширения и подпротоколы, которые он поддерживает, и ответить с ними. Например, запрос: GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Origin: http://javascript.ru Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap, wamp Ответ: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap В ответе выше сервер указывает, что поддерживает расширение deflate-frame, а из запрошенных подпротоколов – только SOAP. WSS Соединение WebSocket можно открывать как WS:// или как WSS://. Протокол WSS представляет собой WebSocket над HTTPS. Кроме большей безопасности, у WSS есть важное преимущество перед обычным WS – большая вероятность соединения. Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет. Если между клиентом и сервером есть прокси, то в случае с HTTP все WebSocket-заголовки и данные передаются через него. Прокси имеет к ним доступ, ведь они никак не шифруются, и может расценить происходящее как нарушение протокола HTTP, обрезать заголовки или оборвать передачу. А в случае с WSS весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через WSS выше, чем через WS. Формат данных Полное описание протокола содержится в RFC 6455. Здесь представлено частичное описание с комментариями самых важных его частей. Если вы хотите понять стандарт, то рекомендуется сначала прочитать это описание. Описание фрейма В протоколе WebSocket предусмотрены несколько видов пакетов («фреймов»). Они делятся на два больших типа: фреймы с данными («data frames») и управляющие («control frames»), предназначенные для проверки связи (PING) и закрытия соединения. Фрейм, согласно стандарту, выглядит так: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| опкод |М| Длина тела | Расширенная длина тела | |I|S|S|S|(4бита)|А| (7бит) | (1 байт) | |N|V|V|V| |С| |(если длина тела==126 или 127) | | |1|2|3| |К| | | | | | | | |А| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Продолжение расширенной длины тела, если длина тела = 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | | Ключ маски, если МАСКА = 1 | +-------------------------------+-------------------------------+ | Ключ маски (продолжение) | Данные фрейма ("тело") | +-------------------------------- - - - - - - - - - - - - - - - + : Данные продолжаются ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Данные продолжаются ... | +---------------------------------------------------------------+ С виду – не очень понятно, во всяком случае, для большинства людей. Позвольте пояснить: читать следует слева-направо, сверху-вниз, каждая горизонтальная полоска это 32 бита. То есть, вот первые 32 бита: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| опкод |М| Длина тела | Расширенная длина тела | |I|S|S|S|(4бита)|А| (7бит) | (1 байт) | |N|V|V|V| |С| |(если длина тела==126 или 127) | | |1|2|3| |К| | | | | | | | |А| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + Сначала идёт бит FIN (вертикальная надпись на рисунке), затем биты RSV1, RSV2, RSV3 (их смысл раскрыт ниже), затем «опкод», «МАСКА» и, наконец, «Длина тела», которая занимает 7 бит. Затем, если «Длина тела» равна 126 или 127, идёт «Расширенная длина тела», потом (на следующей строке, то есть после первых 32 бит) будет её продолжение, ключ маски, и потом данные. А теперь – подробное описание частей фрейма, то есть как именно передаются сообщения: FIN: 1 бит Одно сообщение, если оно очень длинное (вызовом send можно передать хоть целый файл), может состоять из множества фреймов («быть фрагментированным»). У всех фреймов, кроме последнего, этот фрагмент установлен в 0, у последнего – в 1. Если сообщение состоит из одного-единственного фрейма, то FIN в нём равен 1. RSV1, RSV2, RSV3: 1 бит каждый В обычном WebSocket равны 0, предназначены для расширений протокола. Расширение может записать в эти биты свои значения. Опкод: 4 бита Задаёт тип фрейма, который позволяет интерпретировать находящиеся в нём данные. Возможные значения: 0x1 обозначает текстовый фрейм. 0x2 обозначает двоичный фрейм. 0x3-7 зарезервированы для будущих фреймов с данными. 0x8 обозначает закрытие соединения этим фреймом. 0x9 обозначает PING. 0xA обозначает PONG. 0xB-F зарезервированы для будущих управляющих фреймов. 0x0 обозначает фрейм-продолжение для фрагментированного сообщения. Он интерпретируется, исходя из ближайшего предыдущего ненулевого типа. Маска: 1 бит Если этот бит установлен, то данные фрейма маскированы. Более подробно маску и маскирование мы рассмотрим далее. Длина тела: 7 битов, 7+16 битов, или 7+64 битов Если значение поле «Длина тела» лежит в интервале 0-125, то оно обозначает длину тела (используется далее). Если 126, то следующие 2 байта интерпретируются как 16-битное беззнаковое целое число, содержащее длину тела. Если 127, то следующие 8 байт интерпретируются как 64-битное беззнаковое целое, содержащее длину. Такая хитрая схема нужна, чтобы минимизировать накладные расходы. Для сообщений длиной 125 байт и меньше хранение длины потребует всего 7 битов, для бóльших (до 65536) – 7 битов + 2 байта, ну а для ещё бóльших – 7 битов и 8 байт. Этого хватит для хранения длины сообщения размером в гигабайт и более. Ключ маски: 4 байта. Если бит Маска установлен в 0, то этого поля нет. Если в 1 то эти байты содержат маску, которая налагается на тело (см. далее). Данные фрейма (тело) Состоит из «данных расширений» и «данных приложения», которые идут за ними. Данные расширений определяются конкретными расширениями протокола и по умолчанию отсутствуют. Длина тела должна быть равна указанной в заголовке. Примеры Некоторые примеры сообщений: Нефрагментированное текстовое сообщение Hello без маски: 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") В заголовке первый байт содержит FIN=1 и опкод=0x1 (получается 10000001 в двоичной системе, то есть 0x81 – в 16-ричной), далее идёт длина 0x5, далее текст. Фрагментированное текстовое сообщение Hello World из трёх частей, без маски, может выглядеть так: 0x01 0x05 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") 0x00 0x01 0x20 (содержит " ") 0x80 0x05 0x57 0x6f 0x72 0x6c 0x64 (содержит "World") У первого фрейма FIN=0 и текстовый опкод 0x1. У второго FIN=0 и опкод 0x0. При фрагментации сообщения, у всех фреймов, кроме первого, опкод пустой (он один на всё сообщение). У третьего, последнего фрейма FIN=1. А теперь посмотрим на все те замечательные возможности, которые даёт этот формат фрейма. Фрагментация Позволяет отправлять сообщения в тех случаях, когда на момент начала посылки полный размер ещё неизвестен. Например, идёт поиск в базе данных и что-то уже найдено, а что-то ещё может быть позже. У всех сообщений, кроме последнего, бит FIN=0. Опкод указывается только у первого, у остальных он должен быть равен 0x0. PING / PONG В протокол встроена проверка связи при помощи управляющих фреймов типа PING и PONG. Тот, кто хочет проверить соединение, отправляет фрейм PING с произвольным телом. Его получатель должен в разумное время ответить фреймом PONG с тем же телом. Этот функционал встроен в браузерную реализацию, так что браузер ответит на PING сервера, но управлять им из JavaScript нельзя. Иначе говоря, сервер всегда знает, жив ли посетитель или у него проблема с сетью. Чистое закрытие При закрытии соединения сторона, желающая это сделать (обе стороны в WebSocket равноправны) отправляет закрывающий фрейм (опкод 0x8), в теле которого указывает причину закрытия. В браузерной реализации эта причина будет содержаться в свойстве reason события onclose. Наличие такого фрейма позволяет отличить «чистое закрытие» от обрыва связи. В браузерной реализации событие onclose при чистом закрытии имеет event.wasClean = true. Коды закрытия Коды закрытия вебсокета event.code, чтобы не путать их с HTTP-кодами, состоят из 4 цифр: 1000 Нормальное закрытие. 1001 Удалённая сторона «исчезла». Например, процесс сервера убит или браузер перешёл на другую страницу. 1002 Удалённая сторона завершила соединение в связи с ошибкой протокола. 1003 Удалённая сторона завершила соединение в связи с тем, что она получила данные, которые не может принять. Например, сторона, которая понимает только текстовые данные, может закрыть соединение с таким кодом, если приняла бинарное сообщение. Атака «отравленный кэш» В ранних реализациях WebSocket существовала уязвимость, называемая «отравленный кэш» (cache poisoning). Она позволяла атаковать кэширующие прокси-сервера, в частности, корпоративные. Атака осуществлялась так: Хакер заманивает доверчивого посетителя (далее Жертва) на свою страницу. Страница открывает WebSocket-соединение на сайт хакера. Предполагается, что Жертва сидит через прокси. Собственно, на прокси и направлена эта атака. Страница формирует специального вида WebSocket-запрос, который (и здесь самое главное!) ряд прокси серверов не понимают. Они пропускают начальный запрос через себя (который содержит Connection: upgrade) и думают, что далее идёт уже следующий HTTP-запрос. …Но на самом деле там данные, идущие через вебсокет! И обе стороны вебсокета (страница и сервер) контролируются Хакером. Так что хакер может передать в них нечто похожее на GET-запрос к известному ресурсу, например http://code.jquery.com/jquery.js, а сервер ответит «якобы кодом jQuery» с кэширующими заголовками. Прокси послушно проглотит этот ответ и закэширует «якобы jQuery». В результате при загрузке последующих страниц любой пользователь, использующий тот же прокси, что и Жертва, получит вместо http://code.jquery.com/jquery.js хакерский код. Поэтому эта атака и называется «отравленный кэш». Такая атака возможна не для любых прокси, но при анализе уязвимости было показано, что она не теоретическая, и уязвимые прокси действительно есть. Поэтому придумали способ защиты – «маску». Маска для защиты от атаки Для того, чтобы защититься от атаки, и придумана маска. Ключ маски – это случайное 32-битное значение, которое варьируется от пакета к пакету. Тело сообщения проходит через XOR ^ с маской, а получатель восстанавливает его повторным XOR с ней (можно легко доказать, что (x ^ a) ^ a == x). Маска служит двум целям: Она генерируется браузером. Поэтому теперь хакер не сможет управлять реальным содержанием тела сообщения. После накладывания маски оно превратится в бинарную мешанину. Получившийся пакет данных уже точно не может быть воспринят промежуточным прокси как HTTP-запрос. Наложение маски требует дополнительных ресурсов, поэтому протокол WebSocket не требует её. Если по этому протоколу связываются два клиента (не обязательно браузеры), доверяющие друг другу и посредникам, то можно поставить бит Маска в 0, и тогда ключ маски не указывается. Пример Рассмотрим прототип чата на WebSocket и Node.JS. HTML: посетитель отсылает сообщения из формы и принимает в div <!-- форма для отправки сообщений --> <form name="publish"> <input type="text" name="message"> <input type="submit" value="Отправить"> </form> <!-- здесь будут появляться входящие сообщения --> <div id="subscribe"></div> Код на клиенте: // создать подключение var socket = new WebSocket("ws://localhost:8081"); // отправить сообщение из формы publish document.forms.publish.onsubmit = function() { var outgoingMessage = this.message.value; socket.send(outgoingMessage); return false; }; // обработчик входящих сообщений socket.onmessage = function(event) { var incomingMessage = event.data; showMessage(incomingMessage); }; // показать сообщение в div#subscribe function showMessage(message) { var messageElem = document.createElement('div'); messageElem.appendChild(document.createTextNode(message)); document.getElementById('subscribe').appendChild(messageElem); } Серверный код можно писать на любой платформе. В нашем случае это будет Node.JS, с использованием модуля ws: var WebSocketServer = new require('ws'); // подключенные клиенты var clients = {}; // WebSocket-сервер на порту 8081 var webSocketServer = new WebSocketServer.Server({ port: 8081 }); webSocketServer.on('connection', function(ws) { var id = Math.random(); clients[id] = ws; console.log("новое соединение " + id); ws.on('message', function(message) { console.log('получено сообщение ' + message); for (var key in clients) { clients[key].send(message); } }); ws.on('close', function() { console.log('соединение закрыто ' + id); delete clients[id]; }); }); Рабочий пример можно скачать: websocket.zip. Понадобится поставить два модуля: npm install node-static && npm install ws. Итого WebSocket – современное средство коммуникации. Кросс-доменное, универсальное, безопасное. На текущий момент он работает в браузерах IE10+, FF11+, Chrome 16+, Safari 6+, Opera 12.5+. В более старых версиях FF, Chrome, Safari, Opera есть поддержка черновых редакций протокола. Там, где вебсокеты не работают – обычно используют другие транспорты, например IFRAME. Вы найдёте их в других статьях этого раздела. Есть и готовые библиотеки, реализующие функционал COMET с использованием сразу нескольких транспортов, из которых вебсокет имеет приоритет. Как правило, библиотеки состоят из двух частей: клиентской и серверной. Например, для Node.JS одной из самых известных библиотек является Socket.IO. К недостаткам библиотек следует отнести то, что некоторые продвинутые возможности WebSocket, такие как двухсторонний обмен бинарными данными, в них недоступны. С другой – в большинстве случаев стандартного текстового обмена вполне достаточно. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|