背景:
前段时间有个小需求:进入页面后需要根据数据库状态来自行跳转。去查了查,大部分都是用轮询做。自己觉得其实轮询也不是不可以,但是总觉比较尴尬。
因为本身的模式应该是以状态为主体,页面部分为客体,当状态发生变化的时候,主动把信号传给页面,然后页面跟着做跳转。如果用轮询,就变成了以页面为
主体,状态为客体,页面不断的给请求,如果状态变了,然后自己做跳转。然后看到了websocket,不过说实话不懂这东西。socket编程一直都有点模糊。于是
稍微看看这到底是神马。
主题:
去github上查了一波,找到了一个小实例很适合初学。
地址:https://github.com/ghedipunk/PHP-Websockets
主要步骤:
建立socket并监听
服务器端 :
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Failed: socket_create()");socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("Failed: socket_option()"); socket_bind($this->master, $addr, $port) or die("Failed: socket_bind()"); socket_listen($this->master,20) or die("Failed: socket_listen()");
客户端:
var host = "ws://127.0.0.1:9000"; // SET THIS TO YOUR SERVER socket = new WebSocket(host);
websocket协议的三次握手:
服务端
主要是对头部信息的拆分,拿到其中的sec-websocket-key,进行拼接后加密再编码返回给客户端。(PS对这块真不太熟,具体可参见下边的链接,很详细)
提取代码如下
protected function doHandshake($user, $buffer) { $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; $headers = array(); $lines = explode("\n",$buffer); foreach ($lines as $line) { if (strpos($line,":") !== false) { $header = explode(":",$line,2); $headers[strtolower(trim($header[0]))] = trim($header[1]); } elseif (stripos($line,"get ") !== false) { preg_match("/GET (.*) HTTP/i", $buffer, $reqResource); $headers[‘get‘] = trim($reqResource[1]); } } if (isset($headers[‘get‘])) { $user->requestedResource = $headers[‘get‘]; } else { // todo: fail the connection $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; } if (!isset($headers[‘host‘]) || !$this->checkHost($headers[‘host‘])) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } if (!isset($headers[‘upgrade‘]) || strtolower($headers[‘upgrade‘]) != ‘websocket‘) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } if (!isset($headers[‘connection‘]) || strpos(strtolower($headers[‘connection‘]), ‘upgrade‘) === FALSE) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } if (!isset($headers[‘sec-websocket-key‘])) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } else { } if (!isset($headers[‘sec-websocket-version‘]) || strtolower($headers[‘sec-websocket-version‘]) != 13) { $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13"; } if (($this->headerOriginRequired && !isset($headers[‘origin‘]) ) || ($this->headerOriginRequired && !$this->checkOrigin($headers[‘origin‘]))) { $handshakeResponse = "HTTP/1.1 403 Forbidden"; } if (($this->headerSecWebSocketProtocolRequired && !isset($headers[‘sec-websocket-protocol‘])) || ($this->headerSecWebSocketProtocolRequired && !$this->checkWebsocProtocol($headers[‘sec-websocket-protocol‘]))) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } if (($this->headerSecWebSocketExtensionsRequired && !isset($headers[‘sec-websocket-extensions‘])) || ($this->headerSecWebSocketExtensionsRequired && !$this->checkWebsocExtensions($headers[‘sec-websocket-extensions‘]))) { $handshakeResponse = "HTTP/1.1 400 Bad Request"; } // Done verifying the _required_ headers and optionally required headers. if (isset($handshakeResponse)) { socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse)); $this->disconnect($user->socket); return; } $user->headers = $headers; $user->handshake = $buffer; $webSocketKeyHash = sha1($headers[‘sec-websocket-key‘] . $magicGUID); $rawToken = ""; for ($i = 0; $i < 20; $i++) { $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2))); } $handshakeToken = base64_encode($rawToken) . "\r\n"; $subProtocol = (isset($headers[‘sec-websocket-protocol‘])) ? $this->processProtocol($headers[‘sec-websocket-protocol‘]) : ""; $extensions = (isset($headers[‘sec-websocket-extensions‘])) ? $this->processExtensions($headers[‘sec-websocket-extensions‘]) : ""; $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $handshakeToken$subProtocol$extensions\r\n"; socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse)); $this->connected($user); }
客户端
socket.onopen = function(msg) { }
接受和发送信息:
服务器端
在进行通信之前要对数据帧解码/编码才能获取/发送客户端信息。
握手过程代码都集中在 function doHandshake ()中。
数据侦编码代码如下:
/*数据帧编码 @param string 原始信息 @param obj 用户对象 @param string 消息类型 @param boolean 是否为附加数据 return string 经过编码的数据 */ protected function frame($message, $user, $messageType=‘text‘, $messageContinues=false) { switch ($messageType) { case ‘continuous‘: //附加数据帧 $b1 = 0; break; case ‘text‘: //文本数据帧 $b1 = ($user->sendingContinuous) ? 0 : 1; break; case ‘binary‘: //二进制数据帧 $b1 = ($user->sendingContinuous) ? 0 : 2; break; case ‘close‘: //连接关闭 $b1 = 8; break; case ‘ping‘: $b1 = 9; break; case ‘pong‘: $b1 = 10; break; } if ($messageContinues) { $user->sendingContinuous = true; } else { $b1 += 128; $user->sendingContinuous = false; } $length = strlen($message); $lengthField = ""; if ($length < 126) { $b2 = $length; } elseif ($length < 65536) { $b2 = 126; $hexLength = dechex($length); //$this->stdout("Hex Length: $hexLength"); if (strlen($hexLength)%2 == 1) { $hexLength = ‘0‘ . $hexLength; } $n = strlen($hexLength) - 2; for ($i = $n; $i >= 0; $i=$i-2) { $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField; } while (strlen($lengthField) < 2) { $lengthField = chr(0) . $lengthField; } } else { $b2 = 127; $hexLength = dechex($length); if (strlen($hexLength)%2 == 1) { $hexLength = ‘0‘ . $hexLength; } $n = strlen($hexLength) - 2; for ($i = $n; $i >= 0; $i=$i-2) { $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField; } while (strlen($lengthField) < 8) { $lengthField = chr(0) . $lengthField; } } return chr($b1) . chr($b2) . $lengthField . $message; }
数据帧解码如下:
/*数据帧解码 @param string 原始数据帧 @param obj 用户实体 return string 用户端返回的数据 */ protected function deframe($message, &$user) { //echo $this->strtohex($message); $headers = $this->extractHeaders($message); $pongReply = false; $willClose = false; switch($headers[‘opcode‘]) { case 0: case 1: case 2: break; case 8: // todo: close the connection $user->hasSentClose = true; return ""; case 9: $pongReply = true; case 10: break; default: //$this->disconnect($user); // todo: fail connection $willClose = true; break; } /* Deal by split_packet() as now deframe() do only one frame at a time. if ($user->handlingPartialPacket) { $message = $user->partialBuffer . $message; $user->handlingPartialPacket = false; return $this->deframe($message, $user); } */ if ($this->checkRSVBits($headers,$user)) { return false; } if ($willClose) { // todo: fail the connection return false; } $payload = $user->partialMessage . $this->extractPayload($message,$headers); if ($pongReply) { $reply = $this->frame($payload,$user,‘pong‘); socket_write($user->socket,$reply,strlen($reply)); return false; } if ($headers[‘length‘] > strlen($this->applyMask($headers,$payload))) { $user->handlingPartialPacket = true; $user->partialBuffer = $message; return false; } $payload = $this->applyMask($headers,$payload); if ($headers[‘fin‘]) { $user->partialMessage = ""; return $payload; } $user->partialMessage = $payload; return false; }
针对上述过程可以参考参考http://www.qixing318.com/article/643129914.html这篇文章,很详细。
客户端:
客户端相对来说简单一些,接受信息方法为 socket.onmessage,发送消息为 socket.send 方法。
总结:以原来那个需求来说,websocket是有点大材小用了,还是老老实实轮询来的方便和实际。。。。。。。