一、前言
上一篇「封包組合與剖析器設計@node.js (I) 封包組合」,我們示範了如何使用 concentrate 模組,依照我們「自己訂的格式」,將一個名為 person 的資料物件轉換為 buffer。在這篇文章,我們要示範如何使用 dissolve 模組來將這個 buffer 依照自訂義的格式規則來剖析回原本的資料物件。像這類「自己訂的格式」,也就是俗稱的 Domain-specific language (DSL),例如某公司為他們的產品,使用了他們自己所訂的封包格式來進行通訊(傳控制碼、狀態碼、資料等等),這就可稱為 DSL。但在我的觀念中,DSL 是比較廣義的,倒不一定就是指文章中所提的封包格式定義。即便我們是使用 json 資料物件來進行通訊、採用自己定義的物件欄位(或者說介面也可以),都可稱為 DSL。DSL 在不同的情境下,可能會被理解為不同意思,但是很歹勢啦,我實在舉不出甚麼好例子啦!呵呵~不是重點。在比較高的應用層,其實蠻少碰到二進位封包 DSL 的組合跟剖析。假使你只是需要應用層級的協定,不管是資料或介面,我是建議都可以用 json 來完成,端看你怎麼定義物件裡那一坨欄位的意義囉~ 假使覺得 json 的 overhead 有點大,也可以考慮使用 msgpack 這樣的格式來打包物件,你可以在 msgpack 的官網上看到它為不同語言所提供的函式庫。像我最喜歡的 node,就可以直接用 Collina 的 msgpack5 模組。
二、範例程式
我將連同上一次的範例,一起放在 git 上面。大家如果有需要可以直接到這裡下載,或是 clone 回去並 npm install 一下(會安裝 concetrate, dissolve, 與 dissolve-chunks 三個 modules):$ git clone https://github.com/simenkid/packet_demo.git $ cd packet_demo ~/packet_demo$ npm install
三、目標封包
這邊小小回顧一下我們要處理的封包格式,採用上一篇文章中 personBuf_01.js 的例子如下:var person = { sex: 1, // [無號整數, uint8 ]: 0 表示女生, 1 表示男生 name: 'simen', // [字串, utf8]: 長度不一定 age: 37, // [無號整數, uint8 ]: 年齡數字 height: 17200 // [無號整數, uint16]: 單位是 mm }
組合出來的 buffer 如下
<Buffer 01 05 73 69 6d 65 6e 25 30 43>
四、Dissolve 模組的 APIs 介紹
這裡我先簡單介紹一下 dissolve 模組的 APIs。4.1 Numeric Parsing Methods
連結到 dissolve 的說明頁面,我們拉到最下面看 Numeric Methods,它提供了一堆如 int8(key_name), uint8(key_name), uint16le(key_name), int32be(key_name) 的剖析方法,其中 key_name 是字串,指的是你要將剖析結果放到結果物件中的 'key' 的名稱是什麼。它的意思超直覺的,例如 int8(key_name) 是說「哦!我要抓取 buffer 接下來 1 個 byte(因為是int8),然後將它辨識為有號整數。將辨識完的結果,放到結果物件中的 key_name 欄位。」
又以 uint16le(key_name) 為例:
「哦!我要抓取接下來的連續 2 個 bytes(因為是uint16),而且它們是用小端排列(Littile endian) 的阿!所以請用小端的方式將這兩個 bytes 辨識為無號整數 (uint16le)。將辨識完的結果,放到結果物件中的 key_name 欄位。」
再以 int32be(key_name) 為例,現在你知道,這就是在告訴 dissolve 接下來抓取 buffer 接下來的 4 個 bytes 並視為大端方式辨識為有號整數。
4.2 Buffer/String Parsing Methods
這裡有兩支 APIs,意思也很好懂- buffer(key_name, length) - binary slice
- 接下來,要抓 length 這麼多個 bytes,將它視為獨立一段 buffer,掛到結果物件中的 key_name 鍵底下
- string(key_name, length) - utf8 string slice
- 接下來,(預設)用 utf8 的編碼方式,總共要抓出 length 這麼多個字元出來,成為一條字串,掛到結果物件中的 key_name 鍵底下
- 支援的編碼方式,如'utf8', 'ascii' 等請參考 node.js 官方文件
4.3 Tap Method
這支方法就重要了, very 之常用。因為封包格式的定義五花八門,我們經常不能夠直接以 .uint8(), .unt16() 這些方法一連串地持續剖析下去,必須在剖析過程中,加入一些處理邏輯來告訴 Dissolve:
「接下來沒有這麼簡單哦!你要先這樣、再那樣,然後就怎樣....才能辨識出我要的東西哦!」
那要如何告訴 Dissolve 呢?我們只要在剖析方法的串鏈過程中,在需要特殊處理的地方用 .tap() 插入我們的判斷邏輯即可。API 的介面如下,callback 就是我們要告訴 Dissolve 如何處理的函式,key_name 就是將這一段特殊邏輯剖析出來的結果掛到結果物件的 key_name 鍵底下
「接下來沒有這麼簡單哦!你要先這樣、再那樣,然後就怎樣....才能辨識出我要的東西哦!」
那要如何告訴 Dissolve 呢?我們只要在剖析方法的串鏈過程中,在需要特殊處理的地方用 .tap() 插入我們的判斷邏輯即可。API 的介面如下,callback 就是我們要告訴 Dissolve 如何處理的函式,key_name 就是將這一段特殊邏輯剖析出來的結果掛到結果物件的 key_name 鍵底下
- tap(key_name, callback)
- 小提示
- 當你放 key_name 的時候,剖析出來的小結果,就會掛在結果物件的 key_name 這個鍵底下。舉例來說,如果這段邏輯會剖析出一個物件 { foo: 3, bar: [ 5, 6, 7] },我們調用 tap('kerker', function () {...}) 所剖析後的結果物件會長這樣
{ kerker: { foo: 3, bar: [5, 6, 7] } }
- 當你不放 key_name 的時候,我們調用 tap(function () {...}) 所剖析後的結果物件會長這樣,所有 sub-keys 都會直接掛在結果物件之上
{ foo: 3, bar: [5, 6, 7] }
- 結果物件:this.vars
- Dissolve 在剖析過程中,會將剖析完的結果暫存在 this.vars 所指到的物件之下。因此,我們在 tap() 時,可以從 this.vars 身上去挖出前面已剖析好的東西,來協助完成接下來要處理的邏輯。
- 當剖析器全部寫完之後,最後一步就是要掛一個 tap(),調用 this.push(this.vars) 將 結果物件(根物件) 的 this.vars 內容給 push 出去,並將它清空為一個空物件。
- 注意,這裡的 push 方法是 Stream 的方法,不是 Array 的 push(),意思是將結果推出去,此時 Stream 物件就會引發 'readable' 事件,你可以寫個 listener 來監聽該事件並將結果拿出來,又或者將它 pipe 到一個 writtable 的 Stream 物件。
- 在 push(this.vars) 之前,我們可以先修整一下 this.vars 的內容,將一些中間產物先刪除掉再 push() 出去。
- 我們會在本文例子的剖析字串片段,看到這一點。
- this.vars 的 this 並不總是指 "根" 部的 Dissolve 實例,this 所指向的物件是依據 tap 深度的不同,而有不同的 context。關於這一點,我就不多加說明了!在真正實作中,你一定會遇到這個問題,而且這個問題有時候會給開發者帶來一些困擾。
- 我們馬上就碰到的問題:剖析字串!
- 要解出字串,我要先找出它到底有多長 (len 欄位)
- 所以呢!我要在解析出 len 之後,插入一個 tap,在當前已剖析好的結果物件中,撈出 len 的值,這樣我才能告訴 Dissolve 接下來要 .string() 繼續頗析出多長的字串出來
4.4 Loop Method
當我們遇到需要「重複性」頗析的時候,例如「格式」都相同的陣列元素,就可以使用 loop 來協助我們。Loop 會丟一個 end 函式做為 callback 的參數,當你完成重複性剖析的最後一個項目時,就可以在 callback 中調用 end(),這樣 Dissolve 就會停止重複性的剖析,並將剖析結果掛到你所指定的 key_name 之下,這個規則跟 tap() 一樣。它的 API 介面如下:
這時候,loop() 就派上用場啦,我只要在 parser 撰寫的一開始 用 loop() 包裹整套剖析邏輯,在一次完整的剖析結束後,Dissolve 會將依連串的 jobs 再重新填回 jobs list 中哦!
這很重要!舉個例子,假如你現在要 parse 的 buffer 是從 UART 接回來的,那當然希望從 UART 一直收、一直剖析吧!要是不用 loop(),你會發現從 UART 收回來的 buffer 都只被剖析一次,就整個停掉啦!
直接上程式碼 (personParser_01.js),你可以看到我們在 uint8('len') 找出字串長度後,插入一個 tap(),在裡面,我們可以從 this.vars.len 找出字串長度到底是多長,然後在調用 .string() 時,告訴它要剖析出多長的字串。最後一個 tap 的任務就是將最後的結果給 push 出去。
我們用 parse.on() 監聽 'readable' 事件,當此事件被引發時,代表 parser 的剖析結果已經可以讀取了,調用 parser.read() 去讀取即可。
最後,parser.write() 就是實際將 buffer 推入 paser 的時刻,parser 開始正式勤奮工作!執行看看,光榮的時刻來臨了,為我們偉大的結果歡呼吧!(有沒有這麼誇張~~~)
這支程式最後 parse 出來的結果如下,但事情可還沒完~
再執行一次,你可以看到這個作為 meta 功能的 len 欄位就不見啦!Bravo!!
- loop(key_name, callback)
這時候,loop() 就派上用場啦,我只要在 parser 撰寫的一開始 用 loop() 包裹整套剖析邏輯,在一次完整的剖析結束後,Dissolve 會將依連串的 jobs 再重新填回 jobs list 中哦!
這很重要!舉個例子,假如你現在要 parse 的 buffer 是從 UART 接回來的,那當然希望從 UART 一直收、一直剖析吧!要是不用 loop(),你會發現從 UART 收回來的 buffer 都只被剖析一次,就整個停掉啦!
五、開始剖析 personBuf
直接上程式碼 (personParser_01.js),你可以看到我們在 uint8('len') 找出字串長度後,插入一個 tap(),在裡面,我們可以從 this.vars.len 找出字串長度到底是多長,然後在調用 .string() 時,告訴它要剖析出多長的字串。最後一個 tap 的任務就是將最後的結果給 push 出去。
我們用 parse.on() 監聽 'readable' 事件,當此事件被引發時,代表 parser 的剖析結果已經可以讀取了,調用 parser.read() 去讀取即可。
最後,parser.write() 就是實際將 buffer 推入 paser 的時刻,parser 開始正式勤奮工作!執行看看,光榮的時刻來臨了,為我們偉大的結果歡呼吧!(有沒有這麼誇張~~~)
var Dissolve = require('dissolve'); // <Buffer 01 05 73 69 6d 65 6e 25 30 43> var personBuf = new Buffer([ 0x01, 0x05, 0x73, 0x69, 0x6d, 0x65, 0x6e, 0x25, 0x30, 0x43 ]); var parser = Dissolve().uint8('sex').uint8('len') .tap(function () { this.string('name', this.vars.len); }).uint8('age').uint16le('height') .tap(function () { this.push(this.vars); this.vars = {}; }); parser.on('readable', function() { var e; while (e = parser.read()) { console.log(e); } }); parser.write(personBuf);
{ sex: 1, len: 5, name: 'simen', age: 37, height: 17200 }
結果多出了一個欄位 'len',這個前導欄位的中繼功能已經完成了它的職責,所以,我們可以在 push 之前先清理一下,將它刪除。在程式中加入這一行:
// ... .tap(function () { this.string('name', this.vars.len); delete this.vars.len; }).uint8('age').uint16le('height')
再執行一次,你可以看到這個作為 meta 功能的 len 欄位就不見啦!Bravo!!
{ sex: 1, name: 'simen', age: 37, height: 17200 }
六、Loop
現在我們再做一件事,就是在程式碼的最後一行再將 buffer 寫入一次、兩次或三次,隨便~
var Dissolve = require('dissolve'); // ... console.log(e); } }); parser.write(personBuf); parser.write(personBuf); parser.write(personBuf); parser.write(personBuf);
執行看看,結果如下。你發現什麼!我依序一直寫入那麼多 buffer,parser 竟然只剖析了 1 次阿!這就是我在 4.4 小節中所說的事情~
{ sex: 1, name: 'simen', age: 37, height: 17200 }
用 loop() 包裹剖析工作!將程式碼另存為 personParser_02.js,內容如下:
最後執行一下!結果會怎樣咧~
「各!位!觀!眾!........ 黑桃...... . A斯!」
很好!這一刻起,你已經入門了!接下來有件事情要你試試看(假如你有空,而且沒有弄得很賭爛的話....)
我使用以下的格式來編出封包的二進位 buffer
其中 format 列中的 x(1),(1) 指的是它占用 1 byte;y(len) 是它占用 len 個 bytes,而 len 的值是由它的前導欄位所決定
依照此規則編出來的 buffer 如下:
挑戰一下!將這段 buffer 給 parse 回 data1 吧!科科.... 如果你 parse 的出來,就表示你完全OK了!不蓋你!
如果第七節的挑戰,你實在搞不出來!沒關係!下一篇文章,我會介紹如何使用小弟所寫的 dissolve-chunks 模組,用宣告式的方式來撰寫 parser!事情會容易許多!
var Dissolve = require('dissolve'); var personBuf = new Buffer([ 0x01, 0x05, 0x73, 0x69, 0x6d, 0x65, 0x6e, 0x25, 0x30, 0x43 ]); var parser = Dissolve().loop(function (end) { this.uint8('sex').uint8('len') .tap(function () { this.string('name', this.vars.len); delete this.vars.len; }).uint8('age').uint16le('height') .tap(function () { this.push(this.vars); this.vars = {}; }); }); parser.on('readable', function() { var e; while (e = parser.read()) { console.log(e); } }); parser.write(personBuf); parser.write(personBuf); parser.write(personBuf); parser.write(personBuf);
最後執行一下!結果會怎樣咧~
「各!位!觀!眾!........ 黑桃...... . A斯!」
{ sex: 1, name: 'simen', age: 37, height: 17200 } { sex: 1, name: 'simen', age: 37, height: 17200 } { sex: 1, name: 'simen', age: 37, height: 17200 } { sex: 1, name: 'simen', age: 37, height: 17200 }
很好!這一刻起,你已經入門了!接下來有件事情要你試試看(假如你有空,而且沒有弄得很賭爛的話....)
七、牛刀小試
假設現在我有一個物件 data1 長得像這樣var data1 = { x: 100, y: 'hello', z: { z1: 30, z2: 'world!', z3: [ 1, 2, 3, 4, 5 ] }, m: [ 'It ', 'makes ', 'my ', 'life ', 'easier.' ] };
我使用以下的格式來編出封包的二進位 buffer
其中 format 列中的 x(1),(1) 指的是它占用 1 byte;y(len) 是它占用 len 個 bytes,而 len 的值是由它的前導欄位所決定
依照此規則編出來的 buffer 如下:
var data1_buf = new Buffer([ 0x64, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x1e, 0x06, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x03, 0x49, 0x74, 0x20, 0x06, 0x6d, 0x61, 0x6b, 0x65, 0x73, 0x20, 0x03, 0x6d, 0x79, 0x20, 0x05, 0x6c, 0x69, 0x66, 0x65, 0x20, 0x07, 0x65, 0x61, 0x73, 0x69, 0x65, 0x72, 0x2e ]);
挑戰一下!將這段 buffer 給 parse 回 data1 吧!科科.... 如果你 parse 的出來,就表示你完全OK了!不蓋你!
八、小結
這系列目前為止,我們學會如何使用 concentrate 與 dissolve 來組合與剖析封包!其實你已經可以動手規劃屬於自己的 Monitor and Test Instruction Sets 了 (MT指令集),制定一些控制碼欄位、狀態碼欄位,在你的 node 跟單晶片上施展神奇魔法!如果你是 Maker,我相信這點小知識或許可以讓你更上一層樓哦!試試看,用 Node 跟你自己的單晶片應用程式透過 UART 進行 RPC (remote process communication)!如果第七節的挑戰,你實在搞不出來!沒關係!下一篇文章,我會介紹如何使用小弟所寫的 dissolve-chunks 模組,用宣告式的方式來撰寫 parser!事情會容易許多!
可以实现一套rpc报文,用这个方法
ReplyDelete