Websocket協定之php實作
2014-03-11 09:34
OshynSong
閱讀(3620)
評論(6)
編輯
收藏
舉報
前面學習了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
-
分類 前端
, php
-
标簽 websocket
, php實作