php的中文json打包,实现websocket大数据量传输,解决Could not decode a text frame as UTF-8

前言

最近打算弄一个桌游的网页版实现,然后在实现的过程中了解到了websocket,它可以保证服务器和客户端的即时通信,是一个很不错的方案,但是在自己实现功能的时候遇到了挺多的问题,就在此留下自己的解决方案

本方案原参考链接

php实现websocket实时消息推送
博主本来是使用上面的参考链接的示例代码加以修改投入使用的,个人觉得代码写的清晰易懂,是一个不错的参考。

问题列表

后端发送的json中文数据被前端解析成\uxxxx

这个问题是因为php进行json_encode()的时候默认参数是会将中文进行Unicode转义的,这样的话一个中文被转义成Unicode会使传输过程中中文的数据长度增加一倍(UTF-8表示的中文占3个字节,而Unicode转义后需要占用6个字节),并且还需要在前端进行额外处理才能转回中文。

解决方法:给json_encode()增加第二个参数JSON_UNESCAPED_UNICODE,这样可以令json编码的时候不进行Unicode转换。

websocket前端提示Could not decode a text frame as UTF-8

这种情况是因为发送数据时数据包里面含有不完整的utf-8字符。
说到这个我们需要先了解一下websocket的协议:
php的中文json打包,实现websocket大数据量传输,解决Could not decode a text frame as UTF-8_第1张图片
我们在这里重点关注的是Payload len这个位域。Payload len 位于数据包第二个字节的低七位,因为只有七位所以其值只能是0-127。因此协议是这样规定的:
当数据包长度小于126bytes时,Payload len 填真实数据长度,然后后面就紧跟数据即可。
因此如果只用默认的Payload len区域的话,发送的数据帧大小只能是最大125bytes。为了发送更大的数据包,我所参考的博主的代码里是这么处理数据的:

  public function frame($s) {
    $a = str_split($s, 125);
    if (count($a) == 1) {
      return "\x81" . chr(strlen($a[0])) . $a[0];
    }
    $ns = "";
    foreach ($a as $o) {
      $ns .= "\x81" . chr(strlen($o)) . $o;
    }
    return $ns;
  }

这段代码将需要发送的字符串s按125bytes进行分割,然后每125字节进行一次封包,加上数据头和数据长度(此时的数据长度必小于等于125字节)然后再发送。这就导致了接收端实际上接收的并不是一次字符串s,而是每隔125字节就进行分割的多段数据,而一旦有一次分割拆散了一个完整的utf-8字符(3字节)到不同的数据包上,就会出现Could not decode a text frame as UTF-8的问题了。

解决方法

  1. (不推荐)不使用JSON_UNESCAPED_UNICODE参数,中文按Unicode进行转义,这样的话就不会出现被拆散的utf-8字符了,然后接收端还需要做合包算法将多个数据包拼成一个完整的json字符串,最后再将Unicode转义符转换为中文。这样子的话最终接收端接收到的数据是这样的,蓝色数字是数据包长度
    php的中文json打包,实现websocket大数据量传输,解决Could not decode a text frame as UTF-8_第2张图片
    中间省略若干行
    php的中文json打包,实现websocket大数据量传输,解决Could not decode a text frame as UTF-8_第3张图片
  2. 既然我们出现Could not decode a text frame as UTF-8是因为拆包的问题,那我们为什么不在发送的时候就不拆包呢?请看下面的问题

websocket大数据量不拆包传输

回到我们的协议,刚刚说到Payload len当数据包长度小于126的情况是这样的,那么大于等于126的数据包呢其实也不一定要拆包。
websocket协议规定:当数据包长度大于125且小于65535时,Payload len位域的值为126,然后紧接着下两个字符作为无符号16位数代表真实的数据包长度;当数据包长度大于65535且小于(2^32)-1时,Payload len位域的值为127,紧接着下四个字符作为无符号32位数代表真实数据包长度。
因此,想发送更长的数据,选择改变帧头格式而不是拆包发送,这样才是更好的方法
因此我们的代码可以进行这样修改:

  public function frame($s) {
    if(strlen($s)<126)
    {
      return "\x81" . chr(strlen($s)) . $s;
    }else if(strlen($s)<0xffff){
      return "\x81" . chr(126) . chr(strlen($s) >>8) . chr(strlen($s) & 0xff) .$s;
    }
  }

PS:如上代码只支持发送数据不超过65535,如果想发送比65535还大的数据包,需要再加一个else if 分支
当数据包长度>125且<65535时,第二个字节直接填入126,随后根据网络字节序的低位高地址,先填入数据包长度的高八位,再填入数据包长度的低八位,最后再跟上我们需要发送的数据。
最后,因为我们的数据包没有进行拆包,所以也不需要担心UTF-8字符被拆分的问题,为了传输效率和方便可以直接使用utf-8进行传输。
以下是接收端的效果图:php的中文json打包,实现websocket大数据量传输,解决Could not decode a text frame as UTF-8_第4张图片
发送的数据是一模一样,大家可以和上面使用Unicode分包发送的对比一下,高下立判。

本人也处于在对前后端的艰苦探索状态中,如有理解错误,欢迎评论区指正交流,这个问题折腾了我好半天,希望这个博文能让大家了解清楚websocket。

你可能感兴趣的:(web)