Итак, хватит лирики, теперь к делу!
Немного теории.
Handshake
При подключении посредством вебсокетов происходит обмен заголовками наподобие заголовков HTTP, так называемый handshake или по-нашему «рукопожатие».
Клиент отправляет заголовок подобного содержания:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
На что сервер должен ему ответить:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Так написано в литературе (The WebSocket Protocol RFC 6455). Казалось бы что сложного: получил — ответил. Но здесь у меня возникли перые проблемы. Сервер получал заголовок от клиента, отвечал на него, но клиент не реагировал, вне зависимости от клиента (в данном случае — браузера). Пробовал все, на что хватило мозга, ничего не помогало. Подсказка была найдена здесь. Смысл моей ошибки заключался в том, что браузер принимает заголовок с завершающей пустой строкой, а так как я ее не отправлял (ну не нашел в документации про это ни слова), то браузер продолжал ждать заголовок, и событие «вебсокет подключен (WebSocket.onopen)» в браузере не происходило. В итоге мой ответ выглядел следующим образом:
$answer = "HTTP/1.1 101 Switching Protocols\r\n" ."Upgrade: websocket\r\n" ."Connection: Upgrade\r\n" ."Sec-WebSocket-Accept: ".$hash."\r\n" ."Sec-WebSocket-Protocol: chat\r\n\r\n"
И клиент наконец его узрел.
Заголовок сервера
А теперь плавно перейдем к тому, что входит в ответ сервера.
Первая строка: «HTTP/1.1 101 Switching Protocols». Здесь менять ничего не надо. Любой код статуса, отличный от 101 будет означать что «рукопожатие» не завершено.
В строчках Upgrade и Connection если не ввести с соблюдением регистра «websocket» и «Upgrade» соответственно, то клиент должен разорвать соединение. То есть, тоже оставляем как было. Хотя, например, огнелис присылал в заголовке «Connection: keep-alive, Upgrade», возможно ему можно ответить темже, но пока необходимости я в этом не нашел.
Далее идет, пожалуй, единственная строка, где нам надо приложить руку: «Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=». Данная строка объявляет о том, что сервер принимает подключение, и сообщает специальным образом вычисленный хэш от ключа, переданного клиентом в Sec-WebSocket-Key.
Чтобы вычислить хэш нужны:
- Конкатенация ключа клиента и предустановленного GUID. По документации GUID является следующей строкой: «258EAFA5-E914-47DA-95CA-C5AB0DC85B11». Предположим, что ключ клиента мы уже извлекли и храним в переменной $key (не забудьте убрать лидирующие и конечные пробелы, если они каким-то образом попали в переменную)
$hash = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
- Вычисление sha1 от полученной строки, причем результат должен быть в виде двоичной строки из 20 символов.
$hash = sha1($hash,true);
- И последнее — кодирование хеша методом base64
$hash = base64_encode($hash);
Перейдем к следующей строке заголовка сервера, который я отправляю клиенту. «Sec-WebSocket-Protocol: chat» — это необязательный параметр, и он сообщает клиенту по какому подпротоколу сервер будет с ним общаться. Этот подпротокол должен поддерживаться клиентом, и он должен приходить от клиента в одноименном параметре, хотя огнелис и хром при подключении у меня таких параметров в заголовке не отправляли.
Есть еще один вкусный момент, который мне попался в документации. Сервер может указать клиенту какую версию протокола он поддерживает.
К примеру, клиент шлет следующее:
GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 25
Сервер ему отвечает:
HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13, 8, 7
Можно и так:
HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13 Sec-WebSocket-Version: 8, 7
После чего клиент повторяет рукопожатие, но уже с версией протокола которую ему сообщил сервер.
GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 13
Заголовок клиента
Про заголовок клиента говорить особо нечего, разве что остановлюсь на параметрах Origin и Host.
Host содержит адрес сервера и порт, к которому подключается вебсокет.
Origin — опциональное поле, используется как правило браузерами. Содержит имя вебсервера, со страницы которого запущен javascript для подключения к серверу (имхо, не проверял).
Обмен пакетами
Вот тут конечно они сильно замудрили. Фрейм выглядит в документации так:
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| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
Как увидел, так прям сразу лень как-то стало разбираться… но всеже пришлось. Кстати, для тех кто хочет детально разобраться в этом на этой странице есть внятное рускоязычное описание, хотя полностью разбираться, конечно, лучше в исходной документации. Для декодирования и кодирования фреймов я нашел здесь готовые функции hybi10Decode() и hybi10Encode(), которые показали себя, как исправно работающие. Тамже в функции handshake() описан метод получения параметров заголовка клиента.
Учтите также, что после рукопожатий, клиент отправляет серверу только маскированные фреймы, а сервер клиенту только немаскированные, то есть где бит MASK = 0.
В процессе я столкнулся еще с одной проблемой, после рукопожатий и ответа сервера на «hello» клиента, хром выдал следующее:
WebSocket connection to ‘ws://example.com:10001/test’ failed: A server must not mask any frames that it sends to the client.
То есть браузер видел что следующее сообщение было маскированным, хотя я точно знал, что сервер посылал немаскированный фрейм. После длительного чесания репы выяснилось, что это была проблема совместимости протоколов обмена посредством вебсокетов и ActionScript-сокетов для флеша. В моем коде в конец каждого фрейма вставлялся нулевой байт "\0" как требует этого флеш, и соответственно в конец каждого фрейма или заголовка вставлялся этот байт, а браузер читал его уже как начало следующего, так как браузер знает точную длину фрейма или где конец заголовка. Таким образом первый байт следующего заголовка был "\0", а действительный первый сдвигался до второго, что вводило браузер в негодование.
На этом пока все. В завершение хотелось бы сказать, что вебсокеты, как и в целом HTML5, на мой взгляд замечательный инструмент, который позволит браузеру самостоятельно делать то, что раньше ему было не по силам, разумеется без Flash-костылей.
Ссылки
RFC6455
Вебсокеты в Javascript
Проект на GitHub, который работает с 13 версией протокола
ссылка на оригинал статьи http://habrahabr.ru/post/179585/
Добавить комментарий