封包組合與剖析器設計@node.js (III) 產生剖析器

一、前言

上一篇文章,我們介紹如何使用 Dissolve 模組來為你的 DSL 格式封包製作剖析器,在文章最後,我留了一個牛刀小試的資料物件,以及這個資料的編制格式。但是我相信!!!  應該是沒有人有真的試著用 Dissolve 去剖析他!!  科科....
 
  好啦!假設真的有人試著去剖析他好了!(這樣我的故事才能說下去)

二、用 Dissolve 的困難

假使那一題牛刀小試你做過了,你一定會發現,這個看起來也沒有多複雜的資料,竟然會這麼難搞啊!除了數字跟字串,你還遇到了數字的陣列,以及字串的陣列。假使你對 Dissolve 的 loop 有點了解,你會發現還是有點難做,因為你會遇到 loop 中還有 loop 的情況,再考慮剖出來的資料片段要掛在哪個命名空間下,更是雪上加霜啊!而且,當你遇到 3 層以上的 loop,我保證你大概會一邊寫 code 一邊哀爸叫母~
 
  我在工作專案上就曾經遇過這樣的困難,當初是在剖析 ZigBee 的封包,最哭臉的就是 ZCL 的訊息框剖析,有興趣的人可以看看白皮書,那花樣真的很多啊~ 當初,就是照 spec 的規範,一條一條硬幹,最後當然也是順利做完啦!後來,我們有另一個 BLE 的 project,也需要做類似的事情。當時我就想,「哇操,這麼 low level 的東西,雖然是沒有多偉大,但是做起來也是挺辛苦的!這次絕對要好好地研究一下剖析器的實作,要怎樣才能有效率一點。」
 

三、Dissolve-Chunks 的誕生

正是因為要解決工作上的問題,我花了一點時間自己設計了一個「宣告式的剖析器產生器 - Dissolve-Chunks」(好繞口~)

  為什麼要取這個名字呢?因為它是基於 dissolve 這個模組,所以有很大的功勞是 dissolve 所貢獻的,所以向它致敬。後面加一個 Chunks,指的意思是:「我可以把一串很長的資料,切割成一段一段的 data chunk,每一小段都有各自對應的剖析規則來處理,各個擊破」。
 
  這個模組有一些內建的規則,也接受使用者自訂新的規則。面對這些資料,我只要把需要用到的「規則」一一宣告,接著呼叫 join() 把這些規則依序串起來,最後用 compile() 方法,就能組合出一支剖析器,而且命名空間也在宣告時指定好即可,不需要像 dissolve 一層層 tap() 時指定。當我們把最小片段的規則列出來後,還可以使用 squash() 將幾個剖析規則,擠壓成一條中小型片段資料的剖析規則。
  
  重點是,寫 parser 幾乎都是用「宣告」的,這意味著你會很少寫到「剖析過程的處理細節」 (當然,如果是要自訂剖析規則的話,你還是得自己寫處理邏輯)。
  
  這邊我要說明一件事,就是呢!當我在做 parser 的時候,我發現了兩件事情:
  • 格式定義好了,規則就是死的
  • 同樣的 parsing code 一直重複出現

  我就在想,阿規則既然是死的,然後我又一直寫重複的程式碼,在處理那些類似格式的東西,真的好沒效率。當時第一個想法,就是至少應該把重複的程式碼抽成一支函式吧!後來呢,我覺得這根本就跟資料驗證器 (data validator) 或是宣告 Schema 的想法很類似,驗證規則還不就那些,對吧!阿驗證規則也是規則,就是一撮一撮的 validating codes。當我需要執行哪一種驗證,就跟全世界宣告「我是什麼鬼」,然後就有對應的驗證規則會去驗證你是不是個鬼!(什麼鬼啊~~好爛哦!) 我想這應該就是人家講策略模式典型的例子吧!
 
  好啦!總之,我就依照我的發現跟想法,就做出了 Dissolve-Chunks 這個鬼東西,雖然說 npm 上下載量很低,應該沒有人在用啦!哈哈~ 不過呢,這個模組正實際用在我們公司自己的專案上面,到目前為止是還沒出過什麼大問題。小問題也在專案的實際應用上,一一地修正掉了,所以應該還算穩定。其實,它真的蠻好用的,節省我們工程師不少的時間。不管如何,這都是工程師寫程式解決自己痛點的好例子!
 

四、使用 Dissolve-Chunks 產生剖析器

我們的資料跟格式是
var data1 = {
    x: 100,
    y: 'hello',
    z: {
        z1: 30,
        z2: 'world!',
        z3: [ 1, 2, 3, 4, 5 ]
    },
    m: [ 'It ', 'makes ', 'my ', 'life ', 'easier.' ]
};
底下這張圖顯示了我們自訂封包的格式,比較清楚的表格可以看這裡


這一段封包,它的 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
]);
產生剖析器,並試著將這些 binaries 寫入剖析器,看看剖析結果吧!在底下這份程式碼中,它的概念是這樣的:
  • 封包格式已知,我可以用更小段的眼光,依序為每一個小段宣告它的剖析規則
  • chunkRules 這個陣列,裡面所放的每個小規則。每一個小規則,都由 ru 這個物件所提供,它提供了很多常用的規則,例如剖析 uint32le 的整數等等。
  • 每一條 ru 的規則,你可以塞給他一個名稱,例如 ru.uint8('x') 指的就是,等一下你剖析完的 uint8 整數,把它放到剖析物件的 x 這個 property 底下
  • 等到規則一一列完,就 join() 它們,最後呼叫 compile() 把 parser 編出來,這樣 parser 就產生出來吧!我們在 parser 身上掛一個 'parsed' 事件的 listener,當它剖析完之後會引發這個事件通知你
  • 最後,把我們的 binaries (上面給的那段 data1_buf) 寫入 parser 吧!!
var DChunks = require('dissolve-chunks'),
    ru = DChunks().Rule();

var chunkRules = [
    ru.squash([ru.uint8('x'), ru.stringPreLenUint8('y')]),
    ru.squash('z', [
        ru.uint8('z1'),
        ru.stringPreLenUint8('z2'),
        ru.repeat('z3', ru.uint8)
    ]),
    ru.repeat('m', ru.stringPreLenUint8)
];

var parser = DChunks().join(chunkRules).compile();

parser.on('parsed', function (result) {
    console.log(result);
});

parser.write(data1_buf);
你也可以使用另一種 style,一條一條的 rule,依序 join(),最後產生剖析器:
var chunkRules1 = [ ru.uint8('x'), ru.stringPreLenUint8('y') ],
    chunkRule2 = ru.squash('z', [
        ru.uint8('z1'),
        ru.stringPreLenUint8('z2'),
        ru.repeat('z3', ru.uint8)
    ]),
    chunkRule3 = ru.repeat('m', ru.stringPreLenUint8);

var parser = DChunks().join(chunkRules1)
                      .join(chunkRule2)
                      .join(chunkRule3)
                      .compile();

五、後記

想不到這一系列三篇文章,我可以拖那麼久啊!哈哈哈~ 其實 dissolve-chunks 的文件寫得蠻清楚的啦!如果覺得看英文可以接受的話,那麼讀原始文件會更清楚知道如何安排你的rules 或者是怎麼樣撰寫自己的 rules。
 
我想,用宣告式的方式就是程式碼看起來會精簡許多,而且也不會到處都有重複的剖析程式碼。不過,假使你很熱血,其實用 Buffer 提供的 low-level 方法直接硬幹也是可以的,作法千百種,我們只要挑選適合自己的、習慣的方式都 ok 的~~
 
 
 
 

沒有留言

技術提供:Blogger.