一、websocket是什么
    它是html5提出的一种新的通讯协议,用于实现浏览器和服务端双向通讯(长连接),从而告别了以往在web端只能通过keep-alive和普通轮询之类实现即时通讯的历史。
    二、websocket的原理
    以往在浏览器与服务器之间的web长连接都是通过http请求进行模拟的伪长连接,每次发送数据都得按http协议带上一些额外的协议数据和处理操作,效率会相对低下。websocket是一种全新的双工协议,它和http协议一样基于TCP连接。为了兼容现有浏览器的握手规范,websocket借助http协议进行握手,握手成功之后维持状态实现持久化。具体握手过程如下:
    1.客户端向服务端发起http请求,告诉服务端要进行websocket连接。
    为了与普通的http请求进行区分,在进行http请求时在header中加入额外的身份识别数据,以供服务端进行鉴别。下面是一个请求的header示例。
    -收缩代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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的响应和对应的版本,具体示例如下:
    -收缩代码

    1
    2
    3
    4
    Connection:Upgrade
    Sec-WebSocket-Accept:bD98U/y7WLhZEg2mv6uLJI0Fm2M=
    Sec-WebSocket-Version:13
    Upgrade:websocket

    3.浏览器端收到服务端的响应之后进行验证,验证通过则成功握手,双方可以进行数据的收发。
    三、数据帧详解
    websocket连接上之后,发送的数据需要严格按照websocket协议中规定数据格式进行封装。数据结构大体结构如下:
    websocket协议详解及数据处理实例 - 图1
    解析如下
    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的示例,主要包含获取握手中的头部信息、服务端消息打包处理、客户端消息解包处理三个功能。具体看注释。
    -收缩代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    <?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));
    `<br /> //组装报头信息,必须以两个回车结尾`<br />` $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"``;
    `<br /> return` `$upgrade;<br /> }`<br />` `<br />` /<br /> * 提取http请求header中的Sec-WebSocket-Key`<br />` @param string $requestHeaders<br /> `` http协议报头信息<br /> * @return string`<br />` 返回 Sec-WebSocket-Key<br /> ``/<br /> private` `function` `getSecWebSocketKey($requestHeaders)`<br />` {<br /> if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $requestHeaders, $match))<br /> {`<br />` return`$match``[1];
    ``}
    `<br /> return` `"";<br /> }`<br />` `<br />` /
    <br /> * 按websocket协议打包发送给客户端的数据`<br />` @param string $message<br /> `` 要发送的文本内容<br /> * @param real $opcode`<br />` 数据包类型,默认为文本类型<br /> `` 0x0:表示附加数据包<br /> * 0x1:表示文本类型数据包`<br />` 0x2:表示二进制类型数据包<br /> `` 0x3-7:保留<br /> * 0x8:表示断开连接类型数据包`<br />` 0x9:表示ping类型数据包<br /> `` 0xA:表示pong类型数据包<br /> * 0xB-F:保留`<br />` @return string<br /> `` 返回打包后的数据<br /> */`<br />` public`function wrap(``$message``=``""``, ``$opcode = 0x1)
    ``{
    ``$fin``=0x80;
    ``//第一个字节8位为10000001即0x81,其中0x80为100000000,0x1为00000001
    ``$firstByte = ``$fin `
    $opcode;<br /> $dataLength` `=strlen($message);`<br />` `<br />` $payloadLengthExtended=“”;`<br />` $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``);
    ``}
    `<br /> //服务端向客户端不需要做掩码处理,也就是第二字节第一位为0,由于小于等于127转为8位,第一位就为0,所以不需要额外处理`<br />` $encodeData`= ``chr``(``$firstByte``).``chr``(``$payloadLength``).``$payloadLengthExtended``.``$message``;
    ` ``//$encodeData = pack(‘n’, ($firstByte << 8)
    $payloadLength) . $payloadLengthExtended . $message;<br /> <br /> return` `$encodeData;<br /> }`<br />` `<br />` /*<br /> `` 解包客户端发过来的数据<br /> * @param string $message`<br />` 消息<br /> `` @return boolean string<br /> * 解包后的消息,数据不合法则返回false`<br />` */<br /> public` `function` `unwrap($message=“”)`<br />` {<br /> //取第一字节低4位即为opcode`<br />` $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;
    `<br /> $maskKey` `= null;`<br />` $data`= null;
    ``$decodeData = null;
    `<br /> //数据不合法($isMask不为1则表示没有进行掩码处理,0x8表示连接断开)`<br />` if`(``$isMask `!= 1
    $opcode` `== 0x8)`<br />` {<br /> return` `false;`<br />` }<br /> <br /> //获取掩码密钥和原始数据`<br />` 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``;
    ``}
    `<br /> <br />}`