⬆️ ⬇️

WebSocket (Sec-WebSocket-Version: 13) - implementation details, in particular in PHP

Actually, studying this topic, many sites were searched, but nowhere was nothing really explained, or the information was on obsolete protocols. This was a kind of kick in creating this HowTo. This will not be a detailed analysis of all possible problems, but a bit of theory and a description of some things that are trivial to someone, and someone (like me) caused difficulties and loss of time to find a solution. Immediately I will warn you - here it is not considered how to raise a socket server for PHP, similar information on the Internet in bulk. I will proceed from the fact that the socket server already exists and you just need to teach him to communicate through web sockets.

So, enough of the lyrics, now to the point!



A bit of theory.


Handshake


When connected via webboxes, headers are exchanged like HTTP headers, the so-called handshake or in our “handshake”.

The client sends a header with similar content:

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

To which the server should respond to him:

HTTP / 1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK + xOo =

Sec-WebSocket-Protocol: chat



So it is written in the literature (The WebSocket Protocol RFC 6455). It would seem that difficult: received - answered. But here I had my first problems. The server received a header from the client, responded to it, but the client did not respond, regardless of the client (in this case, the browser). I tried everything that had enough brain, nothing helped. A hint was found here . The meaning of my mistake was that the browser accepts a header with a terminating empty string, and since I did not send it (well, I did not find a word about it in the documentation), the browser continued to wait for the header, and the event “webbox connected” (WebSocket.onopen ) "Did not occur in the browser. As a result, my answer was as follows:

$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" 
And the client finally saw him.



Server header


And now let's move on to what is included in the server response.

First line: "HTTP / 1.1 101 Switching Protocols" . There is nothing to change here. Any status code other than 101 will mean that the “handshake” is not completed.



In the lines of Upgrade and Connection, if the “websocket” and “Upgrade” registers are not followed, respectively, the client must terminate the connection. That is, we also leave it as it was. Although, for example, firewall sent in the title “Connection: keep-alive, Upgrade” , maybe it can be answered the same, but so far I haven’t found it necessary.

')

Next is, perhaps, the only line where we need to put a hand: "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK + xOo =" . This line announces that the server accepts the connection, and reports in a special way the calculated hash of the key transmitted by the client to the Sec-WebSocket-Key .

To calculate the hash needed:

  1. Concatenation of the client key and the preset GUID. According to the documentation, the GUID is the following line: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11". Suppose that we have already extracted the client key and stored it in the $ key variable (do not forget to remove the leading and trailing spaces if they somehow fall into the variable)
     $hash = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 
  2. Calculating sha1 from the resulting string, and the result should be in the form of a binary string of 20 characters.
     $hash = sha1($hash,true); 
  3. And the last - base64 hash coding
     $hash = base64_encode($hash); 


Proceed to the next line of the server header, which I send to the client. “Sec-WebSocket-Protocol: chat” is an optional parameter, and it informs the client on which sub-protocol the server will communicate with it. This sub-protocol must be supported by the client, and it must come from the client in the parameter of the same name, although I did not send such parameters in the header when I connected firewall and chrome.



There is one more tasty moment which has got to me in the documentation. The server can tell the client which protocol version it supports.

For example, the client sends the following:
 GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 25 


The server responds to it:
 HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13, 8, 7 
Could be so:
 HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13 Sec-WebSocket-Version: 8, 7 


After that, the client repeats the handshake, but with the version of the protocol that the server has informed him.
 GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 13 


Customer title


There’s nothing much to say about the client’s heading, except for the Origin and Host parameters.

Host contains the server address and port to which the web socket is connected.

Origin is an optional field, usually used by browsers. Contains the name of the web server, from which page javascript is launched to connect to the server (IMHO, did not check).



Packet Exchange


Here, of course, they are very tired. The frame in the documentation looks like this:

  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 ... | +---------------------------------------------------------------+ 


I saw that right away, laziness somehow began to be understood ... but even so, I had to. By the way, for those who want to understand this on this page in detail, there is a distinct Russian-language description, although it is certainly better to fully understand the source documentation. For decoding and encoding frames, I found here the ready-made functions hybi10Decode () and hybi10Encode () , which showed themselves to be working properly. Also in the handshake () function, a method for obtaining client header parameters is described.

Note also that after a handshake, the client sends only masked frames to the server and the server only unmasks the server, that is, where the MASK bit = 0.



In the process, I ran into another problem, after the handshakes and the server response to the client's “hello”, Chrome issued the following:

WebSocket connection to 'ws: //example.com: 10001 / test' failed:
That is, the browser saw that the following message was masked, although I knew for sure that the server sent an unmasked frame. After a long carding turnip, it turned out that it was a problem of interoperability between web protocols and ActionScript sockets for flash. In my code, a zero byte "\ 0" was inserted at the end of each message as required by this flash, and accordingly this byte was inserted at the end of each frame or header, and the browser read it as the beginning of the next, because the browser knows the exact length of the frame or where header Thus, the first byte of the next header was "\ 0", and the actual first one shifted to the second, which made the browser indignant.



That's all for now. In conclusion, I would like to say that web sockets, as well as HTML5 as a whole, is in my opinion a wonderful tool that will allow the browser to independently do what it was previously unable to do, of course, without Flash crutches.



Update

In the comments it was noticed that by specification all messages are sent in utf-8 encoding. This is also an important point, but I forgot to mention it.



Update 17-05-13

Faced with another problem: the browser can send two frames in a row, and the above hybi10Decode () function treats it as one frame, since it reads a string not by the length of the payload length transmitted in the frame, but to the end of the entire frame. After some changes, the function looks like this:

Click
 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) { $msg = decode($frame); $recieved += strlen($msg['payload'])+ $msg['offset']; $frame = substr($frame,$recieved); } 
Please note that there is no support for fragmented frames in this function.



Links

RFC6455

Javascript Web Sockets

A project on GitHub that works with version 13 of the protocol

Source: https://habr.com/ru/post/179585/



All Articles