背景
用戶在訪問我們的網(wǎng)站時(shí)斋日,可以選擇使用線下POS機(jī)進(jìn)行支付,因此我們需要集成并控制POS機(jī)完成刷卡操作和返回?cái)?shù)據(jù)。
然而噪窘,POS機(jī)提供方郵件發(fā)送過來的并不是我們預(yù)想的Http接口或是SDK吏廉,而是150多頁的一份串口集成文檔...(文中涉及的代碼泞遗、編號(hào)、枚舉值都已經(jīng)過模糊處理)
令人頭暈的二進(jìn)制
不同于我們?nèi)粘K褂玫腍TTP協(xié)議:具有標(biāo)準(zhǔn)結(jié)構(gòu)和完備的SDK席覆;可以很容易的構(gòu)建起Server-Client進(jìn)行數(shù)據(jù)傳輸史辙;無需關(guān)注應(yīng)用層(ISO七層)以下的實(shí)現(xiàn)。而串口更像是物理層佩伤,通過指定頻率的高低電平(0/1)來傳輸數(shù)據(jù)聊倔。
因此在使用串口通信時(shí),通常需要設(shè)計(jì)一套自有協(xié)議表達(dá)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)生巡。例如:
image-20211127144504102.png
- 主要部分為消息頭(Header)和消息數(shù)據(jù)(Data)耙蔑,消息數(shù)據(jù)可以有多個(gè)
- 通過
Field Code
可區(qū)別不同的數(shù)據(jù),是一個(gè)變長的數(shù)據(jù)- 數(shù)據(jù)的主要類型為Hex(十六進(jìn)制)孤荣、BCD(二進(jìn)制化整數(shù))甸陌、ASC(asiic碼)
對(duì)照著上述結(jié)構(gòu)來構(gòu)造一個(gè)消息并不是一件困難的事,然而不同的類型的功能指令(Function)會(huì)包含大量不同的消息數(shù)據(jù)(Field Data)盐股。如果我們面向一個(gè)一個(gè)功能指令開發(fā)钱豁,效率低且代碼難以維護(hù) —— 從我們拿到的一份Legacy的代碼中,也能看出這點(diǎn):
在一個(gè)類中疯汁,既有對(duì)業(yè)務(wù)字段的賦值牲尺,又有對(duì)底層數(shù)據(jù)格式的序列化。再加上大量的魔法字符,讓我抄都不知道從何抄起...
于是在“修改這份代碼使其適應(yīng)新版本協(xié)議”和“重寫谤碳,重寫溃卡,重寫!”中蜒简,我毅然選擇了后者...
“封裝瘸羡,他使用了封裝!”
那如何開發(fā)一個(gè)適配底層協(xié)議的SDK呢臭蚁?
遇事不決最铁,量子力學(xué)(不是) / 遇事不決,面向?qū)ο?/strong>(還行)
面向?qū)ο蟮囊淮髢?yōu)勢(shì)是利用封裝垮兑,使邏輯高內(nèi)聚低耦合冷尉。
首先,我將三個(gè)字段類型進(jìn)行了封裝:BCD系枪、ASC雀哨、Hex,使其實(shí)現(xiàn)Attribute接口以實(shí)現(xiàn)toBytes()
方法私爷。此時(shí)業(yè)務(wù)所會(huì)用到的數(shù)據(jù)類型都和Bytes沒有了直接關(guān)系雾棺,BCD、ASC衬浑、Hex成為了實(shí)質(zhì)上的的基本類型捌浩。
同理,Token工秩、分隔符尸饺、長度這些和功能指令(業(yè)務(wù)側(cè))沒有直接關(guān)聯(lián)的數(shù)據(jù)類型也被我抽取了出來。
此時(shí)的Message和0101已完全解耦助币,變成了只含三個(gè)字段類型的POJO類浪听。
一層一層又一層
但是直接使用Message時(shí)還是有一些困難
- ASC對(duì)于上層業(yè)務(wù)指令來說還是太細(xì)節(jié),而且無法很好的表達(dá)業(yè)務(wù)含義
- 對(duì)于指令功能(Function)眉菱,他們不關(guān)心下層如何序列化我的數(shù)據(jù)迹栓,只關(guān)心業(yè)務(wù)數(shù)據(jù)是否正確的被設(shè)置和接收
- 對(duì)于消息數(shù)據(jù)(Message & Message Data),他們不關(guān)心上層數(shù)據(jù)有什么業(yè)務(wù)含義俭缓,也不關(guān)心下層這些數(shù)據(jù)如何被發(fā)送
多重施法克伊! —— 就像Attribute一樣,使用Field接口將上層的業(yè)務(wù)字段進(jìn)行了隔離华坦。此后愿吹,當(dāng)你想要發(fā)起一個(gè)指令,你只需要用富含業(yè)務(wù)信息的Field組建你的數(shù)據(jù)季春,接口會(huì)幫你完成剩下所有的事洗搂。
val request = MakePaymentRequest(ID = "000001", amount = 200.00)
val response = client.send(request)
val isSuccess = response.responseCode == ResponseCode.APPROVED
當(dāng)然還得加上串口連接的部分消返,里面會(huì)使用Blocking隊(duì)列來將異步操作同步化
各個(gè)組件通過接口Request/Response, Message, Field, Attribute進(jìn)行協(xié)作载弄,大大增加了靈活性和可擴(kuò)展性
也使用了注解耘拇、反射來統(tǒng)一實(shí)現(xiàn)對(duì)象的序列化操作
image-20211127160401744.png
測試...
Of cause,為了避免破壞已經(jīng)構(gòu)建好的功能宇攻,測試也是開發(fā)過程中需要慎重對(duì)待的環(huán)節(jié)(前面錯(cuò)一個(gè)bit惫叛,后面讀出來的消息能跟鬼畫符一樣...)。對(duì)于協(xié)議(protocol)層來說逞刷,TDD用起來是非常爽的且高效的嘉涌,但是到了數(shù)據(jù)傳輸部分就難搞起來了。
- 串口的讀寫操作是異步的夸浅,讀操作是通過注冊(cè)監(jiān)聽器實(shí)現(xiàn)的
- 因?yàn)轭愃崎L鏈接仑最,在傳輸過程中遇到錯(cuò)誤會(huì)發(fā)送ACK/NACK的握手信息,并且會(huì)觸發(fā)重試
Option 1:構(gòu)造多線程測試環(huán)境
-
創(chuàng)建Stub Server:使用了
PipedInputStream,PipedOutputStream
將Client的讀寫流給包裝起來帆喇,通過另一個(gè)線程來模擬Server操作里面的數(shù)據(jù)警医,實(shí)現(xiàn)接收請(qǐng)求、返回?cái)?shù)據(jù)坯钦。val serverInputStream = PipedInputStream() val serverOutputStream = PipedOutputStream() val clientInputStream = PipedInputStream(serverOutputStream) // server output -> client input val clientOutputStream = PipedOutputStream(serverInputStream) // client output -> server input val connection = StreamSerialChannel(clientInputStream, clientOutputStream) val mockServer = Thread { Thread.sleep(50) // 1. wait for client serverInputStream.read(ByteArray(requestLength)) // 2. read request in server side serverOutputStream.write(ACK.getBytes()) // 3. send ack to client connection.onDataAvailable() // 4. notify client - simulate comm listener serverOutputStream.write(responseBytes) // 5. send response to client connection.onDataAvailable() // 6. notify client - simulate comm listener Thread.sleep(50) // 7. wait for client serverInputStream.read(ByteArray(1)) // 8. read ack in server side } val client = Client(connection) ....
Option 2:使用Fake的外部程序
-
虛擬串口:Windows和Linux上都有現(xiàn)成的串口調(diào)試工具预皇,Win上的有界面更方便
我使用的是Windows Virtual Serial Port Driver,因?yàn)樘摂M串口不好操作寫數(shù)據(jù)(或者是我太菜)婉刀,我創(chuàng)建了2個(gè)虛擬串口A - B吟温,將他們Pair起來;
還是如上起2個(gè)線程突颊,這時(shí)候Client通過串口A寫入的數(shù)據(jù)鲁豪,會(huì)被正在監(jiān)聽串口B的Server捕獲;
Server如期返回?cái)?shù)據(jù)后洋丐,Client又能夠從串口A接收到Response呈昔;
而且所有數(shù)據(jù)操作和控制都在Test代碼里,一鍵操作還是不錯(cuò)的友绝;
-
USB轉(zhuǎn)串口芯片(稍微硬核)
從某寶網(wǎng)購一塊USB轉(zhuǎn)TTL的串口芯片堤尾,裝上驅(qū)動(dòng)就能用;
把read和write引腳短接迁客,可以驗(yàn)證串口連接和數(shù)據(jù)傳輸是否正確郭宝;
但是想要走通整個(gè)流程,還得控制write的數(shù)據(jù) —— write/read接上樹莓派掷漱,然后(一頓操作)就可以了粘室;
Option 3:連接測試機(jī)
(主要是我們的測試機(jī)器到的太晚,而且還是遠(yuǎn)程調(diào)試卜范,所以才想了上面的辦法先保證基本功能沒問題)
(不過由于前期的良好測試衔统,拿到測試機(jī)后直接run test case沒太大問題)
后記(腦補(bǔ))
此文僅以筆者經(jīng)驗(yàn),闡述使用面向?qū)ο笫址ǚ庋b串口協(xié)議的一種方式。
雖然分析實(shí)現(xiàn)過程以“面向?qū)ο蟆睘橹鹘蹙簦庋b舱殿、基礎(chǔ)、多態(tài)均有险掀,但都是抽象的一種手法 —— 抽象沪袭,即是編程的本質(zhì),對(duì)問題域和解決方案域的提煉樟氢。選擇合適的角度和層級(jí)分析問題冈绊、找尋共性、得出答案埠啃,將過程抽象為模型死宣、方法論、原則碴开,最后用何種代碼十电、何種工具進(jìn)行實(shí)現(xiàn)就不再是問題了。這也是架構(gòu)師的職責(zé)所在 —— 將混沌叹螟、繁雜的問題通過分析變?yōu)閺?fù)雜鹃骂、簡單的知識(shí)(Cynefin)。
雖然此處只是對(duì)串口協(xié)議做了一層封裝罢绽,但相關(guān)的分析方式畏线、分層模型一樣可以套用到其他領(lǐng)域:
- 高級(jí)語言對(duì)匯編指令的抽象封裝
- kubctl對(duì)K8S組件的抽象封裝
- 云服務(wù)對(duì)軟硬件服務(wù)的抽象抽象
- ...
Learning:在如今的云原生時(shí)代,抽象的層級(jí)又被拔高了一些良价,但軟件工程的核心理論(短期)不會(huì)改變寝殴。如何將既有的分析模式、架構(gòu)設(shè)計(jì)擴(kuò)展到云原生領(lǐng)域...