Californium 開源框架分析
引言
物聯網時代,所有裝置都可以接入我們的網際網路。想想看只要有一臺智慧手機,就可以操控所有的裝置,也可以獲取到所有裝置採集的資訊。不過,並不是所有裝置都支援HTTP協議的,而且讓裝置支援HTTP協議也不現實,因為對於裝置來說,這個協議太重了,會消耗大量的頻寬和電量。於是CoAP協議也就運應而生了,我們可以把它看為超簡化版的HTTP協議。而Californium框架,就是對CoAP協議的Java實現。
CoAP協議
在閱讀Californium框架之前,我們需要對CoAP協議有個大致的瞭解,已經懂得了的同學可以直接跳過本章節。
CoAP報文
首先讓我們看一下CoAP協議的報文是長啥樣的:
Version (Ver):長度為2位,表示CoAP協議的版本號。當前版本為01(二進位制表示形式)。
Type (T):長度為2位,表示報文型別。其中各型別及二進位制表示形式如下,Confirmable (00)、Non-confirmable (01)、Acknowledgement (10)、Reset (11)。在描述的時候為了簡便,會將Confirmable縮寫為CON,Non-confirmable縮寫為NON,Acknowledgement縮寫為ACK,Reset縮寫為RST。比如一個報文的型別為Confirmable,我們就會簡寫為CON報文。
Token Length (TKL)
Code:長度為8位,表示響應碼。其中前3位代表一個數,後5位代表一個數。如010 00000,轉為十進位制就是2.00(表示時中間帶一個點),其意思可以理解為HTTP中200 OK響應碼。
Message ID:長度為16位,表示訊息id。用來表示是否為同一個的報文(重發場景下,去重會用到),或者CON請求報文和ACK響應報文的匹配。
Token:長度由TKL欄位決定,表示一次會話記錄。用來關聯請求和響應。有人可能有疑惑,Message ID不是可以將請求和響應關聯嗎?的確,CON型別的請求報文與ACK型別的響應報文可以用Message ID進行關聯,但NON型別的報文由於沒有要求是一對的,所以如果NON型別的報文想成對,那就只能通過相同的Token來匹配了。
Options:長度不確定,表示報文的選項。類似為HTTP的請求頭,內容包括Uri-Host、Uri-Path、Uri-Port等等。
1 1 1 1 1 1 1 1:Payload Marker,用來隔離Options欄位和Payload欄位。
Payload:長度由資料包決定,表示應用層需要的資料。
訊息傳輸模型
CoAP協議是雖然是建立在UDP之上的,但是它有可靠和不可靠兩種傳輸模型。
可靠傳輸模型
如上圖,客戶端通過發起一個CON報文(Message ID = 0x7d34),服務端在收到CON報文之後,需要回復一個ACK報文(Message ID = 0x7d34)。通過Message ID將CON報文和ACK報文對應起來。
確保可靠傳輸的方法有倆:其一,通過服務端回覆ACK報文,客戶端可以確認CON報文已被服務端接收;其二,超時重傳機制。若客戶端在一定時間內未收到ACK報文,則認為CON報文已經在鏈路上丟失,這時候就會重傳CON報文,重傳時間和次數可配置。
不可靠傳輸模型
如上圖,客戶端發起一個NON報文(Message ID = 0x01a0)之後,服務端無需回覆響應,客戶端也不會重發。
請求與響應模型
由於存在可靠與不可靠兩種傳輸模型,那麼對應的也會存在兩種請求與響應模型。
CON請求,ACK響應
如上圖,客戶端發起了一個CON報文(Message ID = 0xbc90, Code = 0.01 GET, Options = {“Uri-Path”:”/temperature”}, Token = 0×71),服務端在收到查詢溫度的請求之後,回覆ACK報文(Message ID = 0xbc90, Code = 2.05 Content, Payload = “22.5 C”, Token = 0×71)。也就是說服務端可以在ACK報文中,就將客戶端查詢溫度的結果一起返回。
當然,還有一種情況,那就是服務端可能由於某些原因不馬上返回結果。如上圖,客戶端發起查詢溫度的CON報文之後,服務端先回復ACK報文。一段時間過後,服務端再發起CON報文給客戶端,並將溫度的結果一起攜帶,客戶端收到結果之後回覆ACK報文。
NON請求,NON響應
如上圖,客戶端發起了一個NON報文(Message ID = 0x7a11, Code = 0.01 GET, Options = {“Uri-Path”:”/temperature”}, Token = 0×74),服務端在收到查詢溫度的請求之後,回覆NON報文(Message ID = 0x23bc, Code = 2.05 Content, Payload = “22.5 C”, Token = 0×74)。
可以發現,CON型別的請求報文與ACK型別的響應報文是通過Message ID進行匹配,NON型別的請求報文與NON型別的響應報文則是通過Token進行匹配。
至此,咱們的CoAP協議初學之路已到了終點,如果還想詳細研究的同學,可以查閱RFC 7252,這裡就不再做詳述了!那麼,接下來就讓我們對Californium開源框架一探究竟吧!
分析入口
想要分析一個框架,最好的方法就是先使用它,再通過debug,一步步地瞭解它是如何執行的。
首先在pom.xml檔案裡引入Californium開源框架的依賴:
1 2 3 4 5 |
|
其次,我們只要在Main函式裡敲兩行程式碼,服務端就啟動起來了:
1 2 3 4 5 6 7 8 |
|
那麼,接下來就讓我們從CoapServer這個類開始,對整個框架進行分析。首先讓我們看看構造方法CoapServer()裡面做了哪些事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
構造方法初始化了一些成員變數。其中,Endpoint負責與網路進行通訊,MessageDeliverer負責分發請求,Resource負責處理請求。接著讓我們看看啟動方法start()又做了哪些事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
啟動方法很簡單,主要是將所有的Endpoint一個個啟動。至此,服務端算是啟動成功了。讓我們稍微總結一下幾個類的關係:
如上圖,訊息會從Network模組傳輸給對應的Endpoint節點,所有的Endpoint節點都會將訊息推給MessageDeliverer,MessageDeliverer根據訊息的內容傳輸給指定的Resource,Resource再對訊息內容進行處理。
接下來,將讓我們再模擬一個客戶端發起一個GET請求,看看服務端是如何接收和處理的吧!客戶端程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
通過前面分析,我們知道Endpoint是直接與網路進行互動的,那麼客戶端發起的GET請求,應該在服務端的Endpoint中收到。框架中Endpoint介面的實現類只有CoapEndpoint,讓我們深入瞭解一下CoapEndpoint的內部實現,看看它是如何接收和處理請求的。
CoapEndpoint類
CoapEndpoint類實現了Endpoint介面,其構造方法如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
從構造方法可以瞭解到,其內部結構如下所示:
那麼,也就是說客戶端發起的GET請求將被InboxImpl類接收。InboxImpl類實現了RawDataChannel介面,該介面只有一個receiveData(RawData raw)方法,InboxImpl類的該方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
再往receiveMessage(RawData raw)方法裡看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
接下來,我們分別對MessageInterceptor(訊息攔截器)、Matcher(匹配器)、CoapStack(Coap協議棧)進行分析,看看他們接收到請求後做了什麼處理。
MessageInterceptor介面
框架本身並沒有提供該介面的任何實現類,我們可以根據業務需求實現該介面,並通過CoapEndpoint.addInterceptor(MessageInterceptor interceptor)方法新增具體的實現類。
Matcher類
我們主要看receiveRequest(Request request)方法,看它對客戶端的GET請求做了哪些操作:
1 2 3 4 5 6 |
|
CoapStack類
CoapStack的類圖比較複雜,其結構可以簡化為下圖:
有人可能會疑惑,這個結構圖是怎麼來,答案就在構造方法裡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
迴歸正題,繼續看CoapStack.receiveRequest(Exchange exchange, Request request)方法是怎麼處理客戶端的GET請求:
1 2 3 |
|
CoapStack在收到請求後,交給了StackBottomAdapter去處理,StackBottomAdapter處理完後就會依次向上傳遞給ReliabilityLayer、BlockwiseLayer、ObserveLayer,最終傳遞給StackTopAdapter。中間的處理細節就不詳述了,直接看StackTopAdapter.receiveRequest(Exchange exchange, Request request)方法:
1 2 3 4 5 6 7 8 9 |
|
可以看到,StackTopAdapter最後會將請求傳遞給MessageDeliverer,至此CoapEndpoint的任務也就算完成了,我們可以通過一張請求訊息流程圖來回顧一下,一個客戶端GET請求最終是如何到達MessageDeliverer的:
MessageDeliverer介面
框架有ServerMessageDeliverer和ClientMessageDeliverer兩個實現類。從CoapServer的構造方法裡知道使用的是ServerMessageDeliverer類。那麼就讓我們看看ServerMessageDeliverer.deliverRequest(Exchange exchange)方法是如何分發GET請求的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
當MessageDeliverer找到Request請求對應的Resource資源後,就會交由Resource資源來處理請求。(是不是很像Spring MVC中的DispatcherServlet,它也負責分發請求給對應的Controller,再由Controller自己處理請求)
Resource介面
還記得CoapServer構造方法裡建立了一個RootResource嗎?它的資源路徑為空,而客戶端發起的GET請求預設也是空路徑。那麼ServerMessageDeliverer就會把請求分發給RootResource處理。RootResource類沒有覆寫handleRequest(Exchange exchange)方法,所以我們看看CoapResource父類的實現:
1 2 3 4 5 6 7 8 9 |
|
由於我們客戶端發起的是GET請求,那麼將會進入到RootResource.handleGET(CoapExchange exchange)方法:
1 2 3 4 |
|
再接著看CoapExchange.respond(ResponseCode code, String payload)方法:
1 2 3 4 5 6 7 8 9 10 11 |
|
看看同名函式裡又做了哪些操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
那麼Exchange.sendResponse(Response response)又是如何傳送響應的呢?
1 2 3 4 5 6 7 8 9 10 11 |
|
原來最終還是交給了Endpoint去傳送響應了啊!之前的GET請求就是從Endpoint中來的。這真是和達康書記一樣,從人民中來,再到人民中去。
在CoapEndpoint類一章節中我們有介紹它的內部結構。那麼當傳送響應的時候,將與之前接收請求相反,先由StackTopAdapter處理、再是依次ObserveLayer、BlockwiseLayer、ReliabilityLayer處理,最後由StackBottomAdapter處理,中間的細節還是老樣子忽略,讓我們直接看StackBottomAdapter.sendResponse(Exchange exchange, Response response)方法:
1 2 3 |
|
請求入口是CoapEndpoint.InboxImpl,而響應出口是CoapEndpint.OutboxImpl,簡單明瞭。最後,讓我們看看OutboxImpl.sendResponse(Exchange exchange, Response response)吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
通過一張響應訊息流程圖來回顧一下,一個服務端響應最終是如何傳輸到網路裡去:
總結
通過服務端的建立和啟動,客戶端發起GET請求,服務端接收請求並返回響應流程,我們對Californium框架有了一個整體的瞭解。俗話說,師父領進門,修行看個人。在分析這個流程的過程中,我省略了很多的細節,意在讓大家對框架有個概念上的理解,在以後二次開發或定位問題時更能抓住重點,著重針對某個模組。最後,也不得不讚嘆一下這款開源框架程式碼邏輯清晰,模組職責劃分明確,靈活地使用設計模式,非常值得我們學習!