>php教程 >php手册 >Websocket协议之php实现

Websocket协议之php实现

WBOY
WBOY원래의
2016-06-06 19:40:552193검색

前面学习了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并监听

<span> 1</span>     <span>function</span><span> createSocket()
</span><span> 2</span> <span>    {
</span><span> 3</span>         $<span>this</span>->master=<span>socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
</span><span> 4</span>             or die("socket_create() failed:"<span>.socket_strerror(socket_last_error()));
</span><span> 5</span>             
<span> 6</span>         socket_set_option($<span>this</span>->master, SOL_SOCKET, SO_REUSEADDR, 1<span>)
</span><span> 7</span>             or die("socket_option() failed"<span>.socket_strerror(socket_last_error()));
</span><span> 8</span>             
<span> 9</span>         socket_bind($<span>this</span>->master, $<span>this</span>->address, $<span>this</span>-><span>port)
</span><span>10</span>             or die("socket_bind() failed"<span>.socket_strerror(socket_last_error()));
</span><span>11</span>             
<span>12</span>         socket_listen($<span>this</span>->master,20<span>)
</span><span>13</span>             or die("socket_listen() failed"<span>.socket_strerror(socket_last_error()));
</span><span>14</span>         
<span>15</span>         $<span>this</span>->say("Server Started : ".date('Y-m-d H:i:s'<span>));
</span><span>16</span>         $<span>this</span>->say("Master socket  : ".$<span>this</span>-><span>master);
</span><span>17</span>         $<span>this</span>->say("Listening on   : ".$<span>this</span>->address." port ".$<span>this</span>->port."\n"<span>);
</span><span>18</span>         
<span>19</span>     }

 

然后启动监听,同时要维护连接到服务器的用户的一个数组(连接池),每连接一个用户,就要push进一个,同时关闭连接后要删除相应的用户的连接。

<span> 1</span>     public <span>function</span><span> __construct($a, $p)
</span><span> 2</span> <span>    {
</span><span> 3</span>         <span>if</span> ($a == 'localhost'<span>)
</span><span> 4</span>             $<span>this</span>->address =<span> $a;
</span><span> 5</span>         <span>else</span> <span>if</span> (preg_match('/^[\d\.]*$/is'<span>, $a))
</span><span> 6</span>             $<span>this</span>->address =<span> long2ip(ip2long($a));
</span><span> 7</span>         <span>else</span>
<span> 8</span>             $<span>this</span>->address =<span> $p;
</span><span> 9</span>         
<span>10</span>         <span>if</span> (is_numeric($p) && intval($p) > 1024 && intval($p) )
<span>11</span>             $<span>this</span>->port =<span> $p;
</span><span>12</span>         <span>else</span>
<span>13</span>             die ("Not valid port:"<span> . $p);
</span><span>14</span>         
<span>15</span>         $<span>this</span>-><span>createSocket();
</span><span>16</span>         array_push($<span>this</span>->sockets, $<span>this</span>-><span>master);
</span><span>17</span>     }

(2)建立连接

维护用户的连接池

<span>1</span>     public <span>function</span><span> connect($clientSocket)
</span><span>2</span> <span>    {
</span><span>3</span>         $user = <span>new</span><span> User();
</span><span>4</span>         $user->id =<span> uniqid();
</span><span>5</span>         $user->socket =<span> $clientSocket;
</span><span>6</span>         array_push($<span>this</span>-><span>users,$user);
</span><span>7</span>         array_push($<span>this</span>-><span>sockets,$clientSocket);
</span><span>8</span>         $<span>this</span>->log($user->socket . " CONNECTED!" . date("Y-m-d H-i-s"<span>));
</span><span>9</span>     }

(3)回复响应头

首先要获取请求头,从中取出Sec-Websocket-Key,同时还应该取出Host、请求方式、Origin等,可以进行安全检查,防止未知的连接。

<span> 1</span>     public <span>function</span><span> getHeaders($req)
</span><span> 2</span> <span>    {
</span><span> 3</span>         $r = $h = $o = <span>null</span><span>;
</span><span> 4</span>         <span>if</span>(preg_match("/GET (.*) HTTP/"<span>   , $req, $match))
</span><span> 5</span>             $r = $match[1<span>];
</span><span> 6</span>         <span>if</span>(preg_match("/Host: (.*)\r\n/"<span>  , $req, $match))
</span><span> 7</span>             $h = $match[1<span>];
</span><span> 8</span>         <span>if</span>(preg_match("/Origin: (.*)\r\n/"<span>, $req, $match))
</span><span> 9</span>             $o = $match[1<span>];
</span><span>10</span>         <span>if</span>(preg_match("/Sec-WebSocket-Key: (.*)\r\n/"<span>, $req, $match))
</span><span>11</span>             $key = $match[1<span>];
</span><span>12</span>             
<span>13</span>         <span>return</span><span> array($r, $h, $o, $key);
</span><span>14</span>     }

之后是得到key然后进行websocket协议规定的加密算法进行计算,返回响应头,这样浏览器验证正确后就握手成功了。这里涉及的详细解析信息过程参见另一篇博文http://blog.csdn.net/u010487568/article/details/20569027

<span> 1</span>     protected <span>function</span> wrap($msg="", $opcode = 0x1<span>)
</span><span> 2</span> <span>    {
</span><span> 3</span>         <span>//</span><span>默认控制帧为0x1(文本数据)</span>
<span> 4</span>         $firstByte = 0x80 |<span> $opcode;
</span><span> 5</span>         $encodedata = <span>null</span><span>;
</span><span> 6</span>         $len =<span> strlen($msg);
</span><span> 7</span>         
<span> 8</span>         <span>if</span> (0 )
<span> 9</span>             $encodedata = chr(0x81<span>) . chr($len) . $msg;
</span><span>10</span>         <span>else</span> <span>if</span> (126 )
<span>11</span> <span>        {
</span><span>12</span>             $low = $len & 0x00FF<span>;
</span><span>13</span>             $high = ($len & 0xFF00) >> 8<span>;
</span><span>14</span>             $encodedata = chr($firstByte) . chr(0x7E<span>) . chr($high) . chr($low) . $msg;
</span><span>15</span> <span>        }
</span><span>16</span>         
<span>17</span>         <span>return</span><span> $encodedata;            
</span><span>18</span>     }

其中我只实现了发送数据长度在2的16次方以下个字符的情况,至于长度为8个字节的超大数据暂未考虑。

<span> 1</span>      private <span>function</span><span> doHandShake($user, $buffer)
</span><span> 2</span> <span>     {
</span><span> 3</span>         $<span>this</span>->log("\nRequesting handshake..."<span>);
</span><span> 4</span>         $<span>this</span>-><span>log($buffer);
</span><span> 5</span>         list($resource, $host, $origin, $key) = $<span>this</span>-><span>getHeaders($buffer);
</span><span> 6</span>         
<span> 7</span>         <span>//</span><span>websocket version 13</span>
<span> 8</span>         $acceptKey = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', <span>true</span><span>));
</span><span> 9</span>         
<span>10</span>         $<span>this</span>->log("Handshaking..."<span>);
</span><span>11</span>         $upgrade  = "HTTP/1.1 101 Switching Protocol\r\n"<span> .
</span><span>12</span>                     "Upgrade: websocket\r\n"<span> .
</span><span>13</span>                     "Connection: Upgrade\r\n"<span> .
</span><span>14</span>                     "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";  <span>//</span><span>必须以两个回车结尾</span>
<span>15</span>         $<span>this</span>-><span>log($upgrade);
</span><span>16</span>         $sent = socket_write($user-><span>socket, $upgrade, strlen($upgrade));
</span><span>17</span>         $user->handshake=<span>true</span><span>;
</span><span>18</span>         $<span>this</span>->log("Done handshaking..."<span>);
</span><span>19</span>         <span>return</span> <span>true</span><span>;
</span><span>20</span>     }

二、数据传输

1、客户端

客户端websocket的API非常容易,直接使用websocket对象的send方法即可。

<span>1</span> ws.send(message);

2、服务器端

客户端发送的数据是经过浏览器支持的websocket进行了mask处理的,而根据规定服务器端返回的数据不能进行掩码处理,但是需要按照协议的数据帧规定进行封装后发送。因此服务器需要接收数据必须将接收到的字节流进行解码。

<span> 1</span>     protected <span>function</span> unwrap($clientSocket, $msg=""<span>)
</span><span> 2</span> <span>    { 
</span><span> 3</span>         $opcode = ord(substr($msg, 0, 1)) & 0x0F<span>;
</span><span> 4</span>         $payloadlen = ord(substr($msg, 1, 1)) & 0x7F<span>;
</span><span> 5</span>         $ismask = (ord(substr($msg, 1, 1)) & 0x80) >> 7<span>;
</span><span> 6</span>         $maskkey = <span>null</span><span>;
</span><span> 7</span>         $oridata = <span>null</span><span>;
</span><span> 8</span>         $decodedata = <span>null</span><span>;
</span><span> 9</span>         
<span>10</span>         <span>//</span><span>关闭连接</span>
<span>11</span>         <span>if</span> ($ismask != 1 || $opcode == 0x8<span>)
</span><span>12</span> <span>        {
</span><span>13</span>             $<span>this</span>-><span>disconnect($clientSocket);
</span><span>14</span>             <span>return</span> <span>null</span><span>;
</span><span>15</span> <span>        }
</span><span>16</span>         
<span>17</span>         <span>//</span><span>获取掩码密钥和原始数据</span>
<span>18</span>         <span>if</span> ($payloadlen = 0<span>)
</span><span>19</span> <span>        {
</span><span>20</span>             $maskkey = substr($msg, 2, 4<span>);
</span><span>21</span>             $oridata = substr($msg, 6<span>);
</span><span>22</span> <span>        }
</span><span>23</span>         <span>else</span> <span>if</span> ($payloadlen == 126<span>)
</span><span>24</span> <span>        {
</span><span>25</span>             $maskkey = substr($msg, 4, 4<span>);
</span><span>26</span>             $oridata = substr($msg, 8<span>);
</span><span>27</span> <span>        }
</span><span>28</span>         <span>else</span> <span>if</span> ($payloadlen == 127<span>)
</span><span>29</span> <span>        {
</span><span>30</span>             $maskkey = substr($msg, 10, 4<span>);
</span><span>31</span>             $oridata = substr($msg, 14<span>);
</span><span>32</span> <span>        }
</span><span>33</span>         $len =<span> strlen($oridata);
</span><span>34</span>         <span>for</span>($i = 0; $i )
<span>35</span> <span>        {
</span><span>36</span>             $decodedata .= $oridata[$i] ^ $maskkey[$i % 4<span>];
</span><span>37</span> <span>        }        
</span><span>38</span>         <span>return</span><span> $decodedata; 
</span><span>39</span>     }

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

效果如下

Websocket协议之php实现
Websocket协议之php实现
数据交互的过程非常的直接,其中“u”是服务器发送给客户端的,然后客户端发送一段随机字符串给服务器。

三、连接关闭

1、客户端

<span>1</span> ws.close();

2、服务器端

需要将维护的用户连接池移除相应的连接用户。

<span> 1</span>     public <span>function</span><span> disconnect($clientSocket)
</span><span> 2</span> <span>    {
</span><span> 3</span>         $found = <span>null</span><span>;
</span><span> 4</span>         $n = count($<span>this</span>-><span>users);
</span><span> 5</span>         <span>for</span>($i = 0; $i)
<span> 6</span> <span>        {
</span><span> 7</span>             <span>if</span>($<span>this</span>->users[$i]->socket ==<span> $clientSocket)
</span><span> 8</span> <span>            { 
</span><span> 9</span>                 $found =<span> $i;
</span><span>10</span>                 <span>break</span><span>;
</span><span>11</span> <span>            }
</span><span>12</span> <span>        }
</span><span>13</span>         $index = array_search($clientSocket,$<span>this</span>-><span>sockets);
</span><span>14</span>         
<span>15</span>         <span>if</span>(!<span>is_null($found))
</span><span>16</span> <span>        { 
</span><span>17</span>             array_splice($<span>this</span>->users, $found, 1<span>);
</span><span>18</span>             array_splice($<span>this</span>->sockets, $index, 1<span>); 
</span><span>19</span>             
<span>20</span> <span>            socket_close($clientSocket);
</span><span>21</span>             $<span>this</span>->say($clientSocket." DISCONNECTED!"<span>);
</span><span>22</span> <span>        }
</span><span>23</span>     }

其中遇到的一个问题就是,如果将上述函数中的socket_close语句提出到if语句外面的时候,当浏览器连接到服务器后,F5刷新页面后会发现出错:

Websocket协议之php实现

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

 

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.