likes
comments
collection
share

CBOR及在Nodejs Web开发中的应用

作者站长头像
站长
· 阅读数 9

IT这个行业的技术术语和缩略语真的是多如牛毛,这不,笔者又发现了一个好东西: CBOR,值得研究探讨和大家分享一下。

什么是CBOR

CBOR(Concise Binary Object Representation,中文译作"简明二进制对象表示") , 其实这个表述是挺准确的,它就是一种数据标识的方法或者数据组织的格式。和我们熟悉的JSON或者XML应该是一类的概念。

其实这个定义还是过于技术化了,如果用一句话来说明,那笔者觉得“二进制版的JSON”是最容易让人理解的。

那为什么先有了XML,后来有了JSON,甚至Google还搞出来一个protoBuff,还需要CBOR呢?笔者觉得这是一个典型的技术发展的过程,要从历史的脉络中寻找和思考。

在远古的HTML的Web时代, HTML是数据形式的标准,它的主要特点和核心技术是“标记语言(Mark Language)”。它使用一系列约定的文本作为标记,来表达数据结构。从后来者的角度来看待,其实这个设计并不是一个很好的设计。当然,由于具有良好的可定义性和可扩展性,它在网页端取得了巨大的成功。显然HTML是为网页表示(与人进行交互)设计的,后来在Web技术上发展出来的数据传输和处理技术也依赖了这一路径,产生了XML。

但其实XML的应用场景已经产生了变化,从着重人机交互,转变成为看重程序和系统之间的交互,这时,处理的效率,资源的占用就变成了一个突出的问题。这样,从后来的发展来看,XML就变成了一个灾难。对于软件程序而言,它过于复杂,处理的效率低下并且容易出错。而随着JS应用程序在Web中的广泛使用,其原生数据格式JSON,就成了新的事实上的数据标准,逐渐替换了XML在数据交换和通讯场景中的角色。

JSON的设计是非常简洁,清晰和优雅的,但从数据和信息的处理效率而言,JSON并不尽善尽美。本质上而言它还是一种考虑到"人"使用方便的设计,比如默认都使用UTF8字符集,没有任何数据压缩和引用的机制等等。如果大量的数据只在系统之间传输和转换,数据处理的效率就具备了更高的优先级,数据本身是否可读和容易理解并不重要。针对这些使用场景,Google提出了Protobuf作为其RPC的数据标准。这里不会深入讨论ProtoBuf的特点和实现方式,仅就笔者简单的使用体验而言,就觉得其开发、部署、应用和维护的过程比较复杂,比如需要安装和配置额外的程序库,需要预定义数据结构等等。

针对这个情况,特别是随着物联网技术的发展,行业需要一种简单、灵活的结构化数据编码方案,CBOR因此而诞生。

那么,作为后来者,它的特点和优势又是什么呢?

CBOR的优势

让我们再回到其在官方网站(cbor.io)上对自己的表述:

RFC 8949 Concise Binary Object Representation

The Concise Binary Object Representation (CBOR) is a data format whose design goals include the possibility of extremely small code size, fairly small message size, and extensibility without the need for version negotiation.

RFC 8949 简明二进制对象表示

CBOR是一种数据格式,它设计的目标包括极小的实现代码,相对较小的消息和无版本协商的扩展性

任何一种技术的产生和存在,都是为了解决特定的问题,或者提供比较的优势,或者适应特定的场景。经过一段时间的实践和使用,笔者体会和其他竞争技术特别是和JSON以及Protobuf比较,CBOR可能的优势如下:

  • 轻量化,它直接使用二进制来编码和表示数据,经过精心的设计,可以做到冗余无效信息较少
  • 兼容性,它直接使用二进制数据作为底层结构,这是各种语言和平台都必须支持的
  • 编码紧凑,这是其设计目标之一,无论是文件存储还是网络传输,它的最终体积相比JSON和XML都有一定优势
  • 复杂和结构的序列化,特别是可以直接编码二进制数据(JSON在默认情况下不支持)
  • 编解码性能,可能是得益于其原生二进制结构
  • 可以和JSON无缝映射和转换,笔者理解好像汇编和高级语言的关系
  • 规范和格式简单,容易程序实现
  • 应用广泛,适应性强,从物联网到互联网到数据存储
  • 作为RFC标准,具备一定的规范和权威性
  • 编码后传输的数据无法直接使用文本方法进行内容搜索和分析,对于业务应用可以提高一些安全性

当然世界上没有只有好的一面的事物,现在看起来其主要问题是应用还不够广泛,技术接受程度较低(看看它的难兄难弟-protobuf)。还有一个原因笔者觉得主要是对于人类而言,不如JSON(完全文本化)那么直观,总是觉得需要一个转换的过程,但其实这是一个认知的问题,从计算机系统的角度来看,那才是数据的本质。

总体来说,CBOR通过提供紧凑的二进制格式实现了在编码效率、交换速度和数据表达能力之间的平衡。它可以作为JSON的高效二进制补充,为诸如物联网等对性能敏感的场景提供高效的数据序列化和交换格式。

其实,刚开始的时候,CBOR是为计算和处理资源有限的使用场景设计的,说的好听就是简明,但其实就是“简陋”。但后来人们发现,在解决了可靠性和可扩展性之后,它其实可以用于更广泛的应用场景包括:

  • 物联网和嵌入式系统的数据交换格式。
  • 实时通信协议的负载信息编码。
  • 对性能和体积敏感的网络API的数据交换格式。
  • 需要长期存档的关键数据的编码格式。

在Nodejs中的应用

CBOR是一种二进制数据编码格式,所以它其实是更加底层的技术。由于我们现有的计算机体现架构都是二进制架构,所以理论上在任何基于二进制的系统上,都可以编写实现程序来对CBOR数据进行处理。

本文主要讨论的内容是其基础应用方式,为了方便讨论,我们选择相关开发和应用环境是Nodejs和JS语言。理论上其他编程环境也有相关的支持,但这里不会展开。

在Nodejs环境中应用CBOR是非常简单的,其核心就是对对象和数据进行编码和解码操作。当然现在的阶段,nodejs和浏览器环境都没有直接支持CBOR。它可以通过npm的方式提供,也可以使用单一js文件源码方式使用。笔者倾向于使用js文件方式,因为这一可以在前端和后端使用相同的方式处理数据。

然后就可以使用类似如下的代码,来对数据进行操作了:

const 
fs = require("fs"),
cbor = require("../lib/cbor.min");

const odata = { 
    id:1, 
    name : "中国China" 
};

const bfile = fs.readFileSync( __dirname + "/a008.png");
const cborEncode = ()=>{
    let ndata = {...odata,bfile };

    let cdata = cbor.encode(ndata);
    console.log("cbor:", cdata.byteLength);
    console.log("data:", cdata);

    let ddata = cbor.decode(cdata);

    console.log("orginal data:", ddata);
}; cborEncode();

const strEncode = ()=>{
    let ndata = {...odata,
        bfile: bfile.toString("base64")
    }
    
    let cdata = JSON.stringify(ndata);

    console.log("string:", Buffer.from(cdata).byteLength);
}; strEncode();

// 执行
node cbimage.js
cbor: 39217
string: 52288

这段代码的要点如下:

引用后得到一个cbor对象用于操作数据

  • 常规情况下可以直接对数据对象进行编码和解码操作

编码操作可以将一个JS对象编码成为CBOR数据;解码操作是编码操作的反操作,将CBOR数据解码成JS对象。 注意这里“JS对象”和“JSON数据”不完全是相同的东西

  • 编解码支持JSON对象中的二进制属性

代码中我们可以看到,图片文件加载后其实是一个Buffer,可以直接设置为JSON对象的属性,并进行CBOR编码。如果不使用CBOR,我们需要将图片数据编码成base64字符串,然后以字符串属性的方式封装到JSON对象,然后进行序列化。

  • 这里有一个和JSON处理的简单对比,相同的数据,最后的大小为39217和52288(字节)

  • 实际上,纯数据不包括图片的大小分别为22和29, 就是说通常情况下cbor的数据规模比json小15%~20%

和Web框架的集成(fastify)

前面看到,CBOR处理JSON对象和数据是非常简单的。但如果要把它真正的集成到Web应用中,可能会有一些问题。比如,一些Web框架可以直接或者方便的处理JSON或者X-WWW-FORM类型的数据,甚至RAW POST BODY字符串,但这是binary类型的数据,需要进行特别的处理。

我们以笔者常用的fastify框架为例,来说明这个问题。具体分为客户端请求、服务请求和响应处理三个主要的部分。

  • 客户端请求

在客户端,可以使用fetch提交cbor类型的数据。这里body的内容就是cbor编码结果;需要设置请求方法是POST;在header中设置内容类型是application/cbor;并且计算和设置内容体的大小。具体代码示例如下:

    // encrypt and sign
    let body = cbor.encode(odata);

    // console.log(edata);
    fetch(url, { 
        method: 'POST', 
        mode: "cors",
        headers: {
            'Accept': 'application/cbor',
            "Content-Type": "application/cbor",
            "Content_Length": body.byteLength,
            "Access-Control-Allow-Origin" : "*",
            "X-Client-Time": 0 | Date.now() / 1000 // add for server check otp 
        },
        body
    })
    .then(res=> {
      if (res.headers.get("Content-Type").includes("json")) {
        return res.json();   
      } else {
        let buf = res.arrayBuffer();   
        return cbor.decode(buf);
      }
    })
    .then(data=> {
        // console.log("response:", JSON.stringify(data)) 
        rv(data);
    })
    .catch(err => {
        console.log("Error:", err);
        rv({ R: 500, C: err.message });
    });

  • 服务端请求处理

在服务端,处理请求,需要为这种类型的数据(application/cbor),添加一个内容解析程序。在程序中需要定义如何处理流数据块,这里的数据块的类型是Buffer。数据传输完成后,将数据数组组装起来,封装到body.cborData 对象中,传送到下一个处理过程。在那个过程中就可以使用cbor进行解密处理了。一般在用cbor解码前,主要先转换为ArrayBuffer。


    app.addContentTypeParser('application/cbor', function (request, payload, done) {
        const data = [];

        // return cbordata buffer warp into cborData 
        payload
        .on('data', chunk => data.push(chunk))
        .on('end', () => {  done(null, { _cborData : Buffer.concat(data) }) });
    });
    
    
    // in other function convert to arraybuffer
    data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);

    // decode data
    let rdata = cbor.decode(data);
    
  • 服务端数据响应和发送

服务端的响应如果要使用cbor数据的话,和一般的响应处理无异。但也需要在header中设置内容类型和数据长度;然后调用send方法,只需要注意使用Buffer类型而已。

        // send back
        let rdata = cbor.encode({...qdata});

        rep
        .header('Access-Control-Allow-Origin','*')
        .header("Content-Type", "application/cbor")
        .header('Content-Length', Buffer.byteLength(rdata))
        .code(200)
        .send(Buffer.from(rdata));

  • 客户端处理响应

在客户端如果收到的数据是ArrayBuffer的话,也是需要进行特别的处理的。这段代码前面已经有了。简单而言就是调用toArrayBuffer()方法;然后就可以使用cbor进行解码了。

注意事项

在使用nodejs中,使用cbor可能需要理解和注意以下问题:

  • CBOR的使用不需要定义数据结构,可以直接处理JS数据和对象
  • CBOR处理的JS对象,可以包括其二进制数据内容的属性,这带来了极大的方便
  • CBOR可以编码任意类型的数据,但通常我们使用JS对象,输出为ArrayBuffer(Uint8Array),而且在前后端是一致的

如果需要使用Nodej Buffer做后续处理,可以使用如下方式转换:

let buf = Buffer.from(data);

  • CBOR解码的输入必须为ArrayBuffer,输出为数据原型或者JS对象

如果输入不是ArrayBuffer,如Nodejs Buffer,可以使用如下方式进行转换:

let abuf = new Uint8Array(Buffer.from(data))

一些附加的技术背景和信息

  • 缘起

笔者最早看到并接触这个技术,是在研究Fido Webauth认证实现的代码中看到的。当时就非常奇怪为什么要使用这么一种编码方式,后来进一步了解了一下,觉得它还是有一定的特点和优势,其实也有很广泛的应用场合。也将其用到了一些实际的业务当中。其实还有一个类似的技术COAP,理念类似,但还没有机会应用和尝试。

  • 编码压缩

这里可以稍微深入一点CBOR的编码技术方面的特点,特别是笔者感兴趣的部分。以下部分信息来自claude。

CBOR的编码规则相对简单,主要有以下几点:

  • 使用变长编码来表示不同的数据类型。
  • 使用最少的字节数表示小整数的值。
  • 使用引用和重复指针来避免重复数据。
  • 对字符串采用长度-值表示。
  • 对map使用延迟解析。

也就是说,CBOR使用了一些技术处理,来使编码更加紧凑。但可能是考虑到处理的效率、实现的难度和兼容方面的问题,它没有使用如霍夫曼编码或类似的数据压缩技术来缩减其数据编码的结果大小。但通过上面编码规则的使用,也能相对得到一个相对比较理想的结果,算是一个工程上的平衡吧。

  • 编码性能

原来笔者可能包括读者,可能会认为cbor的编解码性能应该是相当好的吧。但一个简单的测试却让笔者产生了一些疑问,代码如下:

const strEncode2 = (imode = 1)=>{
    let edata,ddata;
    
    console.time("T1");
    for(let i=0;i<1000000; i++) {
        odata.id = i;
        
        if (imode == 1) {
            edata = cbor.encode(odata);
            ddata = cbor.decode(edata);
            
        } else {
            edata = JSON.stringify(odata);
            ddata = JSON.parse(edata);
        }
    };
    console.timeEnd("T1");
}; strEncode2(2);

// 结果
json: 1.259s
cbor: 3.373s

代码很简单,就是分别用JSON方法和CBOR方法对一百万个js简单对象进行编解码,看看它们的性能如何。各位有兴趣也可以试一试。可以看到JSON其实是领先的,这和理论和猜想不太符合。也有可能是笔者的测试方法或者测试环境不太合理,我们先认可这个结果。虽然性能有差距,但还在一个数量级内,而且单一操作都是在"非常快"的范畴内,所以也不同过于纠结。也可以看出来nodejs(其实是V8)对JSON处理的优化已经达到了相当高的水平。