Websocket協議之php實現


前面學習了HTML5中websocket的握手協議、打開和關閉連接等基礎內容,最近用php實現了與瀏覽器websocket的雙向通信。在學習概念的時候覺得看懂了的內容,真正在實踐過程中還是會遇到各種問題,網上也有一些關於php的websocket的實現,但是只有自己親手寫過之后才知道其中的感受。其中,google有一個開源的phpwebsocket類(https://code.google.com/p/phpwebsocket/),但是從其握手過程中可以明顯看出,這還是最初的websocket協議,請求頭中使用了兩個KEY,並非version 13(現行版本)。下面是本人實踐過程,同時封裝好了一個現行版本的php實現的實用的websocket類。

一、握手

1、客戶端發送請求

websocket協議提供給javascript的API就是特別簡潔易用。

View Code

 

先看效果,客戶端和服務器端握手的結果如下:

2、服務器端

封裝的類為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."\n");
18         
19     }

 

然后啟動監聽,同時要維護連接到服務器的用戶的一個數組(連接池),每連接一個用戶,就要push進一個,同時關閉連接后要刪除相應的用戶的連接。

 1     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         else
13             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: (.*)\r\n/"  , $req, $match))
 7             $h = $match[1];
 8         if(preg_match("/Origin: (.*)\r\n/", $req, $match))
 9             $o = $match[1];
10         if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $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("\nRequesting 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\r\n" .
12                     "Upgrade: websocket\r\n" .
13                     "Connection: Upgrade\r\n" .
14                     "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";  //必須以兩個回車結尾
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、客戶端

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM