精彩文章免费看

WebSocket之JS发送二进制

大家都知道使用socket通信都是二进制,通信框架多是使用二进制通信,高效且快速,但在前端如何编辑发送二进制,二进制数据在日常的JavaScript中很少遇到,但是当你使用WebSocket与后端进行数据交互时,就有可能会用到二进制的数据格式。

JS如何存储和操作二进制

JavaScript中用ArrayBuffer来存储二进制数据。

JavaScript类型化数组将实现拆分为 缓冲和视图 两部分。一个缓冲(ArrayBuffer)描述的是内存中的一段二进制数据,缓冲没有格式可言,并且不提供机制访问其内容。为了访问在缓存对象中包含的内存,你需要使用视图。视图可以将二进制数据转换为实际有类型的数组。一个缓冲可以提供给多个视图进行读取,不同类型的视图读取的内存长度不同,读取出来的数据格式也不同。缓冲和视图的工作方式如下图所示:

var buffer = new ArrayBuffer(8);

上面代码生成了一段8字节的内存区域,每个字节的值默认都是0。1 字节(Byte) = 8 比特(bit),1比特就是一个二进制位(0 或 1)。上面代码生成的8个字节的内存区域,一共有 8*8=64 比特,每一个二进制位都是0。

为了读写这个buffer,我们需要为它指定视图。视图有两种,一种是TypedArray视图,它一共包括9种类型,还有一种是DataView视图,它可以自定义复合类型。 基础用法如下:

var dataView = new DataView(buffer);
dataView.getUint8(0) // 0
var int32View = new Int32Array(buffer);
int32View[0] = 1 // 修改底层内存
var uint8View = new Uint8Array(buffer);
uint8View[0] // 1

数据类型与字节数

// 创建一个16字节长度的缓冲
var buffer = new ArrayBuffer(16);
// 创建一个视图,此视图把缓冲内的数据格式化为一个32位(4字节)有符号整数数组
var int32View = new Int32Array(buffer);
// 我们可以像普通数组一样访问该数组中的元素
for (var i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
// 运行完之后 int32View 为[0,2,4,6]
// 创建另一个视图,此视图把缓冲内的数据格式化为一个16位(2字节)有符号整数数组
var int16View = new Int16Array(buffer);
for (var i = 0; i < int16View.length; i++) {
  console.log(int16View[i]);
// 打印出来的结果依次是0,0,2,0,4,0,6,0
2.png
相信图片已经很直观的表达了这段代码的意思。这里应该有人会疑问,为什么2、4、6这三个数字会排在0的前面,这是因为x86的系统都是使用的小端字节序来存储数据的,小端字节序就是在内存中,数据的高位保存在内存的高地址中,数据的低位保存在内存的低地址中。就拿上面这段代码举例,上图中内存大小排列的顺序是从左向右依次变大,int32View[1]对应的4个字节,它填入的值是 10 (2的2进制表示),把0补齐的话就是 00000000 00000000 00000000 00000010(中间的分隔方便观看),计算机会倒过来填充,最终会成为 00000010 00000000 00000000 00000000。与小端字节序对应的就是大端字节序,它就是我们平时读数字的顺序。

ArrayBuffer

用来表示通用的、固定长度的原始二进制数据缓冲区。
MDN的文档中,我们能够看到ArrayBuffer的介绍。它是在JavaScript中用来进行二进制数据存储的一种数据对象。

下面我们通过一个示例来简单介绍下ArrayBuffer相关操作。

const buffer = new ArrayBuffer(8);  //8个字节
buffer.byteLength; // 结果为8

上面的示例通过创建一个长度为8Byte的二进制数据缓冲区。缓冲区只是一个数据存储的空间,如何对这个存储空间进行读取,完全取决于使用者。例如:8个字节可以当成是2个Int类型的数据,也可以是一个Long类型的数据,或者4个Short型的数据。

DataView

DataView 视图是一个可以从 ArrayBuffer 对象中读写多种数值类型的底层接口,在读写时不用考虑平台字节序问题。

MDN中关于DataView的介绍。DataView提供了大量的API接口来进行数据的读和写操作。但是,首先我们得先看下说明中提到的字节序问题。

在现有的计算机体系中,有两种字节序:

  • 大端字节序:高位在前,低位在后。符合人类阅读习惯。
  • 小端字节序:低位在前,高位在后。符合计算机读取习惯。
  • 上面所说的顺序均是针对多字节对象而言,如Int类型,Long类型。以Int类型数据0x1234为例,如果是大端字节序,那么数据从人类对数值的通常写法上来看就是0x1234;如果是小端字节序,那么从人类对数值的通常写法上来看,应该写成0x3412。

    对于单字节对象如Byte类型数据而言,没有字节序一说。

    在不同的平台中,可能使用不同的字节序,这就是所谓的字节序问题。DataView所谓的在读写时不需要考虑平台字节序问题是指:同时使用DataView进行写入和读取的数据保持一致。

    JS数据转二进制

    对ArrayBuffer和DataView有了一个大概的了解,下面让我们来看下它是如何进行二进制数据操作的。

    let buffer = new ArrayBuffer(6); // 初始化6个Byte的二进制数据缓冲区
    let dataView = new DataView(buffer);
    dataView.setInt16(0, 3); // 从第0个Byte位置开始,放置一个数字为3的Short类型数据(占2 Byte)
    dataView.setInt32(2, 15); // 从第2个Byte位置开始,放置一个数字为15的Short类型数据(占4 Byte)
    

    通过上面的示例,我们一共初始化了6个Byte的存储空间,使用1个Short类型(占2 Byte)和一个Int类型(占4 Byte)的数据进行填充。

    DataView还提供了许多的API接口来进行其他数据类型的处理,如无符号型,浮点数等。他们的使用方法和上面介绍的API相同,我们在这里就不一一进行介绍了,希望了解更多API接口的读者可以查看MDN文档

    JS中Long类型转换为二进制数据

    通过DataView提供的API接口,我们知道了如何处理Short类型、Int类型、Float类型和Double类型。那么,如果是对于Long类型这种原生API中没有提供处理函数的数据类型,我们应该如何处理呢?

    首先,我们需要理解Long数据类型的结构,它是由一个高位的4个Byte和低位的4个Byte组成的数据类型。因为Long类型表示的范围比Number类型大,所以我们在JavaScript中是使用了两个Number类型(即Int类型)的对象来表示Long类型数据,相关的具体细节可以见我之前的博客Long.js源码分析与学习

    理解了JavaScript中如何存储Long类型,我们就知道如果对其进行存储。

    import Long from 'long';
    let long = Long.fromString('123');
    let buffer = new ArrayBuffer(8);
    let dataView = new DataView(buffer);
    dataView.setInt32(0, long.high); // 采用大端字节序放置
    dataView.setInt32(4, long.low);
    

    通过上面的示例,我们将一个Long类型的数据拆分成了两个Int类型的数据,按照大端字节序放入到了ArrayBuffer中。同理,如果是想按照小端字节序放置,只需要将数据进行部分处理后再放入即可,在此我就不过多介绍了。

    如何将二进制数据转换为JS中的数据类型

    当你知道了如何将数据转换为ArrayBuffer中存储的二进制数据后,就能够简单推测出如何进行反向操作——将数据从ArrayBuffer中读取出来,再转换成JavaScript中常用数据类型。

    import Long from 'long';
    let buffer = new ArrayBuffer(14); // 初始化14个Byte的二进制数据缓冲区
    let dataView = new DataView(buffer);
    let long = Long.fromString('123');
    // 数据写入过程
    dataView.setInt16(0, 3); // 从第0个Byte位置开始,放置一个数字为3的Short类型数据(占2 Byte)
    dataView.setInt32(2, 15); // 从第2个Byte位置开始,放置一个数字为15的Short类型数据(占4 Byte)
    dataView.setInt32(6, long.high); // 采用大端字节序放置
    dataView.setInt32(10, long.low);
    // 数据读取过程
    let shortNumber = dataView.getInt16(0);
    let intNumber = dataView.getInt32(2);
    let longNumber = Long.fromBits(dataView.getInt32(10), dataView.getInt32(6)); // 根据大端字节序读取,该构造函数入参依次为:低16位,高16位
    

    JS中WebSocket使用定制二进制通信

    一般情况下,服务端都会定制一套自己的通信协议,如下每个字节定义

  • 第1个字节必须以0xBF开头
  • 第2个字节表示请求类型,如1-request 2-response 3-command
  • 第3个字节开始,写入数据buffer的长度,共占4个字节
  • 第5个字节开始,写入整个buffer
  • 示例代码如下(以protobuf为例):

    protobuf.load("proto/MessageDataProto.proto", function (err, root) {
                // Obtain a message type
                var RequestUser = root.lookupType("com.example.nettydemo.protobuf.RequestUser");
                // Exemplary payload
                var payload = data;
                // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
                var errMsg = RequestUser.verify(payload);
                if (errMsg) {
                    return;
                // Create a new message
                var message = RequestUser.create(payload); // or use .fromObject if conversion is necessary
                // Encode a message to an Uint8Array (browser) or Buffer (node)
                var buffer = RequestUser.encode(message).finish();
                let num = 6; //定制协议前部固定长度
                let len = num+buffer.byteLength;//总字节长度
                let arrBuffer = new ArrayBuffer(len); // 初始化Byte的二进制数据缓冲区
                let dataView = new DataView(arrBuffer);
                //191===0xBF
                dataView.setInt8(0, 191); // 从第0个Byte位置开始,放置一个数字为3的Short类型数据(占1 Byte)
                dataView.setInt8(1, 1); //1-request 2-response 3-command
                //dataView.setInt32(2, 1001); // 从第2个Byte位置开始,放置一个数字为15的Short类型数据(占4 Byte)
                dataView.setInt32(2, buffer.byteLength); //占4个字节
                for (var i = 0; i < buffer.byteLength; i++) {
                    dataView.setInt8(num+i, buffer[i]);
                if (typeof success === "function") {
                    success(dataView)
                if (typeof complete === "function") {
                    complete()
    

    通过使用ArrayBuffer和DataView,我们能够快速的将数字数据从二进制转换为JavaScript常用数据类型如Int、Short等;同时,我们也可以将这些数据类型转换为二进制数据或服务端定制的二进制协议。

    http://www.ruanyifeng.com/blog/2016/11/byte-order.html
    https://cloud.tencent.com/developer/article/1341898

    最后编辑于:2020-06-29 10:56