Home  >  Article  >  php教程  >  Websocket协议之php实现

Websocket协议之php实现

WBOY
WBOYOriginal
2016-06-06 19:40:551988browse

前面学习了HTML5中websocket的握手 协议 、打开和关闭连接等基础内容,最近用php 实现 了与浏览器websocket的双向通信。在学习概念的时候觉得看懂了的内容,真正在实践过程中还是会遇到各种问题,网上也有一些关于php的websocket的 实现 ,但是只有自己亲手

前面学习了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协议之php实现View Code

 

先看效果,客户端和服务器端握手的结果如下:

Websocket协议之php实现

Websocket协议之php实现

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) )
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 )
 9             $encodedata = chr(0x81) . chr($len) . $msg;
10         else if (126 )
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 = 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 )
35         {
36             $decodedata .= $oridata[$i] ^ $maskkey[$i % 4];
37         }        
38         return $decodedata; 
39     }

其中得到掩码和控制帧后需要进行验证,如果掩码不为1直接关闭,如果控制帧为8也直接关闭。后面的原始数据和掩码获取是通过websocket协议的数据帧规范进行的。

效果如下

Websocket协议之php实现
Websocket协议之php实现
数据交互的过程非常的直接,其中“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)
 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刷新页面后会发现出错:

Websocket协议之php实现

后来发现是重复关闭socket了,这个是因为在unwrap函数中遇到了控制帧直接关闭的原因。因此需要注意浏览器已经连接后进行刷新的操作。最后提供整个封装好的类,https://github.com/OshynSong/web/blob/master/websocket.class.php

 

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn