设为首页 收藏本站
查看: 1084|回复: 0

[经验分享] Websocket协议之php实现

[复制链接]

尚未签到

发表于 2015-8-23 08:54:56 | 显示全部楼层 |阅读模式
  前面学习了HTML5中websocket的握手协议、打开和关闭连接等基础内容,最近用php实现了与浏览器websocket的双向通信。在学习概念的时候觉得看懂了的内容,真正在实践过程中还是会遇到各种问题,网上也有一些关于php的websocket的实现,但是只有自己亲手写过之后才知道其中的感受。其中,google有一个开源的phpwebsocket类(https://code.google.com/p/phpwebsocket/),但是从其握手过程中可以明显看出,这还是最初的websocket协议,请求头中使用了两个KEY,并非version 13(现行版本)。下面是本人实践过程,同时封装好了一个现行版本的php实现的实用的websocket类。

一、握手

1、客户端发送请求
  websocket协议提供给javascript的API就是特别简洁易用。


DSC0000.gif View Code  
  先看效果,客户端和服务器端握手的结果如下:
DSC0001.jpg
DSC0002.jpg

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.iyunv.com/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协议的数据帧规范进行的。
  效果如下
DSC0003.jpg
DSC0004.jpg
数据交互的过程非常的直接,其中“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刷新页面后会发现出错:
DSC0005.jpg
  后来发现是重复关闭socket了,这个是因为在unwrap函数中遇到了控制帧直接关闭的原因。因此需要注意浏览器已经连接后进行刷新的操作。最后提供整个封装好的类,https://github.com/OshynSong/web/blob/master/websocket.class.php
  

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-102831-1-1.html 上篇帖子: PHP内核探索之变量(3)- hash table 下篇帖子: C++ 与 php 的交互 之----- C++ 获取 网页文字内容,获取 php 的 echo 值。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表