JS - 使用ArrayBuffer加载二进制文件数据(附:float数组生成二进制bin文件)
随着 HTML5 功能的丰富,现在前端展示的数据变得越来越原来越庞大,甚至会有上万条数据的传输。如果还用传统的 json、xml 这种形式,数据量稍大一些就难堪重任了。
要提高大数据的传输性能,那么数据通信就必须是二进制的。我们可以在 XMLHttpRequest 请求中使用 ArrayBuffer 方式,和服务器进行二进制的传输。
一、ArrayBuffer 与类型化数组
1,ArrayBuffer 介绍
(1)ArrayBuffer 的应用特别广泛,无论是 WebSocket、WebAudio 还是 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 还可以用来读取和上传任意类型的数据:
- 通过 xhr.responseType 可以是设置响应的数据格式,可选参数有:“text”、“arraybuffer”、“blob”或“document”。对应的返回数据为 DOMString、ArrayBuffer、Blob、Document;默认参数为"text"。
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 文件
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 文件,内容如下:
(2)接着执行如下命令,执行完毕后同样会生成一个 data.bin 文件,内容同上面 Java 方式生成的是一样的:
(2)运行结果如下:
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("文件生成成功!");
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)运行结果如下,可以看到这次就没有浮点数精度问题了: