返回 导航

其他

hangge.com

JS - 使用ArrayBuffer加载二进制文件数据(附:float数组生成二进制bin文件)

作者:hangge | 2020-03-27 08:10
    随着 HTML5 功能的丰富,现在前端展示的数据变得越来越原来越庞大,甚至会有上万条数据的传输。如果还用传统的 jsonxml 这种形式,数据量稍大一些就难堪重任了。
    要提高大数据的传输性能,那么数据通信就必须是二进制的。我们可以在 XMLHttpRequest 请求中使用 ArrayBuffer 方式,和服务器进行二进制的传输。

一、ArrayBuffer 与类型化数组

1,ArrayBuffer 介绍

(1)ArrayBuffer 的应用特别广泛,无论是 WebSocketWebAudio 还是 Ajax 等等,前端方面只要是处理大数据或者想提高数据处理性能,那一定是少不了 ArrayBuffer

(2)ArrayBuffer 是一段连续的长度固定的字节序列,如:通过实例化 ArrayBuffer 对象在内存中创建一段二进制存储空间(或叫二进制缓冲区)。
注意:由于是连续的内存空间,故在其上进行的读写操作都会比普通 JS Array 快很多。
// 创建一段字节长度为8的内存空间
var buffer = new ArrayBuffer(8);
// 获取字节长度
console.log(buffer.byteLength); // 8

(3)XMLHttpRequest Level 2 简称 XHR2。相较于老版本的 XHR 只支持文本数据的传送,XHR2 还可以用来读取和上传任意类型的数据:
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
    buffer = xhr.response;
    console.log(buffer)
};
xhr.send();

2,类型化数组(Type Array)

(1)类型化数组是读写 ArrayBuffer 中数据的接口,JS 可以通过 8 种不同的接口创建类型化数组,分别为:
类型 大小(字节) 描述 Web IDL type C语言等效类型 Java等效类型
Int8Array 1 8位二进制带符号整数 -2^7~(2^7) - 1 byte int8_t byte
Uint8Array 1 8位无符号整数 0~(2^8) - 1 octet uint8_t
Int16Array 2 16位二进制带符号整数 -2^15~(2^15)-1 short int16_t short
Uint16Array 2 16位无符号整数 0~(2^16) - 1 unsigned short uint16_t
Int32Array 4 32位二进制带符号整数 -2^31~(2^31)-1 long int32_t int
Uint32Array 4 32位无符号整数 0~(2^32) - 1 unsigned int uint32_t
Float32Array 4 32位IEEE浮点数 unrestricted float float float
Float64Array 8 64位IEEE浮点数 unrestricted double double doubl

(2)如果 ArrayBuffer 里面是单一类型数据,可用对应的类型化数组直接进行解析:
var array = new Uint8Array(buffer);
for(var i = 0; i < array.length; ++i){
  console.log(array[i])
}

(3)如果 ArrayBuffer 里面是混编类型数据,需要用移位的方式解析其中对应类型的数据:
var array = new Uint8Array(buffer [, byteOffset [, length]])
for(var i = 0; i < array.length; ++i){
  console.log(array[i])
}

二、 后台生成二进制 bin 文件

    后台如果需要将数据以二进制的形式传输到前台,一种方式是当前台请求时后台将数据以二进制流的形式返回,另一种是后台事先将数据转成二进制文件,然后前台加载这个二进制文件。 下面分别使用 Java Node.js 演示如何将一个 float[](浮点数数组)转成一个二进制文件。  

1,使用 Java 生成

(1)下面代码将一个数组以二进制的形式写到一个 bin 文件中:
public class Test {
    public static void main(String[] args) {
        // 待写入文件的数据
        float[] fbuf = {161.2f, 51.6f, 167.5f, 59.0f, 159.5f, 49.2f, 157.0f, 63.0f, 155.8f, 53.6f};

        // 开始写入
        try (FileOutputStream fos = new FileOutputStream("/Volumes/BOOTCAMP/data.bin")) {
            for(float f : fbuf){
                byte[] bs = getBytes(f);
                fos.write(bs);
            }
            fos.flush();
            fos.close();//为了节省IO流的开销,需要关闭
            System.out.println("文件生成成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 将 float 转成 byte[]
    public static byte[] getBytes(float data) {
        int intBits = Float.floatToIntBits(data);
        return getBytes(intBits);
    }

    public static byte[] getBytes(int data) {
        byte[] bytes = new byte[4];
        bytes[0] = (byte) (data & 0xff);
        bytes[1] = (byte) ((data & 0xff00) >> 8);
        bytes[2] = (byte) ((data & 0xff0000) >> 16);
        bytes[3] = (byte) ((data & 0xff000000) >> 24);
        return bytes;
    }
}

(2)代码执行后可以看到 data.bin 文件生成成功。

2,使用 Node.js 生成

(1)创建一个 create.js 文件,内容如下:
const fs = require('fs')
 
const buf = new ArrayBuffer(40)   // 创建一个 40 bytes 的缓存区
const float32Arr = new Float32Array(buf)   // type=float32 (4 Bytes) array view

const arr = [161.2, 51.6, 167.5, 59.0, 159.5, 49.2, 157.0, 63.0, 155.8, 53.6];
arr.forEach((val, i) => {
  float32Arr[i] = val
})
 
fs.writeFileSync('data.bin', new Buffer(buf)); // data.bin
console.log("文件生成成功!");

(2)接着执行如下命令,执行完毕后同样会生成一个 data.bin 文件,内容同上面 Java 方式生成的是一样的:
node create.js

三、 前台读取二进制 bin 文件

1,基本用法

(1)下面代码使用 ArrayBuffer 方式加载 data.bin 文件,然后使用 Float32Array 读取里面的数据,最后遍历输出数组里的所有浮点数。
var dataURL = 'http://localhost:8080/uploadFile/data.bin';
var xhr = new XMLHttpRequest();
xhr.open('GET', dataURL, true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
    var rawData = new Float32Array(this.response);
    for (var i = 0; i < rawData.length; i++) {
        console.log(rawData[i]);
    }
}

xhr.send();  

(2)运行结果如下:

2,在 ECharts 上的应用

(1)当使用 ECharts 绘制坐标轴散点图或者地图散点图时,如果数据量十分的庞大,就可以使用二进制的方式来传输散点数据,从而减少数据传输时间以及数据解析转换时间,提高效率:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>ECharts</title>
    <!-- 引入 echarts.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.2.1/echarts.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'));

    // 图标配置
    var option = {
        xAxis: {
            scale: true
        },
        yAxis: {
            scale: true
        },
        tooltip: {
            formatter: '{c}'
        },
        series: [
            {
            type: 'scatter',
            data: [],
        }]
    };

    // 加载散点数据
    var dataURL = 'http://localhost:8080/uploadFile/data.bin';
    var xhr = new XMLHttpRequest();
    xhr.open('GET', dataURL, true);
    xhr.responseType = 'arraybuffer';

    xhr.onload = function (e) {
        // 将原属数据转成图表使用的数据:[a,b,c,d,e,f] => [[a,b],[c,d],[e,f]]
        var rawData = new Float32Array(this.response);
        var data = new Array();
        for (var i = 0; i < rawData.length; i += 2) {
            data.push([rawData[i], rawData[i+1]]);
        }
        // 使用刚指定的配置项和数据显示图表。
        option.series[0].data = data;
        myChart.setOption(option);
    }

    xhr.send();

</script>
</body>
</html>

(2)效果如下:

附:浮点数精度问题解决

1,问题描述

    从前面的运行结果可以发现,当 Java 端将 float 以二进制的形式写入文件,js 再从中读取会产生精度问题。比如第一个原始数据是 161.2,但 js 读取出来就变成了 161.1999969482422

2,解决办法

(1)如果使用传统的数据传输方式,为避免这个问题,后台通常是将 float 转成 String 后再传输到前台。
(2)如果以二进制形式传输的话,可以先将 float 乘以 10 7 次方(因为 float 精度为 7 位)转成 int 类型,前台 js int 类型接收数据,再除以 10000000 还原成原始数据。

3,样例代码

(1)后台将 float 类型的原始数据放大 10000000 倍写入二进制文件:
注意:为了确保精度计算完全准确,我们不能直接将 float 乘以 10000000,而是需要借着 BigDecimal 来计算。
public class Test {
    public static void main(String[] args) {
        // 待写入文件的数据
        float[] fbuf = {161.2f, 51.6f, 167.5f, 59.0f, 159.5f, 49.2f, 157.0f, 63.0f, 155.8f, 53.6f};

        // 开始写入
        try (FileOutputStream fos = new FileOutputStream("/Volumes/BOOTCAMP/data.bin")) {
            for(float f : fbuf){
                //将 float * 10的7次方 转成int型
                BigDecimal a = new BigDecimal(Float.toString(f));
                BigDecimal b = new BigDecimal(Integer.toString(10000000));
                int i = a.multiply(b).intValue();
                byte[] bs = getBytes(i);
                fos.write(bs);
            }
            fos.flush();
            fos.close();//为了节省IO流的开销,需要关闭
            System.out.println("文件生成成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 将 float 转成 byte[]
    public static byte[] getBytes(float data) {
        int intBits = Float.floatToIntBits(data);
        return getBytes(intBits);
    }

    public static byte[] getBytes(int data) {
        byte[] bytes = new byte[4];
        bytes[0] = (byte) (data & 0xff);
        bytes[1] = (byte) ((data & 0xff00) >> 8);
        bytes[2] = (byte) ((data & 0xff0000) >> 16);
        bytes[3] = (byte) ((data & 0xff000000) >> 24);
        return bytes;
    }
}

(2)前台 js 使用 Int32Array 读取数据,然后再除以 10 7 次方的到原始数据:
注意js 1e7 表示 10 7 次方,即 10000000
var dataURL = 'http://localhost:8080/uploadFile/data.bin';
var xhr = new XMLHttpRequest();
xhr.open('GET', dataURL, true);
xhr.responseType = 'arraybuffer';
 
xhr.onload = function(e) {
    var rawData = new Int32Array(this.response);
    for (var i = 0; i < rawData.length; i++) {
        console.log(rawData[i] / 1e7);
    }
} 
  
xhr.send();

(3)运行结果如下,可以看到这次就没有浮点数精度问题了:
评论

全部评论(0)

回到顶部