xiaolingzi's blog

每天都在成长...

欢迎您:亲

websocket协议详解及数据处理实例

xiaolingzi 发表于 2017-03-06 19:20:27

一、websocket是什么

它是html5提出的一种新的通讯协议,用于实现浏览器和服务端双向通讯(长连接),从而告别了以往在web端只能通过keep-alive和普通轮询之类实现即时通讯的历史。

二、websocket的原理

以往在浏览器与服务器之间的web长连接都是通过http请求进行模拟的伪长连接,每次发送数据都得按http协议带上一些额外的协议数据和处理操作,效率会相对低下。websocket是一种全新的双工协议,它和http协议一样基于TCP连接。为了兼容现有浏览器的握手规范,websocket借助http协议进行握手,握手成功之后维持状态实现持久化。具体握手过程如下:

1.客户端向服务端发起http请求,告诉服务端要进行websocket连接。

为了与普通的http请求进行区分,在进行http请求时在header中加入额外的身份识别数据,以供服务端进行鉴别。下面是一个请求的header示例。

Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:no-cache
Connection:Upgrade
Host:127.0.0.1:9555
Origin:file://
Pragma:no-cache
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:u+fb2A3oNozG1loxWw2c7Q==
Sec-WebSocket-Version:13
Upgrade:websocket
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

其中Upgrade:websocket是告诉服务端要进行的是websocket连接,Sec-WebSocket-Key是一个base64串,用于验证合法性,Sec-WebSocket-Version是websocket通讯协议的版本,目前最新为13.

2.服务端在收到请求后,进行连接判断,如果是websocket则做出对应的响应。

服务端在鉴别出是websocket请求之后,会提取请求header中的Sec-WebSocket-Key,然后再拼接上一个特殊的字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行sha1和base64加密并将其作为header的Sec-WebSocket-Accept参数的值在header中返回。服务端统一在header中加入身份识别告诉客户端这个是websocket的响应和对应的版本,具体示例如下:

Connection:Upgrade
Sec-WebSocket-Accept:bD98U/y7WLhZEg2mv6uLJI0Fm2M=
Sec-WebSocket-Version:13
Upgrade:websocket

3.浏览器端收到服务端的响应之后进行验证,验证通过则成功握手,双方可以进行数据的收发。

三、数据帧详解

websocket连接上之后,发送的数据需要严格按照websocket协议中规定数据格式进行封装。数据结构大体结构如下:


解析如下

1. 第一个区块,即第一个字节

(1)FIN用于描述消息是否结束,1结束,0则还有数据包

(2)RSA1-RSA3这三位为预留扩展,默认0

(3)opcode为消息类型,这4位转为16进制值表示的意思如下

* 0x0:表示附加数据包

* 0x1:表示文本类型数据包

* 0x2:表示二进制类型数据包

* 0x3-7:保留

* 0x8:表示断开连接类型数据包

* 0x9:表示ping类型数据包

* 0xA:表示pong类型数据包

* 0xB-F:保留

2. 第二个区块,即第二个字节

(1)第一位MASK用于描述是否进行掩码处理,1是0否。客户端向服务端发送数据时需要进行掩码处理,否则不合法;服务端向客户端发送数据不能进行掩码处理,否则也不合法。也就是客户端向服务端发送时该值为1,反之为0。

(2)第二字节剩下7位用来表示数据长度。由于7位二进制最大值转为十进制只能到127,所有只有小于该长度是它才用来表示实际长度,否则再后面进行扩展来存储长度。具体规定如下:

a. 如果数据长度小于等于125,那么该7位用来表示实际数据长度。

b. 如果数据长度为126到65535之间,该7位的值固定为126,也就是1111110。往后扩展两个字节(16位,第三个区块)用于存储实际数据长度。

c. 如果数据长度大于65535,该7位的值固定为127,也就是1111111。往后扩展8个字节(64位,第三个区块)用于存储实际数据长度。

3. 第三个区块

这个区块就是第2条里面说的扩展用于存储数据实际长度的区位,根据a,b,c三种情况可能没有,也可能两个字节或者4个字节长。

4. 第四个区块。

该区块用于存储掩码密钥(masking key),只有在第二字节中的mask为1,也就是消息进行了掩码处理时才有,否则没有。所以服务端向客户端发送消息就没有这块。

5. 第五个区块

实际的数据的存储区域。

四、数据处理示例

这里写了一个php的示例,主要包含获取握手中的头部信息、服务端消息打包处理、客户端消息解包处理三个功能。具体看注释。

<?php
class WebSocket
{
    /**
     * 服务端根据Sec-WebSocket-Key生成握手数据,将给数据发回客户端完成连接
     * @param string $requestHeaders
     * http协议报头信息
     * @return string
     * 返回握手http协议报头
     */
    public function getHandShakeHeaders($requestHeaders)
    {
        //提取http请求header中的Sec-WebSocket-Key
        $key=$this->getSecWebSocketKey($requestHeaders);
        //将key加上特殊串进行sha1和base64加密作为accept key
        $acceptKey = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
         
        //组装报头信息,必须以两个回车结尾
        $upgrade  = "HTTP/1.1 101 Switching Protocol\r\n" .
                "Upgrade: websocket\r\n" .
                "Sec-WebSocket-Version: 13\r\n".
                "Connection: Upgrade\r\n" .
                "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";
         
        return $upgrade;
    }
         
    /**
     * 提取http请求header中的Sec-WebSocket-Key
     * @param string $requestHeaders
     * http协议报头信息
     * @return string
     * 返回 Sec-WebSocket-Key
     */
    private function getSecWebSocketKey($requestHeaders)
    {
        if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $requestHeaders, $match))
        {
            return $match[1];
        }
             
        return "";
    }
     
    /**
     * 按websocket协议打包发送给客户端的数据
     * @param string $message
     * 要发送的文本内容
     * @param real $opcode
     * 数据包类型,默认为文本类型
     * 0x0:表示附加数据包
     * 0x1:表示文本类型数据包
     * 0x2:表示二进制类型数据包
     * 0x3-7:保留
     * 0x8:表示断开连接类型数据包
     * 0x9:表示ping类型数据包
     * 0xA:表示pong类型数据包
     * 0xB-F:保留
     * @return string
     * 返回打包后的数据
     */
    public function wrap($message="", $opcode = 0x1)
    {
        $fin=0x80;
        //第一个字节8位为10000001即0x81,其中0x80为100000000,0x1为00000001
        $firstByte = $fin | $opcode;
        $dataLength = strlen($message);
         
        $payloadLengthExtended="";
        $payloadLength = 0;
        if (0 <= $dataLength && $dataLength <= 125)
        {
            //如果数据长度为0-125,则payload长度即为数据长度,存在第二个字节
            $payloadLength=$dataLength;
        }
        else if ($dataLength>=126 && $dataLength <= 65535)
        {
            //如果数据的长度为126-65535(0xFFFF),第二个字节默认存储的长度为126(0x7E),再接两个字节16位来表示长度
            $payloadLength = 126;
            //通过pack函数转为2字节16位的二进制字符串
            $payloadLengthExtended = pack('n', $dataLength);
        }
        else
        {
            //如果数据的长度大于65535(0xFFFF),第二个字节默认存储的长度为127(0x7F),再接8个字节64位来表示长度
            $payloadLength = 127;
            //通过pack函数转为8字节64位的二进制字符串,4个空字节(x)32位和一个32位整形(N)
            $payloadLengthExtended=pack("xxxxN", $dataLength);
        }
             
        //服务端向客户端不需要做掩码处理,也就是第二字节第一位为0,由于小于等于127转为8位,第一位就为0,所以不需要额外处理
        $encodeData = chr($firstByte).chr($payloadLength).$payloadLengthExtended.$message;
        //$encodeData = pack('n', ($firstByte << 8) | $payloadLength) . $payloadLengthExtended . $message;
             
        return $encodeData;
    }
         
    /**
     * 解包客户端发过来的数据
     * @param string $message
     * 消息
     * @return  boolean|string
     * 解包后的消息,数据不合法则返回false
     */
    public function unwrap($message="")
    {
        //取第一字节低4位即为opcode
        $opcode = ord(substr($message, 0, 1)) & 0x0F;
        //取第二字节低7位则为payload长度(第一位为mask)
        $payloadLength = ord(substr($message, 1, 1)) & 0x7F;
        //取第二字节高一位及为mask值(0或1,是否进行掩码处理)
        $isMask = (ord(substr($message, 1, 1)) & 0x80) >> 7;
             
        $maskKey = null;
        $data = null;
        $decodeData = null;
         
        //数据不合法($isMask不为1则表示没有进行掩码处理,0x8表示连接断开)
        if ($isMask != 1 || $opcode == 0x8)
        {
            return false;
        }
         
        //获取掩码密钥和原始数据
        if ($payloadLength >= 0 && $payloadLength <= 125)
        {
            //如果payload长度为0-125,第二字节为payload长度,3-6的4个字节为mask key,剩余为数据
            $maskKey = substr($message, 2, 4);
            $data = substr($message, 6);
        }
        else if ($payloadLength == 126)
        {
            //如果payload长度为126,第二字节为payload长度,3-4的两个字节为数据长度,5-8的4个字节为mask key,剩余为数据
            $maskKey = substr($message, 4, 4);
            $data = substr($message, 8);
        }
        else if ($payloadLength == 127)
        {
            //如果payload长度为127,第二字节为payload长度,3-10的8个字节为数据长度,11-14的4个字节为mask key,剩余为数据
            $maskKey = substr($message, 10, 4);
            $data = substr($message, 14);
        }
        //进行掩码处理
        $length = strlen($data);
        for($i = 0; $i < $length; $i++)
        {
            $decodeData .= $data[$i] ^ $maskKey[$i % 4];
        }
        return $decodeData;
    }
         
     
}


      

转载请注明出处:http://www.xxling.com/article/3103.aspx

  • 分类: html5
  • 阅读: (2990)
  • 评论: (0)
拍砖 取消
请输入昵称
请输入邮箱
*
 选择评论类型
300字以内  请输入评论内容