前面學習了HTML5中websocket的握手協議、打開和關閉連接等基礎內容,最近用php實現了與瀏覽器websocket的雙向通信。在學習概念的時候覺得看懂了的內容,真正在實踐過程中還是會遇到各種問題,網上也有一些關于php的websocket的實現,但是只有自己親手寫過之后才知道其中的感受。其中,google有一個開源的phpwebsocket類(https://code.google.com/p/phpwebsocket/),但是從其握手過程中可以明顯看出,這還是最初的websocket協議,請求頭中使用了兩個KEY,并非version 13(現行版本)。下面是本人實踐過程,同時封裝好了一個現行版本的php實現的實用的websocket類。
一、握手 1、客戶端發送請求websocket協議提供給javascript的API就是特別簡潔易用。
先看效果,客戶端和服務器端握手的結果如下:
封裝的類為WebSocket,address和port為類的屬性。
(1)建立socket并監聽1 function createSocket() 2 { 3 $this->master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) 4 or die('socket_create() failed:'.socket_strerror(socket_last_error())); 5 6 socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) 7 or die('socket_option() failed'.socket_strerror(socket_last_error())); 8 9 socket_bind($this->master, $this->address, $this->port)10 or die('socket_bind() failed'.socket_strerror(socket_last_error()));11 12 socket_listen($this->master,20)13 or die('socket_listen() failed'.socket_strerror(socket_last_error()));14 15 $this->say('Server Started : '.date('Y-m-d H:i:s'));16 $this->say('Master socket : '.$this->master);17 $this->say('Listening on : '.$this->address.' port '.$this->port.'');18 19 }
然后啟動監聽,同時要維護連接到服務器的用戶的一個數組(連接池),每連接一個用戶,就要push進一個,同時關閉連接后要刪除相應的用戶的連接。
1 html' target='_blank'>public function __construct($a, $p) 2 { 3 if ($a == 'localhost') 4 $this->address = $a; 5 else if (preg_match('/^[d.]*$/is', $a)) 6 $this->address = long2ip(ip2long($a)); 7 else 8 $this->address = $p; 9 10 if (is_numeric($p) && intval($p) > 1024 && intval($p) < 65536)11 $this->port = $p;12 else13 die ('Not valid port:' . $p);14 15 $this->createSocket();16 array_push($this->sockets, $this->master);17 }(2)建立連接
維護用戶的連接池
1 public function connect($clientSocket)2 {3 $user = new User();4 $user->id = uniqid();5 $user->socket = $clientSocket;6 array_push($this->users,$user);7 array_push($this->sockets,$clientSocket);8 $this->log($user->socket . ' CONNECTED!' . date('Y-m-d H-i-s'));9 }(3)回復響應頭
首先要獲取請求頭,從中取出Sec-Websocket-Key,同時還應該取出Host、請求方式、Origin等,可以進行安全檢查,防止未知的連接。
1 public function getHeaders($req) 2 { 3 $r = $h = $o = null; 4 if(preg_match('/GET (.*) HTTP/' , $req, $match)) 5 $r = $match[1]; 6 if(preg_match('/Host: (.*)/' , $req, $match)) 7 $h = $match[1]; 8 if(preg_match('/Origin: (.*)/', $req, $match)) 9 $o = $match[1];10 if(preg_match('/Sec-WebSocket-Key: (.*)/', $req, $match))11 $key = $match[1];12 13 return array($r, $h, $o, $key);14 }
之后是得到key然后進行websocket協議規定的加密算法進行計算,返回響應頭,這樣瀏覽器驗證正確后就握手成功了。這里涉及的詳細解析信息過程參見另一篇博文http://blog.csdn.net/u010487568/article/details/20569027
1 protected function wrap($msg='', $opcode = 0x1) 2 { 3 //默認控制幀為0x1(文本數據) 4 $firstByte = 0x80 | $opcode; 5 $encodedata = null; 6 $len = strlen($msg); 7 8 if (0 <= $len && $len <= 125) 9 $encodedata = chr(0x81) . chr($len) . $msg;10 else if (126 <= $len && $len <= 0xFFFF)11 {12 $low = $len & 0x00FF;13 $high = ($len & 0xFF00) >> 8;14 $encodedata = chr($firstByte) . chr(0x7E) . chr($high) . chr($low) . $msg;15 }16 17 return $encodedata; 18 }
其中我只實現了發送數據長度在2的16次方以下個字符的情況,至于長度為8個字節的超大數據暫未考慮。
1 private function doHandShake($user, $buffer) 2 { 3 $this->log('Requesting handshake...'); 4 $this->log($buffer); 5 list($resource, $host, $origin, $key) = $this->getHeaders($buffer); 6 7 //websocket version 13 8 $acceptKey = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); 9 10 $this->log('Handshaking...');11 $upgrade = 'HTTP/1.1 101 Switching Protocol' .12 'Upgrade: websocket' .13 'Connection: Upgrade' .14 'Sec-WebSocket-Accept: ' . $acceptKey . ''; //必須以兩個回車結尾15 $this->log($upgrade);16 $sent = socket_write($user->socket, $upgrade, strlen($upgrade));17 $user->handshake=true;18 $this->log('Done handshaking...');19 return true;20 }二、數據傳輸 1、客戶端
客戶端websocket的API非常容易,直接使用websocket對象的send方法即可。
1 ws.send(message);2、服務器端
客戶端發送的數據是經過瀏覽器支持的websocket進行了mask處理的,而根據規定服務器端返回的數據不能進行掩碼處理,但是需要按照協議的數據幀規定進行封裝后發送。因此服務器需要接收數據必須將接收到的字節流進行解碼。
1 protected function unwrap($clientSocket, $msg='') 2 { 3 $opcode = ord(substr($msg, 0, 1)) & 0x0F; 4 $payloadlen = ord(substr($msg, 1, 1)) & 0x7F; 5 $ismask = (ord(substr($msg, 1, 1)) & 0x80) >> 7; 6 $maskkey = null; 7 $oridata = null; 8 $decodedata = null; 9 10 //關閉連接11 if ($ismask != 1 || $opcode == 0x8)12 {13 $this->disconnect($clientSocket);14 return null;15 }16 17 //獲取掩碼密鑰和原始數據18 if ($payloadlen <= 125 && $payloadlen >= 0)19 {20 $maskkey = substr($msg, 2, 4);21 $oridata = substr($msg, 6);22 }23 else if ($payloadlen == 126)24 {25 $maskkey = substr($msg, 4, 4);26 $oridata = substr($msg, 8);27 }28 else if ($payloadlen == 127)29 {30 $maskkey = substr($msg, 10, 4);31 $oridata = substr($msg, 14);32 }33 $len = strlen($oridata);34 for($i = 0; $i < $len; $i++)35 {36 $decodedata .= $oridata[$i] ^ $maskkey[$i % 4];37 } 38 return $decodedata; 39 }
其中得到掩碼和控制幀后需要進行驗證,如果掩碼不為1直接關閉,如果控制幀為8也直接關閉。后面的原始數據和掩碼獲取是通過websocket協議的數據幀規范進行的。
效果如下
數據交互的過程非常的直接,其中“u”是服務器發送給客戶端的,然后客戶端發送一段隨機字符串給服務器。
1 ws.close();2、服務器端
需要將維護的用戶連接池移除相應的連接用戶。
1 public function disconnect($clientSocket) 2 { 3 $found = null; 4 $n = count($this->users); 5 for($i = 0; $i<$n; $i++) 6 { 7 if($this->users[$i]->socket == $clientSocket) 8 { 9 $found = $i;10 break;11 }12 }13 $index = array_search($clientSocket,$this->sockets);14 15 if(!is_null($found))16 { 17 array_splice($this->users, $found, 1);18 array_splice($this->sockets, $index, 1); 19 20 socket_close($clientSocket);21 $this->say($clientSocket.' DISCONNECTED!');22 }23 }
其中遇到的一個問題就是,如果將上述函數中的socket_close語句提出到if語句外面的時候,當瀏覽器連接到服務器后,F5刷新頁面后會發現出錯:
后來發現是重復關閉socket了,這個是因為在unwrap函數中遇到了控制幀直接關閉的原因。因此需要注意瀏覽器已經連接后進行刷新的操作。最后提供整個封裝好的類,https://github.com/OshynSong/web/blob/master/websocket.class.php
PHP編程鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。
新聞熱點
疑難解答