# 使用 EBNF 來撰寫通訊協定的人讀、機讀共用文件 * Git repo:https://github.com/linahbei/continues-protocol-development * 共筆版本:https://hackmd.io/JreKVtPZR-uD78Pe1tY2Tg?view ###### tags `持續開發`, `系統設計` --- ## 1. 概觀 ### 什麼是 BNF / EBNF BNF 是一種用來「描述語法」的表達式,可以理解為**描述程式語言怎麼定義的語言**。身為工程師一定對 BNF 的用法不陌生,只是熟悉度可能不同。例如命令列指令、說明文件中,表示指令、參數**可選 (option)** 的中括號 “``[ … ]``”,或是**必要**的大括號 “``< … >``”。 ### 問題描述 設計、實作通訊協定的時候,花費最多人力重複工作 (routines)、重工 (re-work) 的任務,就是人讀文件,和機讀 (可程式化) 文件的同步、一致性維護。尤其是身為軟體開發團隊,這種*非***持續開發 (continues)**,需求、文件、實作不能反應真實狀況的方式,不僅僅是勞心勞力,也不太聰明。 特別是開發非標準、工具尚未完善的通訊協定時,這種不一致,追著**歷史文件**考古的現象就會更嚴重。舉例來說,如果是開發 RESTful API,我們還有現成的 [OpenAPI](https://swagger.io/specification/) 可以救場。但是開發自訂的通訊協定時,各種缺少工程意義的討論方式、文件產出,就會讓規格變得難以閱讀 (不論是過於精簡或是囉嗦)。 ### 解決方案 我們選擇使用 EBNF,來撰寫通訊協定的人讀、機讀共用文件;更重要的是,開始練習像個工程師一樣的工作。 ## 2. 準備工作 ### 風險評估與專案資源盤點 雖然說建置一個持續開發、持續文件的模式是這麼的美好,但是身為合格的從業工程師,別忘了首要工作是交付可用的軟體。至於什麼是可用的軟體,就要考量你的通訊協定究竟有多大、後續會怎麼變動、是誰來用,還有專案可用的資源。 即使是開發標準的通訊協定專案,使用現成熟悉的工具,例如 OpenAPI、Jupyter Notebook,重點仍然是交出可用的東西。 ### 背景知識與工具 #### 正規表示式 (Regular Expression) 如果你已經閱讀過 BNF / EBNF 的資料了,請再了解[正規表示式](https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F)。後續在撰寫 EBNF 中會用到。 #### 參考現有的撰寫方式 我們在設計通訊協定的時候,可能會遇到不同類型的格式,例如編碼格式 -- binary、plain text,或是欄位格式 -- fixed、標記語言等等。因此可以參考現有、類似規格的 EBNF 撰寫方法。 |編碼格式|欄位格式|參考範例| |:------|:------|:-----| |Plain text|標記語言|[JSON](https://github.com/cierelabs/yaml_spirit/blob/master/doc/specs/json-ebnf.txt)| |Binary|Fixed|Modbus| |Plain text|Fixed|Modbus RTU| #### 工具程式 ##### 詞法分析、語法分析 **機讀**工具,用來產生分析 BNF 的程式碼 ##### 語意分析 (?) ##### 文件產生器 **人讀**工具,用來產生 user manual。雖然說 BNF 已經是**可以人讀**的格式了,不過我們還是需要一份合格、可用的 user (development) manual。 * [EBNF Visualizer](https://people.cs.vt.edu/~kafura/ComputationalThinking/Class-Notes/Manual/manual.html) ## 3. 實作練習 ### 設計一個簡單的 plain text / fixed protocol 首先,我們先用一般的開發方法 -- 也就是用程式實作 + 文件撰寫的思維,規劃一個簡單的**遠端開關控制** protocol。這是個一問一答、一對一的 protocol,用途是通知遠端的某個控制器,開啟或是關閉開關 (relay),並且回復成功與否。(要設計一個簡單又有明顯錯誤的範例真不容易) 如果這個 protocol 是由一個不愛寫文件的工程師來規劃,第一版的**文件**、規格書可能長這樣: ```c= struct packet_s { char header; //'C' = commanding, 'R' = responding short int sn; //Serial number / tag, 0 ~ 32767 short int relay; //Relay ID, 0 ~ 32767 short int flag; //Commanding: 0 = turn OFF, 1 = turn ON //Responding: 2 = OK, 3 = Fail }; ``` ### 開始寫規格書,同步需求 從以上的規格,我們可以發現一個溝通上的問題,既然這是個 plain text 的 protocol,那麼整數型別的 ``sn``, ``relay``,和 ``flag`` 是怎麼回事?實作的時候會不會產生什麼誤會?不過既然 PoC 程式也寫好了,只能從規格書修正、補完這段設計。 首先,先用正規表示式,把 protocol 格式明確的定義下來;這裡的明確定義,指的是讓 reviewer 的大腦冷靜下來,好好思考怎麼定義出合法、可以使用的 protocol。 ``` ^(C|R)[0-9]{5}[0-9]{5}[0-3]{1}$ ``` :::info 一個合法的 protocol、指令會是這樣:``R00201001001`` ::: 然後再依照 plain text 的技術限制、建置需求,把規格稍微整理清楚: ``` Protocl 格式:<標頭><Tag><Relay ID><動作 Flag> * 標頭:'C' = 控制指令,'R' = 執行結果 * Tag:流水號 tag,user defined * Relay ID: 要控制的 Relay ID * 動作 Flag - 控制用:'0' = ON,'1' = OFF - 執行結果:'2' = 成功,'3' = 失敗 * 總長度:22 個字元 * Tag and ID range:'00000' ~ '32767' (5 digits ASCII decimal char with leading zeros) ``` 如果再講究一點,後面還有文書排版工作、範例程式說明要寫。 ### 用 EBNF 描述這段 protocol 看完以上各版本阿哩拉雜的規格,還有共通文件、溝通上的問題後 (例如:相依於特定程式語言,繁瑣的文字描述,精簡但是不知道在幹嘛的正規表示式),我們可以將 protocol 改寫為語言無關的 EBNF。 :::info * 測試資料:``R00201001001`` * [線上工具](https://mdkrajnak.github.io/ebnftest/) ::: ```= Packet ::= (Header)(Tag)(Relay)(Flag) (* Fields *) Header ::= #'^'((Command)|(Response)) Tag ::= #'[0-9]{5}' Relay ::= #'[0-9]{5}' Flag ::= ((SendOff)|(SendOn)|(ResultOk)|(ResultFail))#'$' (* Headers *) Command ::= 'C' Response ::= 'R' (* Flags *) SendOff ::= '0' SendOn ::= '1' ResultOk ::= '2' ResultFail ::= '3' ``` 這樣改寫完後,雖然沒有很漂亮,而且 protocol 本身也是虛構、無法用於實務問題上。但是也初步完成了一個可以人讀、機讀,用於描述通訊協定的文件了。 ### 改寫為 ``Lex`` 看得懂的格式 :::info * [說明](https://zh.wikipedia.org/wiki/Lex) * [WIndows 10 安裝](http://gnuwin32.sourceforge.net/packages/flex.htm) ::: :::warning 如果我們只是要對 protocol 的封包做詞法分析,或者說 protocol 並沒有複雜到像是一個腳本語言,那麼改寫、撰寫 ``Lex`` 規則其實成本有點高。因此下一步是,尋找 EBNF 的詞法分析程式產生器。 ::: *待續...*