Android InputStream的阻塞問題(一)

Barr
Jun 24, 2024

--

Photo by Robert Zunikoff on Unsplash

這次遇到一個蠻有趣的問題,目標是使用SerialPort來與Arduino設備通訊,抽掉其他設定部分,可以簡化成兩個部分
1. 輸入:InputStream
2. 輸出:OutputStream

先簡單介紹一下InputStreamOutputStream,他們兩個是Java世界中最基本的IO流,從1.0就存在的元老級API,但相應的代價是它們也少了些現代特性,例如,它們兩個的 read() write() 都是阻塞的。

回到場景,目標是透過InputStreamOutputStream來和Arduino設備通訊,在最開始的需求中是純粹的單方通訊,單純由Android端發送Cmd到Arduino,剩下的就不干我們的事了(畢竟也沒方法確認 ¯⁠\⁠_⁠(⁠ツ⁠)⁠_⁠/⁠¯

後來在一些需求與實現改變下,可以從InputStream接收Arduino端的Ack(Acknowledgment 確認訊息),總算完成了雙端通訊,也可以針對響應執行後續的操作,但有個問題,Arduino的響應存在遺失,並不會次次Cmd都能夠正確的返回Ack。

最開始的設計因為是單向通訊無法確認結果,所以單純把Android端的Cmd送過去即可,並沒有後續的處理。但在新的需求下並須為每次Cmd都檢測Ack確認該次命令是否完成,但同時Arduino的響應可能有遺漏,這就比較頭大了。

要在最原始的流程中加上Ack處理本身很簡單,剛開始想使用兩個管道獨立處理,一邊專門處理send Cmd另一邊則處理receive Ack,兩邊完全獨立互不干涉,好處是完全不用等待Ack所有命令都可以接近無延遲下達,但問題就出在設計Ack的目的就是因為響應遺漏的後續處理問題。試想一下,如果IO兩邊完全獨立處理,該怎麼確認Cmd與Ack的對應呢?
當然每個Cmd都會有相應的Ack,但並不是完全一對一,有些Cmd的Ack是相同的;還有當同一個Cmd下達多次卻並沒有相對應次數的Ack,該怎麼確認哪一次Cmd是失效的?

問題之所以存在,也是因為Ack返回的只是特定狀態,並不帶額外參數,也沒辦法利用ID的方式來綁定Cmd和Ack,經過討論後確認以單次一對一的方式進行通訊,只有當收到Ack後才會發送下一次Cmd,避免上面遇到的問題,這就目前的前情提要了。

其實這個方案還蠻直覺的,前面也提到InputStreamOutputStream兩者皆是阻塞的,也就是說直接利用阻塞特性可以輕易讓每次命令都是同步,這也大大簡化了整體複雜度,但最麻煩的是遇到「響應遺漏」!

如果InputStream.read()是阻塞的,那如果Ack遺漏了會發生什麼?結果就是卡在最後一次Cmd!因為上面的設計是「處理完本次Cmd(收到Ack)後才會發送下一次Cmd」,所以只要Ack遺漏,就會一直等下去...

沒有關係,我們有Coroutine!既然Coroutine可以取消,那把InputStream.read()放到Coroutine裡面整個取消總可以了吧?好像還蠻直覺的對吧?

        val ack = withTimeoutOrNull(TIMEOUT_MILLIS) {
inputStream.bufferedReader().readLine()
}

如果timeout直接返回null,用null判斷為Cmd失敗,可以重試也可以跳過,這樣總該沒問題了...嗎?

後篇:Android InputStream的阻塞問題(二)

--

--