DWilliams Posted November 15, 2011 Share Posted November 15, 2011 I've been tinkering with websockets lately and attempting to create a server in PHP. I read this summary on Wikipedia to gain an understanding of the protocol. It seems rather easy to implement, but I'm having some issues. I have a test page with JavaScript attempting to open a websocket and a socket server script in PHP that does nothing other than listen for and accept connections. Now, this seems to work on the surface. A socket is opened and immediately closes, which is what I would expect since the server is not coded to follow websocket protocol. What is confusing me, however, is that no handshake request is ever sent by the JS client. I have wireshark set up and capturing all packets going back and forth, and the initial handshake packet described by Wikipedia is never sent. Maybe my sleep deprived brain is missing something obvious here, but shouldn't the client always send the first handshake packet as soon as the socket is opened, regardless of whether the server in question is just a dumb script that doesn't know what to do with it? Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/ Share on other sites More sharing options...
kicken Posted November 15, 2011 Share Posted November 15, 2011 Without seeing the code behind it, can't really say much about why you see what you do. If you php script doesn't hold the socket open, probably it's being closed before the handshake can be sent. Such as if your doing like $client = socket_accept($server); socket_close($client); //or just ending script the socket will open then close immediately, without giving a chance for anything to be sent. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288322 Share on other sites More sharing options...
DWilliams Posted November 15, 2011 Author Share Posted November 15, 2011 The server code is just some generic PHP socket server script I picked up for testing purposes: <?php // Set time limit to indefinite execution set_time_limit (0); function server_log($msg) { echo '[' . date('G:i:s') . "] $msg\n"; } // Set the ip and port we will listen on $address = 'localhost'; $port = 10000; $max_clients = 10; // Array that will hold client information $client = Array(); // Create a TCP Stream socket $sock = socket_create(AF_INET, SOCK_STREAM, 0); // Bind the socket to an address/port socket_bind($sock, $address, $port) or die('Could not bind to address'); server_log("Server started on {$address}:{$port}"); // Start listening for connections socket_listen($sock); // Loop continuously while (true) { // Setup clients listen socket for reading $read[0] = $sock; for ($i = 0; $i < $max_clients; $i++) { if (isset($client[$i]['sock'])) $read[$i + 1] = $client[$i]['sock']; } // Set up a blocking call to socket_select() if (socket_select($read, $write = NULL, $except = NULL, $tv_sec = 5) < 1) continue; /* if a new connection is being made add it to the client array */ if (in_array($sock, $read)) { for ($i = 0; $i < $max_clients; $i++) { if (empty($client[$i]['sock'])) { $client[$i]['sock'] = socket_accept($sock); echo "New client connected $i\r\n"; break; } elseif ($i == $max_clients - 1) echo "Too many clients...\r\n"; } } // end if in_array // If a client is trying to write - handle it now for ($i = 0; $i < $max_clients; $i++) { // for each client if (isset($client[$i]['sock'])) { if (in_array($client[$i]['sock'], $read)) { $input = socket_read($client[$i]['sock'], 1024); if ($input == null) { echo "Client disconnecting $i\r\n"; // Zero length string meaning disconnected unset($client[$i]); } else { echo "New input received $i\r\n"; // send it to the other clients for ($j = 0; $j < $max_clients; $j++) { if (isset($client[$j]['sock']) && $j != $i) { echo "Writing '$input' to client $j\r\n"; socket_write($client[$j]['sock'], $input, strlen($input)); } } if ($input == 'exit') { // requested disconnect socket_close($client[$i]['sock']); } } } else { echo "Client disconnected $i\r\n"; // Close the socket socket_close($client[$i]['sock']); unset($client[$i]); } } } } // end while // Close the master sockets socket_close($sock); That seems like it should work for purposes of getting the JS websocket to send its handshake Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288333 Share on other sites More sharing options...
kicken Posted November 15, 2011 Share Posted November 15, 2011 Do you watch the output of the PHP server script while testing? Can you see it output a message when the connection is accepted and when data is received? I see your binding to localhost, note that Wireshark will not capture packets going across localhost without some hacks. It'd be easier to probably rely on output from the server script, or setup the server on a different host so your connecting via LAN/Internet and wireshark can see the packets. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288345 Share on other sites More sharing options...
DWilliams Posted November 15, 2011 Author Share Posted November 15, 2011 Yeah, the script outputs that it's being connected to: New client connected 0 Client disconnected 0 And I'm capturing loopback with wireshark. I can see all the packets for downloading the page itself, just nothing about websockets or the HTTP upgrade packet. I've looked through every HTTP packet it produces and run searches for any of the terms that should be in the handshake packet Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288348 Share on other sites More sharing options...
DWilliams Posted November 15, 2011 Author Share Posted November 15, 2011 Hmm I tried setting up the websocket example from this page and it doesn't work either. Similar result. No connection is made although the server script still shows something hitting it. No handshake packets are sent on this example either. Maybe something else is the issue here? I'm not running through a proxy or anything. I've tried it both from the local machine as well as from another computer and the result is the same EDIT: I've also tried in both Firefox 8 and Chrome 15 Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288364 Share on other sites More sharing options...
DWilliams Posted November 16, 2011 Author Share Posted November 16, 2011 After some more tinkering I think I have confirmed that it is indeed the server script that's causing the problem. I don't see anything wrong with it but with new server code I was able to capture the packet. Now my problem is that the WebSocket protocol seems to be adapting so rapidly that trying to apply it seems like trying to hit a moving target. This is the packet I captured (sent from Chrome): GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 192.168.1.29:12345 Sec-WebSocket-Origin: http://192.168.1.29 Sec-WebSocket-Key: 0fEMxyrlgsuDp8IA02nxCA== Sec-WebSocket-Version: 8 That differs from all the examples I can find on the internet as well as the Wikipedia page, so I guess I have to hunt down the IETF protocol description for version 8 and implement it off that... Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288788 Share on other sites More sharing options...
kicken Posted November 17, 2011 Share Posted November 17, 2011 Now my problem is that the WebSocket protocol seems to be adapting so rapidly that trying to apply it seems like trying to hit a moving target. I was messing with Websockets toward the middle of the year and had developed some php scripts to manage them. I went to pull them up to post as an example, found out they don't work anymore because the protocol changed. I looked at updating it, didn't have the time though. Not sure how much more it will be changing, Hopefully it gets nailed down soon. I believe the current spec is http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17. The 'version 8' in the header maybe corrisponds to http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-08. I'd probably look at the newest and implement it, probably will still work with what chrome has implemented. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288822 Share on other sites More sharing options...
DWilliams Posted November 17, 2011 Author Share Posted November 17, 2011 I've implemented it with hybi-17 but it doesn't work. I don't see any differences in the handshake between what I implemented and the IETF paper for version 8. This is my handshake function: function dohandshake($user, $buffer) { server_log(1, 'Requesting handshake...'); // Determine which version of the WebSocket protocol the client is using if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/ ", $buffer, $match)) $version = $match[1]; else return false; if($version == { // Extract header variables if(preg_match("/GET (.*) HTTP/" ,$buffer,$match)){ $r=$match[1]; } if(preg_match("/Host: (.*)\r\n/" ,$buffer,$match)){ $h=$match[1]; } if(preg_match("/Sec-WebSocket-Origin: (.*)\r\n/",$buffer,$match)){ $o=$match[1]; } if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$buffer,$match)){ $k = $match[1]; } // Generate our Socket-Accept key based on the IETF specifications $accept_key = $k . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; $accept_key = sha1($accept_key, true); $accept_key = base64_encode($accept_key); $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: $accept_key"; socket_write($user->socket, $upgrade, strlen($upgrade)); $user->handshake = true; return true; } else { server_log("Client is trying to use an unsupported WebSocket protocol ({$version})"); return false; } } I can't tell for sure whether it's working but I suspect the handshake is not working.The JS client connects to it but when it tries to send data, I get a "Error: INVALID_STATE_ERR: DOM Exception 11" exception, and the readyState never changes from 0. When I feed the sample keys into my function it produces the correct accept key... Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288853 Share on other sites More sharing options...
kicken Posted November 17, 2011 Share Posted November 17, 2011 Try echoing back the Sec-WebSocket-Origin: and Sec-WebSocket-Location: headers. Just send them in the response with the same values that were sent to you. I seem to recall chrome not accepting unless they were sent when I was working on it before. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288970 Share on other sites More sharing options...
DWilliams Posted November 17, 2011 Author Share Posted November 17, 2011 So it turns out the solution was much simpler than it seemed. The browser expects "\r\n\r\n" at the end of the handshake. Works fine with that, I now have a working websocket server Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1288994 Share on other sites More sharing options...
ignace Posted November 18, 2011 Share Posted November 18, 2011 Can you post the entire script so others may use it/learn from your experience? Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1289251 Share on other sites More sharing options...
DWilliams Posted November 18, 2011 Author Share Posted November 18, 2011 Can you post the entire script so others may use it/learn from your experience? Sure, here's the server code: http://pastebin.com/a1Mzt3pC Pretty much any simple websocket will work for it, here's the one I was using to test: http://code.google.com/p/phpwebsocket/source/browse/trunk/%20phpwebsocket/client.html I don't take credit for most of it, I mainly pieced it together from several outdated and/or half-working sources. My main contribution to it is the handshaking code. It has flaws obviously, the main being is that it isn't coded to properly respond to ping/pong/close packets but the framework is there and ready to be implemented. It's a good starting point for somebody wanting to implement a server, though. Also, for people not used to working with sockets in PHP take note that by default PHP has no support for listening sockets. In Linux/mac you'll need to recompile with --enable-sockets and under Windows you have to enable some module. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1289423 Share on other sites More sharing options...
kicken Posted November 19, 2011 Share Posted November 19, 2011 I spent a little time fixing up the code I had written for websockets to handle the updated protocol. Here's the code, if anyone finds it useful. It is based around PHP's Stream's api rather than the socket extension. It's not a full and complete implementation, but should be enough to get things started for anyone really interested in websockets. WebSocketConnection.class.php: <?php require __DIR__.DIRECTORY_SEPARATOR.'WebSocketMessage.class.php'; require __DIR__.DIRECTORY_SEPARATOR.'ILogger.interface.php'; class WebsocketConnection { private $mReadBuffer; protected $mProtoType; protected $mProtoVersion; protected $mState; protected $mReceivedClose; protected $mSentClose; protected $mCloseStartedOn; protected $mStream; protected $mRequestUri; protected $mHeaders; protected $mLoggers; const HANDSHAKE=1; const ESTABLISHED=2; const CLOSING=3; const CLOSED=4; const PROTO_HYBI='hybi'; const PROTO_HIXIE='hixie'; public function __get($nm){ throw new Exception('Unknown property '.$nm); } public function __set($nm,$v){ throw new Exception('Unknown property '.$nm); } public function __construct(){ $this->mState = self::CLOSED; $this->mReadBuffer = null; $this->mStream = null; $this->mHeaders=array(); $this->mRequestUri=''; $this->mLoggers=array(); $this->mReceivedClose=false; $this->mSentClose=false; $this->mCloseStartedOn=0; } public function addLogger(ILogger $logger){ $this->mLoggers[]=$logger; } private function log($str){ foreach ($this->mLoggers as $l){ $l->log($str); } } public function GetStream(){ return $this->mStream; } public function IsConnected(){ return $this->mState == self::ESTABLISHED || $this->mState == self::CLOSING; } /** * Fill the read buffer from the stream * */ protected function fillReadBuffer(){ $r = array($this->mStream); $w = $e = null; while (stream_select($r, $w, $e, 0, 0) > 0){ $this->log('Reading data from stream'); $data = fread($this->mStream, 1024); if (strlen($data)==0){ //Reading 0 data means the stream has been closed $this->Close(true); return; } $this->mReadBuffer .= $data; //$this->log('Read data: '.$data); $r = array($this->mStream); $w = $e = null; } } protected function WriteStream($str){ fwrite($this->mStream, $str); } /** * Locates the first instance of a set of strings * Returns the position in $str, optionally returns the length * of the string found in $oLen */ protected function indexOfFirst($str, $findArr, &$oLen=null){ $posArr = array(); foreach ($findArr as $f){ $posArr[] = array(strpos($str, $f), strlen($f)); } $finalPos = false; $oLen = null; foreach ($posArr as $p){ if ($p[0] !== false){ if ($finalPos === false || $finalPos > $p[0]){ $finalPos = $p[0]; $oLen = $p[1]; } } } return $finalPos; } /** * Read up to (and including) the next \r\n from the input buffer * Returns false if a full line cannot be read. * */ protected function readLine(&$line){ $pos = $this->indexOfFirst($this->mReadBuffer, array("\r\n", "\n", "\r"), $len); if ($pos !== false){ $end = $pos+$len; $line = substr($this->mReadBuffer, 0, $end); $this->mReadBuffer = substr($this->mReadBuffer, $end); return true; } return false; } /** * Reads exactly the specified number of bytes */ protected function readExactly($bytes, &$output){ if (strlen($this->mReadBuffer) > $bytes){ $output = substr($this->mReadBuffer, 0, $bytes); $this->mReadBuffer = substr($this->mReadBuffer, $bytes); return true; } return false; } /** * Peeks at the buffer by reading the requested data but not removing it from the buffer. * */ protected function peekExactly($bytes, &$output){ if (strlen($this->mReadBuffer) > $bytes){ $output = substr($this->mReadBuffer, 0, $bytes); return true; } return false; } /** * Read the opening handshake from other stream resources * */ protected function readHeaders(){ $endOfHeaders=false; do { $this->fillReadBuffer(); $curHeader=array(); while (!$endOfHeaders && $this->readLine($line)){ $line = rtrim($line, "\r\n"); if ($line==''){ $endOfHeaders=true; } else { if (substr($line, 0, 3) == 'GET'){ $resourceStart = strpos($line, ' ')+1; $resourceEnd = strpos($line, ' ', $resourceStart); $this->mRequestUri = substr($line, $resourceStart, $resourceEnd-$resourceStart); } else { if (!ctype_space($line[0])){ if (!empty($curHeader)){ $this->mHeaders[$curHeader[0]] = $curHeader[1]; } $pos = strpos($line, ':'); $curHeader[0] = rtrim(strtoupper(substr($line, 0, $pos))); $curHeader[1] = ltrim(substr($line, $pos+1)); } else { $curHeader[1] .= ' '.ltrim($line); } } } } } while (!$endOfHeaders); if ($curHeader){ $this->mHeaders[$curHeader[0]] = $curHeader[1]; } } /** * Upgrades the connection, processing the opening handshake and enabling the websocket. * */ public function DoUpgrade($stream){ if (!is_resource($stream)){ throw new InvalidArgumentException('Stream must be specified and must be a resource'); } $this->mStream = $stream; if (function_exists('stream_set_read_buffer')){ stream_set_read_buffer($this->mStream, 0); } stream_set_write_buffer($this->mStream, 0); stream_set_timeout($this->mStream, 2, 0); return $this->ProcessHandshake(); } protected function ProcessHandshake(){ $this->readHeaders(); $this->log('Headers are: '.print_r($this->mHeaders, true)); $responseCode = 101; $acceptKey = null; if (isset($this->mHeaders['SEC-WEBSOCKET-KEY1']) && isset($this->mHeaders['SEC-WEBSOCKET-KEY2'])){ $this->mProtoType = self::PROTO_HIXIE; $this->mProtoVersion = 76; } else if (isset($this->mHeaders['SEC-WEBSOCKET-KEY'])){ $this->mProtoType = self::PROTO_HYBI; $this->mProtoVersion = 17; } else { $this->log('Unknown websocket protocol.'); throw new Exception('Unknown websocket protocol'); } if (isset($this->mHeaders['SEC-WEBSOCKET-VERSION'])){ $this->mProtoVersion = intval($this->mHeaders['SEC-WEBSOCKET-VERSION']); } if ($this->mProtoType==self::PROTO_HIXIE){ while (!$this->readExactly(8, $last8)){ $this->fillReadBuffer(); } $keys = array(null, null, $last8); try { foreach (array($this->mHeaders['SEC-WEBSOCKET-KEY1'], $this->mHeaders['SEC-WEBSOCKET-KEY2']) as $n=>$val){ $digits = ''; $spaces = 0; for ($i=0; $i<strlen($val); $i++){ $c = $val[$i]; if (ctype_digit($c)){ $digits .= $c; } else if (ctype_space($c)){ $spaces++; } } if ($spaces==0){ throw new Exception('No spaces found in key. Must be at least one.'); } $keys[$n] = floatval(floatval($digits)/$spaces); } $finalKey = pack('N2a8', intval($keys[0]), intval($keys[1]), $keys[2]); $acceptKey = md5($finalKey, true); } catch (Exception $e){ $this->log('Unable to generate accept key.'); $responseCode=400; break; } } else { $key = trim($this->mHeaders['SEC-WEBSOCKET-KEY']); $key = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; $acceptKey = base64_encode(sha1($key, true)); } if (!isset($this->mHeaders['UPGRADE']) || strcasecmp($this->mHeaders['UPGRADE'], 'websocket') != 0){ $this->log('No or Bad upgrade header'); $this->log('$this->mHeaders[uPGRADE] = '.$this->mHeaders['UPGRADE']); $this->log('strcasecmp = '.strcasecmp($this->mHeaders['UPGRADE'], 'websocket')); var_dump($this->mHeaders['UPGRADE']); $responseCode = 400; } /*if (isset($this->mHeaders['SEC-WEBSOCKET-PROTOCOL'])){ $requestedProtos = explode(',', $this->mHeaders['SEC-WEBSOCKET-PROTOCOL']); $hasValidProto = false; foreach ($requestedProtos as $p){ if ($this->mServer->IsValidProtocol($p)){ $hasValidProto = true; } } if (!$hasValidProto){ $responseCode = 406; break; } }*/ if (!$acceptKey){ $responseCode=500; } $origin = isset($this->mHeaders['ORIGIN'])?$this->mHeaders['ORIGIN']:'null'; $resource = sprintf('ws://%s%s', $this->mHeaders['HOST'], $this->mRequestUri); $this->log('Response Code is '.$responseCode); switch ($responseCode){ case 101: $response = "HTTP/1.1 101 Upgraded\r\n"; $response .= "Upgrade: WebSocket\r\n"; $response .= "Connection: Upgrade\r\n"; $response .= "Sec-WebSocket-Origin: {$origin}\r\n"; $response .= "Sec-WebSocket-Location: {$resource}\r\n"; if (isset($this->mHeaders['SEC-WEBSOCKET-PROTOCOL'])){ $response .= "Sec-WebSocket-Protocol: {$this->mHeaders['SEC-WEBSOCKET-PROTOCOL']}\r\n"; } if ($this->mProtoType==self::PROTO_HIXIE){ $response .= "\r\n{$acceptKey}"; } else { $response .= "Sec-WebSocket-Accept: {$acceptKey}\r\n"; $response .= "\r\n"; } break; case 400: $response = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"; break; case 406: $response = "HTTP/1.1 406 Not Acceptable\r\nConnection: close\r\n\r\n"; break; case 500: default: $response = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n"; break; } $this->WriteStream($response); if ($responseCode == 101){ $this->mState = self::ESTABLISHED; return true; } else { fclose($this->mStream); $this->mStream = null; $this->mState = self::CLOSED; return false; } } public function Close($force=false){ if (!$this->mCloseStartedOn){ $this->mCloseStartedOn=time(); $this->log('Starting close at: '.$this->mCloseStartedOn); } if ($force || ($this->mSentClose && $this->mReceivedClose) || time()-$this->mCloseStartedOn > 10){ if ($this->mStream) fclose($this->mStream); $this->mState = self::CLOSED; $this->mStream = null; $this->log('Shutdown stream, socket is closed.'); } else { $this->mState = self::CLOSING; if (!$this->mSentClose){ $this->log('Sending close frame.'); $m = new WebSocketMessage(WebSocketMessage::TYPE_CLOSE); $this->SendMessage($m); } } } public function GetMessage(){ $this->fillReadBuffer(); $m = $this->ReadMessage(); if ($m && $m->Type == WebSocketMessage::TYPE_CLOSE){ $this->mReceivedClose = true; $this->Close(); $m = null; } return $m; } public function SendMessage(WebSocketMessage $msg){ if ($this->mProtoType == self::PROTO_HYBI){ $this->sendHybiMessage($msg); } else { $this->sendHixieMessage($msg); } if ($msg->Type == WebSocketMessage::TYPE_CLOSE){ $this->mSentClose=true; $this->Close(); } } protected function ReadMessage(){ $buff = $this->mReadBuffer; $ret=null; if ($this->mProtoType == self::PROTO_HYBI){ $ret=$this->readHybiMessage($buff); } else { $ret=$this->readHixieMessage($buff); } if ($ret){ $this->mReadBuffer = $buff; } return $ret; } protected function readHixieMessage(&$buffer){ $ret = null; if (strlen($buffer) > 0){ $type = ord($buffer[0]); if (($type & 0x80) == 0x80 && isset($buffer[1])){ $length = 0; $byteNum=1; do { $lenByte=ord($buffer[$byteNum++]); $length = ($length*128)+($lenByte&0x7F); } while (isset($buffer[$byteNum]) && ($lenByte&0x80)==0x80); if ($type == 0xFF && $length == 0){ $ret = new WebSocketMessage(WebSocketMessage::TYPE_CLOSE, null); $buffer = substr($buffer, min(strlen($buffer),2)); } else if ($length > 0){ $length = min(strlen($buffer),$length); $buffer = substr($buffer, $length); } } else if (($type&0x80) == 0){ $payload=array(); $byteNum=1; while (isset($buffer[$byteNum]) && ord($buffer[$byteNum]) != 0xFF){ $payload[] = $buffer[$byteNum++]; } if ($type == 0){ $ret = new WebSocketMessage(WebSocketMessage::TYPE_TEXT, implode('', $payload), count($payload)); $buffer = substr($buffer, $byteNum+1); } } } return $ret; } protected function readHybiMessage(&$buffer){ $ret = null; do { if (strlen($buffer) > 2){ $bytes = substr($buffer, 0, 2); $buffer = substr($buffer, 2); $byte = ord($bytes[0]); $flags = ($byte&0xF0)>>4; $opcode = $byte&0x0F; $byte = ord($bytes[1]); $ismasked = ($byte&0x80)>>7; $payloadLen = $byte&0x7F; $this->log(sprintf("Flags: %d; Opcode: %d; Masked: %d; Length: %d", $flags, $opcode, $ismasked, $payloadLen)); $validLen=false; switch ($payloadLen){ case '126': if (strlen($buffer) > 2){ $payloadLenBytes = substr($buffer, 0, 2); $buffer = substr($buffer, 2); $payloadLen = unpack('nlen', $data); $payloadLen = $payloadLen['len']; $validLen=true; } break; case '127': if (strlen($buffer) > { $payloadLenBytes = substr($buffer, 0, ; $buffer = substr($buffer, ; $data = unpack("Nhi/Nlo", $payloadLenBytes); $payloadLen = ($data['hi']<<32)|$data['lo']; $validLen=true; } break; default: /* $payloadLen is itself. */ } if ($opcode == 0x8){ //Disable mask on closing frames $ismasked=0; } if ($ismasked && strlen($buffer) > 4){ $maskKey = substr($buffer, 0, 4); $buffer = substr($buffer, 4); $this->log('Mask key: '.$maskKey); } else if ($ismasked){ break; //No mask given, but one should be given. } if (strlen($buffer) >= $payloadLen){ $payload = substr($buffer, 0, $payloadLen); $buffer = substr($buffer, $payloadLen); //$this->log('Payload (pre-mask): '.$payload); if ($ismasked){ $payload = $this->applyMask($payload, $payloadLen, $maskKey); //$this->log('Payload (post-mask): '.$payload); } $ret = new WebsocketMessage($opcode, $payload, $payloadLen); if ($flags&0x8 == 0){ $obj = $this->readHybiMessage($buffer); if ($obj){ $ret->Data .= $obj->Data; $ret->DataLength += $obj->DataLength; $buffer = $newBuffer; } } } } } while (false); return $ret; } protected function applyMask($payload, $payloadLen, $mask){ $newPayload=''; for ($i=0; $i<$payloadLen; $i++){ $keyByte = unpack('cchar', $mask[$i%4]); $keyByte = $keyByte['char']; $newPayload .= chr(ord($payload[$i]) ^ $keyByte); } return $newPayload; } protected function sendHybiMessage($msg){ $len = $msg->DataLength; if ($len > 125){ $len = 126; } $packet = pack('cc', 0x80 | $msg->Type, 0x00 | $len); if ($len == 126){ $packet .= pack('n', $msg->DataLength); } $packet .= $msg->Data; $this->WriteStream($packet); } protected function sendHixieMessage($msg){ if ($msg->Type == WebSocketMessage::TYPE_CLOSE){ $packet = chr(0xff).chr(0x00); } else { $utf8 = iconv('ISO-8859-1', 'UTF-8', $msg->Data); $packet = chr(0).$utf8.chr(0xFF); } $this->WriteStream($packet); } } ILogger.interface.php <?php interface ILogger { public function Log($str); } WebSocketMessage.class.php <?php class WebSocketMessage { public $Data; public $DataLength; public $Type; const TYPE_TEXT = 0x1; const TYPE_BINARY = 0x2; const TYPE_CLOSE = 0x8; public function __construct($Type, $Data='', $Len=null){ $this->Data = $Data; $this->Type = $Type; $this->DataLength = $Len?:strlen($Data); } public function __get($nm){ throw new Exception('Unknown property '.$nm); } public function __set($nm,$v){ throw new Exception('Unknown property '.$nm); } } And a sample server script: <?php require 'WebSocketConnection.class.php'; class EchoLogger implements ILogger { public function log($str){ echo rtrim($str), "\r\n"; } } $server=stream_socket_server('tcp://0.0.0.0:8010'); echo "Listening for connections\r\n"; while ($client=stream_socket_accept($server)){ $ws = new WebsocketConnection(); $ws->addLogger(new EchoLogger()); echo "Upgrading connection\r\n"; if ($ws->DoUpgrade($client)){ echo "Upgrade complete, entering loop\r\n"; do { $r = array($ws->GetStream()); $w = $e = null; if (stream_select($r, $w, $e, 10, 0) > 0){ if ($m = $ws->GetMessage()){ $m->Data = md5($m->Data); $m->DataLength = strlen($m->Data); $ws->SendMessage($m); } } } while($ws->IsConnected()); } else { echo "Upgrade failed\r\n"; } } Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1289467 Share on other sites More sharing options...
ignace Posted November 20, 2011 Share Posted November 20, 2011 Thank you both. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1289721 Share on other sites More sharing options...
jawef Posted November 25, 2011 Share Posted November 25, 2011 Thank you also DWilliams , I was writing a PHP Websocket tutorial for my colleagues and I just realized from your code that when I decoded from hybi 10 I just ignored close , ping and pong types. When I will finish it I will open new thread since I have many concerns about server performance using Websockets. Again thank you all. Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1291061 Share on other sites More sharing options...
bonewhat Posted June 11, 2012 Share Posted June 11, 2012 Thanks for the readHybiMessage! In case anyone else reads and tries to use that code please be aware of the typo in WebSocketConnection.class.php in the case '126' in the readHybiMessage it should be $payloadLen = unpack('nlen', $payloadLenBytes); instead of $payloadLen = unpack('nlen', $data); and yes, i registered only to thank and come with the correction =) Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1353022 Share on other sites More sharing options...
RobertP Posted June 11, 2012 Share Posted June 11, 2012 This is not mine, i just found it on my hd and thought it might help. not sure if it even works. i came across it once, and thought i should save it. <?php error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); date_default_timezone_set('America/Chicago'); $master = WebSocket('192.168.1.29', 12345); $sockets = array($master); $users = array(); while(true) { $changed = $sockets; socket_select($changed,$write=NULL,$except=NULL,NULL); foreach($changed as $socket) { if($socket == $master) { $client = socket_accept($master); if($client < 0) { server_log('socket_accept() failed', 2); continue; } else { connect($client); } } else { $bytes = socket_recv($socket,$buffer,2048,0); if($bytes==0) { disconnect($socket); } else { $user = getuserbysocket($socket); if(!$user->handshake) { dohandshake($user,$buffer); } else { // Decode the WebSocket data and process accordingly $data = hybi10Decode($buffer); if($data['type'] == 'text') { process($user, $data['payload']); } elseif($data['type'] == 'ping') { } elseif($data['type'] == 'pong') { } elseif($data['type'] == 'close') { } } } } } } //--------------------------------------------------------------- function server_log($msg, $sev = 1) { echo '[' . date('G:i:s') . "] $msg\n"; } function process($user, $msg) { server_log("Message received: ". $msg); } function send($client,$msg) { server_log("> ".$msg, 1); $msg = hybi10Encode($msg, 'close'); socket_write($client,$msg,strlen($msg)); } function WebSocket($address,$port) { $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() failed"); socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1) or die("socket_option() failed"); socket_bind($master, $address, $port) or die("socket_bind() failed"); socket_listen($master,20) or die("socket_listen() failed"); server_log('Server Started : ' . date('Y-m-d H:i:s'), 1); server_log("Master socket : $master", 1); server_log("Listening on : {$address}:{$port}", 1); return $master; } function connect($socket) { global $sockets,$users; $user = new User(); $user->id = uniqid(); $user->socket = $socket; array_push($users,$user); array_push($sockets,$socket); server_log('New client connected', 1); } function disconnect($socket){ global $sockets,$users; $found=null; $n=count($users); for($i=0;$i<$n;$i++){ if($users[$i]->socket==$socket){ $found=$i; break; } } if(!is_null($found)){ array_splice($users,$found,1); } $index = array_search($socket,$sockets); socket_close($socket); console($socket." DISCONNECTED!"); if($index>=0){ array_splice($sockets,$index,1); } } function dohandshake($user, $buffer) { server_log('Requesting handshake...', 1); // Determine which version of the WebSocket protocol the client is using if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/ ", $buffer, $match)) $version = $match[1]; else return false; if($version == { // Extract header variables if(preg_match("/GET (.*) HTTP/" ,$buffer,$match)){ $r=$match[1]; } if(preg_match("/Host: (.*)\r\n/" ,$buffer,$match)){ $h=$match[1]; } if(preg_match("/Sec-WebSocket-Origin: (.*)\r\n/",$buffer,$match)){ $o=$match[1]; } if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$buffer,$match)){ $k = $match[1]; } // Generate our Socket-Accept key based on the IETF specifications $accept_key = $k . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; $accept_key = sha1($accept_key, true); $accept_key = base64_encode($accept_key); $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: $accept_key\r\n\r\n"; socket_write($user->socket, $upgrade, strlen($upgrade)); $user->handshake = true; return true; } else { server_log("Client is trying to use an unsupported WebSocket protocol ({$version})", 1); return false; } } function getuserbysocket($socket) { global $users; $found = null; foreach($users as $user) { if($user->socket==$socket) { $found = $user; break; } } return $found; } function hybi10Encode($payload, $type = 'text', $masked = true) { $frameHead = array(); $frame = ''; $payloadLength = strlen($payload); switch($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), ; $frameHead[1] = ($masked === true) ? 255 : 127; for($i = 0; $i < 8; $i++) { $frameHead[$i+2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 (close connection if frame too big) if($frameHead[2] > 127) { $this->close(1004); return false; } } elseif($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), ; $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; } // convert frame-head to string: foreach(array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if($masked === true) { // generate a random mask: $mask = array(); for($i = 0; $i < 4; $i++) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: $framePayload = array(); for($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame; } function hybi10Decode($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; // close connection if unmasked frame is received: if($isMasked === false) { $this->close(1002); } switch($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: // Close connection on unknown opcode: $this->close(1003); break; } if($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; } elseif($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; } else { $mask = substr($data, 2, 4); $payloadOffset = 6; } $dataLength = strlen($data); if($isMasked === true) { for($i = $payloadOffset; $i < $dataLength; $i++) { $j = $i - $payloadOffset; $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData; } class User{ var $id; var $socket; var $handshake; } Quote Link to comment https://forums.phpfreaks.com/topic/251186-websocket-protocol/#findComment-1353029 Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.