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):長度為4位,表示Token欄位的長度。
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 = 0x71)
,服務端在收到查詢溫度的請求之後,回覆ACK報文(Message ID = 0xbc90, Code = 2.05 Content, Payload = "22.5 C", Token = 0x71)
。也就是說服務端可以在ACK報文中,就將客戶端查詢溫度的結果一起返回。
當然,還有一種情況,那就是服務端可能由於某些原因不馬上返回結果。如上圖,客戶端發起查詢溫度的CON報文之後,服務端先回復ACK報文。一段時間過後,服務端再發起CON報文給客戶端,並將溫度的結果一起攜帶,客戶端收到結果之後回覆ACK報文。
NON請求,NON響應:
如上圖,客戶端發起了一個NON報文(Message ID = 0x7a11, Code = 0.01 GET, Options = {"Uri-Path":"/temperature"}, Token = 0x74)
,服務端在收到查詢溫度的請求之後,回覆NON報文(Message ID = 0x23bc, Code = 2.05 Content, Payload = "22.5 C", Token = 0x74)
。
可以發現,CON型別的請求報文與ACK型別的響應報文是通過Message ID進行匹配,NON型別的請求報文與NON型別的響應報文則是通過Token進行匹配。
至此,我們們的CoAP協議初學之路已到了終點,如果還想詳細研究的同學,可以查閱RFC 7252,這裡就不再做詳述了!那麼,接下來就讓我們對Californium開源框架一探究竟吧!
分析入口
想要分析一個框架,最好的方法就是先使用它,再通過debug,一步步地瞭解它是如何執行的。
首先在pom.xml檔案裡引入Californium開源框架的依賴:
<dependency> <groupId>org.eclipse.californium</groupId> <artifactId>californium-core</artifactId> <version>2.0.0-M1</version> </dependency>
其次,我們只要在Main函式裡敲兩行程式碼,服務端就啟動起來了:
public static void main(String[] args) { // 建立服務端 CoapServer server = new CoapServer(); // 啟動服務端 server.start(); }
那麼,接下來就讓我們從CoapServer這個類開始,對整個框架進行分析。首先讓我們看看構造方法CoapServer()
裡面做了哪些事:
public CoapServer(final NetworkConfig config, final int... ports) { // 初始化配置 if (config != null) { this.config = config; } else { this.config = NetworkConfig.getStandard(); } // 初始化Resource this.root = createRoot(); // 初始化MessageDeliverer this.deliverer = new ServerMessageDeliverer(root); CoapResource wellKnown = new CoapResource(".well-known"); wellKnown.setVisible(false); wellKnown.add(new DiscoveryResource(root)); root.add(wellKnown); // 初始化EndPoints this.endpoints = new ArrayList<>(); // 初始化執行緒池 this.executor = Executors.newScheduledThreadPool(this.config.getInt(NetworkConfig.Keys.PROTOCOL_STAGE_THREAD_COUNT), new NamedThreadFactory("CoapServer#")); // 新增Endpoint for (int port : ports) { addEndpoint(new CoapEndpoint(port, this.config)); } }
構造方法初始化了一些成員變數。其中,Endpoint負責與網路進行通訊,MessageDeliverer負責分發請求,Resource負責處理請求。接著讓我們看看啟動方法start()
又做了哪些事:
public void start() { // 如果沒有一個Endpoint與CoapServer進行繫結,那就建立一個預設的Endpoint ... // 一個一個地將Endpoint啟動 int started = 0; for (Endpoint ep:endpoints) { try { ep.start(); ++started; } catch (IOException e) { LOGGER.log(Level.SEVERE, "Cannot start server endpoint [" + ep.getAddress() + "]", e); } } if (started==0) { throw new IllegalStateException("None of the server endpoints could be started"); } }
啟動方法很簡單,主要是將所有的Endpoint一個個啟動。至此,服務端算是啟動成功了。讓我們稍微總結一下幾個類的關係:
如上圖,訊息會從Network模組傳輸給對應的Endpoint節點,所有的Endpoint節點都會將訊息推給MessageDeliverer,MessageDeliverer根據訊息的內容傳輸給指定的Resource,Resource再對訊息內容進行處理。
接下來,將讓我們再模擬一個客戶端發起一個GET請求,看看服務端是如何接收和處理的吧!客戶端程式碼如下:
public static void main(String[] args) throws URISyntaxException { // 確定請求路徑 URI uri = new URI("127.0.0.1"); // 建立客戶端 CoapClient client = new CoapClient(uri); // 發起一個GET請求 client.get(); }
通過前面分析,我們知道Endpoint是直接與網路進行互動的,那麼客戶端發起的GET請求,應該在服務端的Endpoint中收到。框架中Endpoint介面的實現類只有CoapEndpoint,讓我們深入瞭解一下CoapEndpoint的內部實現,看看它是如何接收和處理請求的。
CoapEndpoint類
CoapEndpoint類實現了Endpoint介面,其構造方法如下:
public CoapEndpoint(Connector connector, NetworkConfig config, ObservationStore store) { this.config = config; this.connector = connector; if (store == null) { this.matcher = new Matcher(config, new NotificationDispatcher(), new InMemoryObservationStore()); } else { this.matcher = new Matcher(config, new NotificationDispatcher(), store); } this.coapstack = new CoapStack(config, new OutboxImpl()); this.connector.setRawDataReceiver(new InboxImpl()); }
從構造方法可以瞭解到,其內部結構如下所示:
那麼,也就是說客戶端發起的GET請求將被InboxImpl類接收。InboxImpl類實現了RawDataChannel介面,該介面只有一個receiveData(RawData raw)
方法,InboxImpl類的該方法如下:
public void receiveData(final RawData raw) { // 引數校驗 ... // 啟動執行緒處理收到的訊息 runInProtocolStage(new Runnable() { @Override public void run() { receiveMessage(raw); } }); }
再往receiveMessage(RawData raw)
方法裡看:
private void receiveMessage(final RawData raw) { // 解析資料來源 DataParser parser = new DataParser(raw.getBytes()); // 如果是請求資料 if (parser.isRequest()) { // 一些非關鍵操作 ... // 訊息攔截器接收請求 for (MessageInterceptor interceptor:interceptors) { interceptor.receiveRequest(request); } // 匹配器接收請求,並返回Exchange物件 Exchange exchange = matcher.receiveRequest(request); // Coap協議棧接收請求 coapstack.receiveRequest(exchange, request); } // 如果是響應資料,則與請求資料一樣,分別由訊息攔截器、匹配器、Coap協議棧接收響應 ... // 如果是空資料,則與請求資料、響應資料一樣,分別由訊息攔截器、匹配器、Coap協議棧接收空資料 ... // 一些非關鍵操作 ... }
接下來,我們分別對MessageInterceptor(訊息攔截器)、Matcher(匹配器)、CoapStack(Coap協議棧)進行分析,看看他們接收到請求後做了什麼處理。
MessageInterceptor介面
框架本身並沒有提供該介面的任何實現類,我們可以根據業務需求實現該介面,並通過CoapEndpoint.addInterceptor(MessageInterceptor interceptor)
方法新增具體的實現類。
Matcher類
我們主要看receiveRequest(Request request)
方法,看它對客戶端的GET請求做了哪些操作:
public Exchange receiveRequest(Request request) { // 根據Request請求,填充並返回Exchange物件 ... }
CoapStack類
CoapStack的類圖比較複雜,其結構可以簡化為下圖:
有人可能會疑惑,這個結構圖是怎麼來,答案就在構造方法裡:
public CoapStack(NetworkConfig config, Outbox outbox) { // 初始化棧頂 this.top = new StackTopAdapter(); // 初始化棧底 this.bottom = new StackBottomAdapter(); // 初始化出口 this.outbox = outbox; // 初始化ReliabilityLayer ... // 初始化層級 this.layers = new Layer.TopDownBuilder() .add(top) .add(new ObserveLayer(config)) .add(new BlockwiseLayer(config)) .add(reliabilityLayer) .add(bottom) .create(); }
迴歸正題,繼續看CoapStack.receiveRequest(Exchange exchange, Request request)
方法是怎麼處理客戶端的GET請求:
public void receiveRequest(Exchange exchange, Request request) { bottom.receiveRequest(exchange, request); }
CoapStack在收到請求後,交給了StackBottomAdapter去處理,StackBottomAdapter處理完後就會依次向上傳遞給ReliabilityLayer、BlockwiseLayer、ObserveLayer,最終傳遞給StackTopAdapter。中間的處理細節就不詳述了,直接看StackTopAdapter.receiveRequest(Exchange exchange, Request request)
方法:
public void receiveRequest(Exchange exchange, Request request) { // 一些非關鍵操作 ... // 將請求傳遞給訊息分發器 deliverer.deliverRequest(exchange); }
可以看到,StackTopAdapter最後會將請求傳遞給MessageDeliverer,至此CoapEndpoint的任務也就算完成了,我們可以通過一張請求訊息流程圖來回顧一下,一個客戶端GET請求最終是如何到達MessageDeliverer的:
MessageDeliverer介面
框架有ServerMessageDeliverer和ClientMessageDeliverer兩個實現類。從CoapServer的構造方法裡知道使用的是ServerMessageDeliverer類。那麼就讓我們看看ServerMessageDeliverer.deliverRequest(Exchange exchange)
方法是如何分發GET請求的:
public void deliverRequest(final Exchange exchange) { // 從exchange裡獲取request Request request = exchange.getRequest(); // 從request裡獲取請求路徑 List<String> path = request.getOptions().getUriPath(); // 找出請求路徑對應的Resource final Resource resource = findResource(path); // 一些非關鍵操作 ... // 由Resource來真正地處理請求 resource.handleRequest(exchange); // 一些非關鍵操作 ... }
當MessageDeliverer找到Request請求對應的Resource資源後,就會交由Resource資源來處理請求。(是不是很像Spring MVC中的DispatcherServlet,它也負責分發請求給對應的Controller,再由Controller自己處理請求)
Resource介面
還記得CoapServer構造方法裡建立了一個RootResource嗎?它的資源路徑為空,而客戶端發起的GET請求預設也是空路徑。那麼ServerMessageDeliverer就會把請求分發給RootResource處理。RootResource類沒有覆寫handleRequest(Exchange exchange)
方法,所以我們看看CoapResource父類的實現:
public void handleRequest(final Exchange exchange) { Code code = exchange.getRequest().getCode(); switch (code) { case GET: handleGET(new CoapExchange(exchange, this)); break; case POST: handlePOST(new CoapExchange(exchange, this)); break; case PUT: handlePUT(new CoapExchange(exchange, this)); break; case DELETE: handleDELETE(new CoapExchange(exchange, this)); break; } }
由於我們客戶端發起的是GET請求,那麼將會進入到RootResource.handleGET(CoapExchange exchange)
方法:
public void handleGET(CoapExchange exchange) { // 由CoapExchange返回響應 exchange.respond(ResponseCode.CONTENT, msg); }
再接著看CoapExchange.respond(ResponseCode code, String payload)
方法:
public void respond(ResponseCode code, String payload) { // 生成響應並賦值 Response response = new Response(code); response.setPayload(payload); response.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN); // 呼叫同名函式 respond(response); }
看看同名函式裡又做了哪些操作:
public void respond(Response response) { // 引數校驗 ... // 設定Response屬性 ... // 檢查關係 resource.checkObserveRelation(exchange, response); // 由成員變數Exchange傳送響應 exchange.sendResponse(response); }
那麼Exchange.sendResponse(Response response)
又是如何傳送響應的呢?
public void sendResponse(Response response) { // 設定Response屬性 response.setDestination(request.getSource()); response.setDestinationPort(request.getSourcePort()); setResponse(response); // 由Endpoint傳送響應 endpoint.sendResponse(this, response); }
原來最終還是交給了Endpoint去傳送響應了啊!之前的GET請求就是從Endpoint中來的。這真是和達康書記一樣,從人民中來,再到人民中去。
在CoapEndpoint類一章節中我們有介紹它的內部結構。那麼當傳送響應的時候,將與之前接收請求相反,先由StackTopAdapter處理、再是依次ObserveLayer、BlockwiseLayer、ReliabilityLayer處理,最後由StackBottomAdapter處理,中間的細節還是老樣子忽略,讓我們直接看StackBottomAdapter.sendResponse(Exchange exchange, Response response)
方法:
public void sendResponse(Exchange exchange, Response response) { outbox.sendResponse(exchange, response); }
請求入口是CoapEndpoint.InboxImpl,而響應出口是CoapEndpint.OutboxImpl,簡單明瞭。最後,讓我們看看OutboxImpl.sendResponse(Exchange exchange, Response response)
吧:
public void sendResponse(Exchange exchange, Response response) { // 一些非關鍵操作 ... // 匹配器傳送響應 matcher.sendResponse(exchange, response); // 訊息攔截器傳送響應 for (MessageInterceptor interceptor:interceptors) { interceptor.sendResponse(response); } // 真正地傳送響應到網路裡 connector.send(Serializer.serialize(response)); }
通過一張響應訊息流程圖來回顧一下,一個服務端響應最終是如何傳輸到網路裡去:
總結
通過服務端的建立和啟動,客戶端發起GET請求,服務端接收請求並返回響應流程,我們對Californium框架有了一個整體的瞭解。俗話說,師父領進門,修行看個人。在分析這個流程的過程中,我省略了很多的細節,意在讓大家對框架有個概念上的理解,在以後二次開發或定位問題時更能抓住重點,著重針對某個模組。最後,也不得不讚嘆一下這款開源框架程式碼邏輯清晰,模組職責劃分明確,靈活地使用設計模式,非常值得我們學習!
相關文章
- 阿里巴巴開源路由框架 - ARouter 分析阿里路由框架
- 阿里開源 iOS 協程開發框架 coobjc原始碼分析阿里iOS框架OBJ原始碼
- 美團外賣開源路由框架 WMRouter 原始碼分析路由框架原始碼
- Dewdrop:開源事件源框架事件框架
- 開源框架(整理)框架
- 你的第一款開源影片分析框架框架
- 【Android開源專案分析】android輕量級開源快取框架——ASimpleCache(ACache)原始碼分析Android快取框架原始碼
- phpGrace開源PHP框架PHP框架
- PHP開源AJAX框架PHP框架
- Swift開發開源框架KatanaSwift框架
- 開源JAVA單機爬蟲框架簡介,優缺點分析Java爬蟲框架
- Workerman開源框架的作者框架
- 開源RAG框架彙總框架
- PhalApi(π框架) - PHP輕量級開源介面框架API框架PHP
- 開源 POC 框架學習 (kunpeng)框架
- 分享個人開源爬蟲框架爬蟲框架
- EacooPHP框架【開源、免費、好用】OOPPHP框架
- 滴滴開源小程式框架Mpx框架
- IDEA升級開源框架Idea框架
- 開源漏洞檢測框架收集框架
- Android常用的開源框架Android框架
- Android 面試開源框架篇Android面試框架
- KIXEYE Chassis開源微服務框架微服務框架
- .NET平臺下開源框架框架
- Android的MVC開源框架AndroidMVC框架
- Jease 開源內容管理框架框架
- 常用開源框架中設計模式使用分析- 裝飾器模式(Decorator Pattern)框架設計模式
- 【Android】開源專案UniversalImageLoader及開源框架ImageLoaderAndroid框架
- Socket開發框架之框架設計及分析框架
- 前端開發必看的幾個開源框架!前端框架
- 深度學習開發必備開源框架深度學習框架
- 微前端框架chunchao(春潮)開源啦前端框架
- 國內優秀MES開源框架框架
- novaframework/nova:Erlang的開源Web框架。FrameworkWeb框架
- Java面試寶典之開源框架!Java面試框架
- 5款Java微服務開源框架Java微服務框架
- 微軟開源機器學習框架——infer.NET微軟機器學習框架
- 微軟開源機器學習框架——infer.NET微軟機器學習框架