Californium开源框架之源码分析(二)

coap模块,协议中的常量定义与消息模型。

Posted by Wudashan on June 1, 2017

项目源码地址:https://github.com/eclipse/californium

coap包

coap包目录下,主要是CoAP协议中定义的常量和消息基本模型。

CoAP类

该类定义了CoAP报文的一些常量,以及按协议要求定义了报文中各字段的格式。包括:

  • 消息类型:CON,NON,ACK,RST
  • 请求码:GET,POST,PUT,DELETE
  • 响应码:成功2.XX,客户端错误4.XX,服务端错误5.XX

OptionNumberRegistry类

根据RFC 7252的第12.2章节,该类定义了CoAP报文中Option字段支持的值。其值如下表格:

+--------+------------------+-----------+ 
| Number | Name             | Reference | 
+--------+------------------+-----------+ 
|      0 | (Reserved)       | [RFC7252] | 
|      1 | If-Match         | [RFC7252] |
|      3 | Uri-Host         | [RFC7252] | 
|      4 | ETag             | [RFC7252] |  
|      5 | If-None-Match    | [RFC7252] |  
|      6 | Observe          |           |  
|      7 | Uri-Port         | [RFC7252] |  
|      8 | Location-Path    | [RFC7252] | 
|     11 | Uri-Path         | [RFC7252] |
|     12 | Content-Format   | [RFC7252] |
|     14 | Max-Age          | [RFC7252] | 
|     15 | Uri-Query        | [RFC7252] |  
|     17 | Accept           | [RFC7252] |   
|     20 | Location-Query   | [RFC7252] |  
|     23 | Block2           |           |    
|     27 | Block1           |           | 
|     28 | Size2            |           | 
|     35 | Proxy-Uri        | [RFC7252] |  
|     39 | Proxy-Scheme     | [RFC7252] | 
|     60 | Size1            | [RFC7252] |
|    128 | (Reserved)       | [RFC7252] | 
|    132 | (Reserved)       | [RFC7252] |  
|    136 | (Reserved)       | [RFC7252] |   
|    140 | (Reserved)       | [RFC7252] |    
+--------+------------------+-----------+ 

MediaTypeRegistry类

根据RFC 7252的第12.3章节,该类定义了CoAP报文中Option字段的Content-Format选项支持的值。其值如下表格:

+--------------------------+----------+----+----------------------+
| Media type               | Encoding | ID | Reference            |
+--------------------------+----------+----+----------------------+
| text/plain;              | -        |  0 | [RFC2046] [RFC3676]  |
| charset=utf-8            |          |    | [RFC5147]            |
| application/link-format  | -        | 40 | [RFC6690]            |
| application/xml          | -        | 41 | [RFC3023]            |
| application/octet-stream | -        | 42 | [RFC2045] [RFC2046]  |
| application/exi          | -        | 47 | [REC-exi-20140211]   |
| application/json         | -        | 50 | [RFC7159]            |
+--------------------------+----------+----+----------------------+

MessageFormatException类

该类继承RuntimeException类,即属于运行时(非受检)异常。当解析二进制形式的CoAP报文失败时,程序就会抛出该异常。

BlockOption类

该类表示的是CoAP报文中Option字段的Block1和Block2值,由于这两个选项比较特殊,所以单独封装成一个类。

Option类

该类是个Option字段的通用表示类。请求报文和响应报文都有可能携带多个Option,可以通过Option的序号判断该Option是必须还是可选,安全还是非安全。如果某个Option选项是安全的,那么还可以判断它是否是可缓存的。算法如下:

Critical = (OptionNumber & 1);
UnSafe = (OptionNumber & 2);
NoCacheKey = ((OptionNumber & 0x1e) == 0x1c)

说起来非常抽象,让我们直接看一个实例,Option序号二进制表示形式如下:

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
|           | NoCacheKey| U | C |
+---+---+---+---+---+---+---+---+

从OptionNumberRegistry类查询到Uri-Host的序号为3,转换为二进制就是000 000 1 1

  • Critical = (00000011 & 00000001) = 1,所以Uri-Host是必选的;
  • UnSafe = (00000011 & 00000010) != 0,所以Uri-Host是不安全的;
  • NoCacheKey = (00000011 & 00011110) != 00011100,所以Uri-Host不是非缓存键。

OptionSet类

该类是一个POJO类,其含义为CoAP协议里消息体的Option集合,我们从它的成员变量就可以看出来:

public class OptionSet {
    private List<byte[]> if_match_list;
    private String       uri_host;
    private List<byte[]> etag_list;
    private boolean      if_none_match; // true if option is set
    private Integer      uri_port; // null if no port is explicitly defined
    private List<String> location_path_list;
    private List<String> uri_path_list;
    private Integer      content_format;
    private Long         max_age; // (0-4 bytes)
    private List<String> uri_query_list;
    private Integer      accept;
    private List<String> location_query_list;
    private String       proxy_uri;
    private String       proxy_scheme;
    private BlockOption  block1;
    private BlockOption  block2;
    private Integer      size1;
    private Integer      size2;
    private Integer      observe;
    private List<Option> others; // Arbitrary options
    ...
}

它涵盖了RFC 7252中定义的所有Option字段,包括上面提到的通用Option类和单独BlockOption类,也都统一的封装在该类中。

Message类

终于到我们coap包中最重要的一个类了,该类是所有CoAP消息的基类。CoAP消息一共有三个子类:请求消息Request类、响应消息Response类、空消息EmptyMessage类。每个CoAP消息都包括消息类型Type、MID、token、OptionSet和payload等等。

除此之外,一个消息可以被应答、拒绝、取消、超时,所以在该类也定义了相应的成员变量来表示:

public abstract class Message {
    // 表示消息是否已应答
    private boolean acknowledged;

    // 表示消息是否被拒绝
    private boolean rejected;

    // 表示消息是否被取消
    private boolean canceled;

    // 表示消息是否已超时
    private boolean timedOut; // Important for CONs
    ...
}

该类使用了观察者设计模式。通过Message.addMessageObserver()加入观察者,当消息状态(应答、拒绝、取消、超时)出现变更时,会通知相应的MessageObserver类。从具体的内部实现代码即可发现:

public void setCanceled(boolean canceled) {
    this.canceled = canceled;
    if (canceled) {
        // 逐个通知观察者
        for (MessageObserver handler : getMessageObservers()) {
            // 回调观察者的相应方法
            handler.onCancel();
        }
    }
}

Request类

该类继承自Message类,表示一个CoAP请求消息。其消息类型Type为CON或者NON,请求码Code为GET、PUT、POST、DELETE的其中一种。一个请求消息需要通过Endpoint类来发送到它的目的地去。若不指定Endpoint,则框架会通过EndpointManager来生成一个默认的Endpoint。通常由服务端回复一个Response类,即一个对应的CoAP响应消息。客户端可以发起一个同步请求:

// 创建一个GET请求并发送
Request request = new Request(Code.GET);
request.setURI("coap://example.com:5683/sensors/temperature");
request.send();
// 同步等待服务器响应
Response response = request.waitForResponse();

当然,有同步请求就有异步请求,通过添加观察者到Request类中,当请求消息收到响应时回调观察者:

// 创建一个GET请求
Request request = new Request(Code.GET);
request.setURI("coap://example.com:5683/sensors/temperature");
// 添加观察者
request.addMessageObserver(new MessageObserverAdapter() {
    public void responded(Response response) {
        // 收到响应时回调该方法
        if (response.getCode() == ResponseCode.CONTENT) {
            System.out.println("Received :" + response.getPayloadString());
        } else {
            // 错误处理
        }
    }
});
// 发送请求
request.send();

我们还可以自定义Request类中Option的值,例如:

// 创建一个POST请求
Request post = new Request(Code.POST);
post.setPayload("Plain text");
// 自定义Option的值
post.getOptions()
    .setContentFormat(MediaTypeRegistry.TEXT_PLAIN)
    .setAccept(MediaTypeRegistry.TEXT_PLAIN)
    .setIfNoneMatch(true);
// 发起同步请求
String response = post.send().waitForResponse().getPayloadString();

Response类

该类继承自Message类,表示一个CoAP响应消息。一个响应消息可以是ACK、CON或NON三种类型。响应体里包含着响应码,这与HTTP响应码类似。该类还提供了一个静态工厂方法来生成Response类:

public static Response createResponse(Request request, ResponseCode code) {
    Response response = new Response(code);
    response.setDestination(request.getSource());
    response.setDestinationPort(request.getSourcePort());
    return response;
}

从静态工厂方法我们看到,只设置了响应消息的响应码、目的地地址和端口号,而Type、MID和token都没有进行赋值。这是因为,Type和MID通常会在ReliabilityLayer类里自动设置,token则通常在Matcher类里自动设置。

EmptyMessage类

该类继承自Message类,表示一个CoAP空消息。一个空消息只能是ACK或者RST两种类型。所以该类也只提供了两个静态工厂方法:EmptyMessage.newACK()EmptyMessage.newRST()

MessageObserver接口

该接口是一个观察者,当事件发生变化时调用相应的方法:

  • onResponse() 当收到响应时回调
  • onAcknowledgement() 当收到应答时回调
  • onReject() 当请求消息被拒绝时回调
  • onTimeout() 当客户端停止重传且仍然没有从对端收到任何消息时回调
  • onCancel() 当消息被取消时回调

正如我们前面在Message类所提到的,可以通过Message.addMessageObserver()方法来注册观察者。需要注意的是,CoAP协议支持客户端对服务端的资源Resource进行订阅(当资源发生变化时服务端主动发送响应消息给客户端),这种订阅是通过NotificationListener接口进行监听的,大家不要和这个类搞混了。

MessageObserverAdapter类

该类是一个抽象类,实现了MessageObserver接口。看这个类的名字,还以为使用到了适配器设计模式,仔细看源代码才发现,该类只是将MessageObserver接口中的所有方法都提供空实现:

public abstract class MessageObserverAdapter implements MessageObserver {
    @Override
    public void onResponse(Response response) {
        // 什么也不做
    }
    @Override
    public void onTimeout() {
        // 什么也不做
    }
    ...
}

大家可能会比较疑惑框架为什么要提供一个这样看上去无意义的抽象类,其实是因为如果开发者在编写自己的MessageObserver实现类时,可能只关注onResponse()和onTimeout()方法,那么为了减少其他不必要的代码,可以直接继承MessageObserverAdapter抽象类,然后按需覆盖这两个方法。


系列文章

Californium开源框架之源码分析(一)—— 整体认识

Californium开源框架之源码分析(二)—— coap包 <– 当前位置

Californium开源框架之源码分析(三)—— observe包

Californium开源框架之源码分析(四)—— server包

Californium开源框架之源码分析(五)—— network包(上)

Californium开源框架之源码分析(六)—— network包(下)

Californium开源框架之源码分析(七)—— core包

Californium开源框架之源码分析(八)—— element包