Documente Academic
Documente Profesional
Documente Cultură
Dec. 30, 2011 In this very first post on my new blog, I show you how Ive carried out a simple push system using WebSocket specified by the RFC 6455 document. Unfortunately, I cant bring you a live demo because of some hosting service restrictions that dont allow me to create sockets. So, I worked on localhost with my own computer using Ubuntu, Apache and PHP-CLI. Nevertheless, you can download scripts on my github account. Youll find one of several basic implementations of a communication between clients and a server by WebSocket in PHP. I deliberately didnt complicate the code (by adding other features or improvements) to make it easy to understand. 1. How does a WebSocket basically work? 2. The server side 1. Handshake 2. Unmasking/Encoding data frames 3. Pushing data 3. The client side 1. Receiving data 2. Leave the application 4. More information
Only after the handshake acceptance, both sides can communicate. Many clients can be connected to the server. For some performance reasons, the limit of clients can be set before creating a new socket.
Handshake
First of all, the handshake has to be done to let both, client and server, communicate. The browser introduces itself by sending HTTP headers, something like:
GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:5001 Origin: http://localhost Sec-WebSocket-Key: k2towQT28s50DtKptTjZbg== Sec-WebSocket-Version: 13
The Sec-WebSocket-Version determines the protocol version of the connection. The HTTP Headers could differ regarding the version. Thats why we make sure that the information that we want to extract are correct. For instance, Origin was SecWebSocket-Origin in the version 8. Moreover, server responses are not the same regarding versions. In the following code, it deals with the version 13. At the moment Im writing, the unique web-browser which supports this implementation is Google Chrome 16, others would be rejected.
/** * Do the handshaking between client and server * @param $client
* @param $headers */ private function handshake($client, $headers) { $this->console("Getting client WebSocket version..."); if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/", $headers, $match)) $version = $match[1]; else { $this->console("The client doesn't support WebSocket"); return false; } $this->console("Client WebSocket version is {$version}, (required: 13)"); if($version == 13) { // Extract header variables $this->console("Getting headers..."); if(preg_match("/GET (.*) HTTP/", $headers, $match)) $root = $match[1]; if(preg_match("/Host: (.*)\r\n/", $headers, $match)) $host = $match[1]; if(preg_match("/Origin: (.*)\r\n/", $headers, $match)) $origin = $match[1]; if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $headers, $match)) $key = $match[1]; $this->console("Client headers are:"); $this->console("\t- Root: ".$root); $this->console("\t- Host: ".$host); $this->console("\t- Origin: ".$origin); $this->console("\t- Sec-WebSocket-Key: ".$key); $this->console("Generating Sec-WebSocket-Accept key..."); $acceptKey = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; $acceptKey = base64_encode(sha1($acceptKey, true)); $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"; $this->console( "Sending this response to the client #{$client->getId()}:" ."\r\n".$upgrade ); socket_write($client->getSocket(), $upgrade); $client->setHandshake(true); $this->console("Handshake is successfully done!"); return true; } else { $this->console( "WebSocket version 13 required" ."(the client supports version {$version})" ); return false; } }
We have to encode the text before sending to the client. If a sent text is wrongly encoded, the client might close the connection or not correctly receive it. This is a basic implementation without encryption:
/** * Encode a text for sending to clients via ws:// * @param $text */ private function encode($text) { // 0x1 text frame (FIN + opcode) $b1 = 0x80 | (0x1 & 0x0f); $length = strlen($text); if($length 125 && $length < 65536) $header = pack('CCS', $b1, 126, $length); elseif($length >= 65536) $header = pack('CCN', $b1, 127, $length); return $header.$text; }
Pushing data
Now that we can send/receive messages and each side can display them properly, were going to see the pushing part. First of all, the script creates one process for the server + n processes for n connected clients. Were going to fork the server process to make it still accepts new clients and children processes will send data to clients. This method below have to be called after the handshake.
/** * Start a child process for pushing data * @param unknown_type $client */ private function startProcess($client) { $this->console("Start a child process"); $pid = pcntl_fork(); if($pid == -1) { die('could not fork'); } elseif($pid) { // process $client->setPid($pid); } else { // we are the child while(true) { // push something to the client $seconds = rand(2, 5); $this->send($client, "I am waiting {$seconds} seconds"); sleep($seconds); } } }
Note that we set the PID of the Client object in the server process. When a client leaves the application, it will send a quit command to the server (more information in the client side section). The server will treat the message and will send a kill request to the process. However, we can make it in another way: instead of forking the server process, we could execute another program and kill it properly.
+ 'the closing handshake (readyState '+this.readyState+')' ); else if(this.readyState == 3) write( 'Connection closed... The connection has been closed' + 'or could not be opened (readyState '+this.readyState+')' ); else write('Connection closed... (unhandled readyState '+this.readyState+')'); }; ws.onerror = function(event) { terminal.innerHTML = '<li style="color: red;">'+event.data+'</li>' + terminal.innerHTML; };
Receiving data
The client doesnt have to make a request to the server to know if there are new data. When there is something new, the server will send automatically to the concerned client. That new data is handled by the onmessage event.
ws.onmessage = function(msg) { write('Server says: '+msg.data); };