What is websocket in php

WebSocket (Sec-WebSocket-Version: 13) — тонкости реализации, в частности на PHP

Собственно, изучая данную тему, было перерыто много сайтов, но нигде толком ничего не объяснялось, либо информация была по устаревшим ныне протоколам. Это и послужило своеобразным пинком для создания этого HowTo. Это будет не детальный разбор всех возможных проблем, но немного теории и описание некоторых вещей которые для кого-то являются банальщиной, а у кого-то (вроде меня) вызвали трудности и потерю времени на поиск решения. Сразу предупрежу — здесь не рассматривается как поднять сокет-сервер на PHP, подобной информации в интернете навалом. Буду исходить из того, что сокет-сервер уже существует и надо лишь научить его общаться через вебсокеты.
Итак, хватит лирики, теперь к делу!

Немного теории.
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"

И клиент наконец его узрел.

Читайте также:  Python deep copy dict
Заголовок сервера

А теперь плавно перейдем к тому, что входит в ответ сервера.
Первая строка: «HTTP/1.1 101 Switching Protocols». Здесь менять ничего не надо. Любой код статуса, отличный от 101 будет означать что «рукопожатие» не завершено.

В строчках Upgrade и Connection если не ввести с соблюдением регистра «websocket» и «Upgrade» соответственно, то клиент должен разорвать соединение. То есть, тоже оставляем как было. Хотя, например, огнелис присылал в заголовке «Connection: keep-alive, Upgrade», возможно ему можно ответить темже, но пока необходимости я в этом не нашел.

    Конкатенация ключа клиента и предустановленного GUID. По документации GUID является следующей строкой: «258EAFA5-E914-47DA-95CA-C5AB0DC85B11». Предположим, что ключ клиента мы уже извлекли и храним в переменной $key (не забудьте убрать лидирующие и конечные пробелы, если они каким-то образом попали в переменную)

$hash = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 

Есть еще один вкусный момент, который мне попался в документации. Сервер может указать клиенту какую версию протокола он поддерживает.
К примеру, клиент шлет следующее:

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-костылей.

Update
В комментариях было подмечено, что по спецификации все сообщения посылаются в кодировке utf-8. Это тоже важный момент, но я забыл о нем упомянуть.

Update 17-05-13
Столкнулся с еще одной проблемой: браузер может отослать два фрейма подряд, а вышеприведенная функция hybi10Decode() обрабатывает его как один фрейм, так как читает строку не по переданной в фрейме длине payload length, а до конца всего фрейма. После некоторых изменений функция выглядит так:

function decode($data)< $payloadLength = ''; $mask = ''; $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; if($isMasked === false) $this->close(1002);// close connection if unmasked frame is received switch($opcode) < case 1: $decodedData['type'] = 'text'; break;// text frame case 8: $decodedData['type'] = 'close'; break;// connection close frame case 9: $decodedData['type'] = 'ping'; break;// ping frame case 10: $decodedData['type'] = 'pong'; break;// pong frame default: $this->close(1003); break;// Close connection on unknown opcode > if($payloadLength === 126) < $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = sprintf('%016b', ord($data[2]).ord($data[3])); $dataLength = base_convert($dataLength,2,10); >elseif($payloadLength === 127) < $mask = substr($data, 10, 4); $payloadOffset = 14; $dataLength = ''; for ($i=2;$i<8;$i++) $dataLength .=sprintf('%08b',ord($data[$i])); $dataLength = base_convert($dataLength,2,10); >else < $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = base_convert(sprintf('%08b',ord($data[1]) & 63),2,10); >if($isMasked === true) < for($i = $payloadOffset; $i < $dataLength+$payloadOffset; $i++)< $j = $i - $payloadOffset; $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; >$decodedData['payload'] = $unmaskedPayload; > else < $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); >$decodedData['offset'] = $payloadOffset; return $decodedData; > // а использую я ее таким образом ($frame - байты которые сервер получил от клиента) $recieved = 0; while(strlen($frame)> 0)

Обратите внимание, что в этой фунции нет поддержки фрагментированных фреймов.

Источник

Start Using HTML5 WebSockets Today With a PHP Server

Umar Hansa

Umar Hansa Last updated Mar 30, 2023

One of the exciting features of HTML5 is WebSockets, which let us talk to the server without using AJAX requests. In this tutorial, we’ll review the process of running a WebSocket server in PHP, and then building a client to send and receive messages to it over the WebSocket protocol.

If you want to learn how to use WebSockets with a Node server, check out our companion post.

What Are WebSockets?

A WebSocket is a persistent bi-directional communication channel between a client (e.g. a browser) and a back-end service. In contrast with HTTP request/response connections, WebSockets can transport any number of protocols and provide server-to-client message delivery without polling.

What Do WebSockets Replace?

WebSockets are designed to address the limitations of traditional HTTP-based communication. Before the introduction of WebSockets, HTTP requests and responses were stateless, meaning that each request/response pair was independent of previous and subsequent requests/responses. Due to that, it was really difficult to implement real-time communication, where the server needs to push data to the client as soon as it becomes available.

WebSockets also provide advantages over older technologies such as AJAX long polling and server-sent events (SSE). WebSockets can replace long polling easily. This is an interesting concept. The client sends a request to the server, and now, rather than the server responding with data it may not have, it essentially keeps the connection open until the fresh, up-to-date data is ready to be sent. The client next receives this and sends another request. This has its benefits: decreased latency being one of them, as a connection which has already been opened does not require a new connection to be established. However, long polling isn’t really a piece of fancy technology: it’s also possible for a request to time out, and thus a new connection will be needed anyway.

Many AJAX applications make use of the above—this often leads to poor resource utilization.

Wouldn’t it be great if the server could send data to clients who are willing to listen without some sort of pre-established connection? Welcome to the world of push technology!

Overall, WebSockets provide a more efficient and effective solution for real-time communication, and they have become really popular for building real-time web applications.

Install the Ratchet WebSockets Library

Ratchet is a PHP library which enables building real-time, bi-directional, and event-driven applications with WebSockets. Today, we’ll use this library to implement the WebSockets server.

To install the Ratchet WebSockets library, you need to use Composer, which is a dependency manager for PHP. Here are the steps to install Ratchet using Composer.

I assume that Composer is already installed on your system. With that in place, you can use the following Composer command to install the Ratchet library.

$composer require cboden/ratchet

Upon successful installation, it should create the following composer.json file.

Источник

Оцените статью