WebSocket之JS发送二进制

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

JS如何存储和操作二进制

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

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

1.png

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

数据类型与字节数

视图类型 说明 字节
Uint8Array 8位无符号整数 1字节
Int8Array 8位有符号整数 1字节
Uint8ClampedArray 8位无符号整数(溢出处理不同) 1字节
Uint16Array 16位无符号整数 2字节
Int16Array 16位有符号整数 2字节
Uint32Array 32位无符号整数 4字节
Int32Array 32位有符号整数 4字节
Float32Array 32位IEEE浮点数 4字节
Float64Array 64位IEEE浮点数 8字节

一个完整的例子:

// 创建一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270