1. 程式人生 > >Californium 開源框架分析

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 = 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

<dependency>

<groupId>org.eclipse.californium</groupId>

<artifactId>californium-core</artifactId>

<version>2.0.0-M1</version>

</dependency>

其次,我們只要在Main函式裡敲兩行程式碼,服務端就啟動起來了:

1

2

3

4

5

6

7

8

public static void main(String[] args) {

// 建立服務端

CoapServer server = new CoapServer();

// 啟動服務端

server.start();

}

那麼,接下來就讓我們從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

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()又做了哪些事:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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請求,看看服務端是如何接收和處理的吧!客戶端程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

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介面,其構造方法如下:

1

2

3

4

5

6

7

8

9

10

11

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類的該方法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public void receiveData(final RawData raw) {

// 引數校驗

...

// 啟動執行緒處理收到的訊息

runInProtocolStage(new Runnable() {

@Override

public void run() {

receiveMessage(raw);

}

});

}

再往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

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請求做了哪些操作:

1

2

3

4

5

6

public Exchange receiveRequest(Request request) {

// 根據Request請求,填充並返回Exchange物件

...

}

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

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請求:

1

2

3

public void receiveRequest(Exchange exchange, Request request) {

bottom.receiveRequest(exchange, request);

}

CoapStack在收到請求後,交給了StackBottomAdapter去處理,StackBottomAdapter處理完後就會依次向上傳遞給ReliabilityLayer、BlockwiseLayer、ObserveLayer,最終傳遞給StackTopAdapter。中間的處理細節就不詳述了,直接看StackTopAdapter.receiveRequest(Exchange exchange, Request request)方法:

1

2

3

4

5

6

7

8

9

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請求的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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父類的實現:

1

2

3

4

5

6

7

8

9

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)方法:

1

2

3

4

public void handleGET(CoapExchange exchange) {

// 由CoapExchange返回響應

exchange.respond(ResponseCode.CONTENT, msg);

}

再接著看CoapExchange.respond(ResponseCode code, String payload)方法:

1

2

3

4

5

6

7

8

9

10

11

public void respond(ResponseCode code, String payload) {

// 生成響應並賦值

Response response = new Response(code);

response.setPayload(payload);

response.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN);

// 呼叫同名函式

respond(response);

}

看看同名函式裡又做了哪些操作:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public void respond(Response response) {

// 引數校驗

...

// 設定Response屬性

...

// 檢查關係

resource.checkObserveRelation(exchange, response);

// 由成員變數Exchange傳送響應

exchange.sendResponse(response);

}

那麼Exchange.sendResponse(Response response)又是如何傳送響應的呢?

1

2

3

4

5

6

7

8

9

10

11

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)方法:

1

2

3

public void sendResponse(Exchange exchange, Response response) {

outbox.sendResponse(exchange, response);

}

請求入口是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

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框架有了一個整體的瞭解。俗話說,師父領進門,修行看個人。在分析這個流程的過程中,我省略了很多的細節,意在讓大家對框架有個概念上的理解,在以後二次開發或定位問題時更能抓住重點,著重針對某個模組。最後,也不得不讚嘆一下這款開源框架程式碼邏輯清晰,模組職責劃分明確,靈活地使用設計模式,非常值得我們學習!