websocket.md

August 16, 2016 · View on GitHub

WebSocket Back

We use WebSocket to create a connection between Client and Server, to send or receive data.

Client (JavaScript)

1. new an object

/**
 * [WebSocket]
 * @param {[string]} url      [url to connect]
 * @param {[string]} protocol [protocol designed by servers, which will be a empty string by default]
 */
var webSocket = new WebSocket('http://www.example.com/socketserver' 'socket');

2. available methods

/**
 * [close description]
 * @param  {[number]} code   [status code, which is 1000 by default, meaning normal closing]
 * @param  {[string]} reason [the reason why]
 */
webSocket.close('300', 'Personal Reson');

webSocket.send('data');

A list of Close Status Code:

StatusNameDescription
0–999Reserved and not used.
1000CLOSE_NORMALNormal closure; the connection successfully completed whatever purpose for which it was created.
1001CLOSE_GOING_AWAYThe endpoint is going away, either because of a server failure or because the browser is navigating away from the page that opened the connection.
1002CLOSE_PROTOCOL_ERRORThe endpoint is terminating the connection due to a protocol error.
1003CLOSE_UNSUPPORTEDThe connection is being terminated because the endpoint received data of a type it cannot accept (for example, a text-only endpoint received binary data).
1004Reserved. A meaning might be defined in the future.
1005CLOSE_NO_STATUSReserved. Indicates that no status code was provided even though one was expected.
1006CLOSE_ABNORMALReserved. Used to indicate that a connection was closed abnormally (that is, with no close frame being sent) when a status code is expected.
1007Unsupported DataThe endpoint is terminating the connection because a message was received that contained inconsistent data (e.g., non-UTF-8 data within a text message).
1008Policy ViolationThe endpoint is terminating the connection because it received a message that violates its policy. This is a generic status code, used when codes 1003 and 1009 are not suitable.
1009CLOSE_TOO_LARGEThe endpoint is terminating the connection because a data frame was received that is too large.
1010Missing ExtensionThe client is terminating the connection because it expected the server to negotiate one or more extension, but the server didn't.
1011Internal ErrorThe server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
1012Service RestartThe server is terminating the connection because it is restarting. [Ref]
1013Try Again LaterThe server is terminating the connection due to a temporary condition, e.g. it is overloaded and is casting off some of its clients. [Ref]
1014Reserved for future use by the WebSocket standard.
1015TLS HandshakeReserved. Indicates that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).
1016–1999Reserved for future use by the WebSocket standard.
2000–2999Reserved for use by WebSocket extensions.
3000–3999Available for use by libraries and frameworks. May not be used by applications. Available for registration at the IANA via first-come, first-serve.
4000–4999Available for use by applications.

3. available attributes

NameTypeDescription
binaryTypeStringType of transferring data, which should be 'blob' or 'arraybuffer'
bufferAmountNumberNumber of sendding data (read-only)
extensionsStringExtensions the Server chooses
urlStringConstructor url
protocolStringConstructor protocol
readyStateNumberConnection state
oncloseEventClose listener
onerrorEventError listener
onmessageEventMessage Listener
onopenEventOpen listener

A list of Ready State Code

ConstantValueDescription
CONNECTING0The connection is setting up
OPEN1The connection has been built up
CLOSING2The connection is closing
CLOSED3The connection has been closed, or failed to build up

4. Compatibility Problems

  • Desktop
FeaturesChromeFirefox (Gecko)Internet ExplorerOperaSafari
Basic(Yes)4.0 (2.0)(Yes)(Yes)?
Words Protocol?6.0 (6.0)???

Notice that: After Gecko 6.0, constructor has been changed to MozWebSocket.

Notice that: extensions attribute is only supported by Gecko 8.0 or more.

  • Mobile
FeaturesAndroidFirefox Mobile (Gecko)IE MobileOpera MobileSafari Mobile
Basic?7.0 (7.0)???
Words Protocol?7.0 (7.0)???

Server (PHP)

Even though you're building a server, a client still has to start the WebSocket handshake process. So you must know how to interpret the client's request. The client will send a pretty standard HTTP request that looks like this (the HTTP version must be 1.1 or greater, and the method must be GET):

GET /socketserver HTTP/1.1
Host: www.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: socket
Sec-WebSocket-Version: 13

1. create a class

<?php
class WebSocket
{
    /** the client */
    private $master;
    
    /** sockets with different state */
    private $sockets = [];
    
    /** whether has a handShake */
    private $handShake = false;
    
    /** extract key from the request header */
    private function getKey($req)
    {
        $key = null;
        
        if (preg_match('/Sec-WebSocket-Key: (.*)\r\n/'), $req, $match) {
            $key = $match[1];
        }
        
        return $key;
    }
    
    /** encrypt key */
    private function encry($req)
    {
        $key = $this->getKey($req);
        $mask = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
        
        return base64_encode(sha1($key, $mask, true));
    }
    
    /** under Version 13 */
    private function encryOld($key1, $key2, $l8b)
    {
        $key1_num = implode($key1_num[0]);
        $key2_num = implode($key2_num[0]);
        
        /** count space */
        preg_match_all('/([ ]+)/', $key1, $key1_spc);
        preg_match_all('/([ ]+)/', $key2, $key2_spc);
    
        if ($key1_spc === 0 || $key2_spc === 0) { 
            $this->log("Invalid key");
            return;
        }
        
        /** some math */
        $key1_sec = pack('N', $key1_num / $key1_spc);
        $key2_sec = pack('N', $key2_num / $key2_spc);
    
        return md5($key1_sec . $key2_sec . $l8b, 1);
    }
    
    /** hand shake */
    private function doHandShake($socket, $req)
    {
        $acceptKey = $this->encry($req);
        
        $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
           "Upgrade: websocket\r\n" .
           "Connection: Upgrade\r\n" .
           "Sec-WebSocket-Accept: " . $acceptKey . "\r\n" .
           "\r\n";
        
        socket_write($socket, $upgrade.chr(0), strlen($upgrade.chr(0)));
        
        $this->handShake = true;
    }
    
    /** parse data frames */
    private function decode($buffer)
    {
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;
    
        if ($len === 126)  {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127)  {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else  {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }
        
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        
        return $decoded;
    }
    
    /** generate data frames */
    private function encode($msg)
    {
        $str = str_split($msg, 125);
        
        if (count($str) === 1) {
            return "\x81" . chr(strlen($str[0])) . $str[0];
        }
        
        $ns = '';
        
        foreach ($str as $o) {
            $ns .= "\x81" . chr(strlen($o)) . $o;
        }
        
        return $ns;
    }
    
    /** send messages to the client */
    private function send($client, $msg)
    {
        $msg = $this->encode($msg);
        socket_write($client, $msg, strlen($msg));
    }
    
    public function __construct($address, $port = 80)
    {
        $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) || die('socket_create() failed');
        
        socket_set_option($this->master, SQL_SOCKET, SO_REUSEADR, 1) || die('socket_option() failed');
        
        socket_bind($this->master, $address, $port) || die('socket_bind() failed');
        
        socket_listen($this->master, 2) || die('socket_listen() failed');
        
        $this->sockets[] = $this->master;
        
        /** loop for listen */
        for (;;) {
            $write = null;
            $except = null;
            
            socket_select($this->sockets, $write, $except, null);
            
            foreach ($this->sockets as $socket) {
                if ($socket === $this->master) {
                    $client = socket_accept($this->master);
                
                    if ($client < 0) {
                        echo 'socket_accept() failed';
                        continue;
                    } else {
                        /** connect($client); */
                        array_push($this->sockets, $client);
                        echo 'connect client';
                    }
                } else {
                    $bytes = @socket_recv($socket, $buffer, 2048, 0);
                   
                    if ($bytes == 0) {
                        return;
                    }
                    
                    if (!$this->handshake) {
                        /** echo 'shakeHands'; */
                        $this->doHandShake($socket, $buffer);
                    } else {
                        /** echo 'send file'; */
                        $buffer = $this->decode($buffer);
                        /** process($socket, $buffer); */
                    }
                }
            }
        }
    }
}
?>

The format of a Received Data Frame

 0               1               2               3              
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 4               5               6               7              
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
 8               9               10              11             
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
 12              13              14              15
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

2. new an object

<?php
$ws = new WebSocket('localhost');
?>