[关闭]
@ironzhang 2019-07-01T02:48:58.000000Z 字数 10292 阅读 366

CoAP软著-设计手册

工作/coap


软件简介

本软件以golang库的方式实现了CoAP协议及某些CoAP的扩展协议,具体实现的协议如下:

CoAP简介

由于物联网中的很多设备都是资源受限型的,即只有少量的内存空间和有限的计算能力,所以传统的HTTP协议应用在物联网上就显得过于庞大而不适用。 IETF的CoRE工作组提出了一种基于REST架构的CoAP协议。

CoAP是一种应用层协议,消息格式十分紧凑的,默认运行在UDP上,可满足受限环境的特殊需求,特别考虑了能源、楼宇自动化和其它M2M应用。

从逻辑上,可以把CoAP协议划分为两层:消息层,用于处理UDP数据包和异步;请求/响应层,处理CoAP请求并回复响应,具体见图1。

        +----------------------+
        |      Application     |
        +----------------------+
        +----------------------+  \
        |  Requests/Responses  |  |
        |----------------------|  | CoAP
        |       Messages       |  |
        +----------------------+  /
        +----------------------+
        |          UDP         |
        +----------------------+

        图1 CoAP中的抽象层次

本小节将对CoAP协议做一个简单的描述,更多的CoAP协议资料,可参考RFC7252。

CoAP特性

CoAP协议主要有如下特性:

消息格式

CoAP消息用二进制格式进行编码。这个消息格式以一个固定4个字节的头部开始。此后是一个长度在0到8字节之间的Token。Token值之后是0个或多个Type-Length-Value(TLV)格式的选项(Option)。之后到整个数据报的结尾都是payload部分,payload可以为空。

|       0       |       1       |       2       |       3       |
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T |  TKL  |      Code     |          Message ID           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Token (if any, TKL bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1|    Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                     图2 消息格式

CoAP定义了许多option。消息中的每个option都有一个option编号,option值长度,和option值。
消息中的option号(TLV格式中的T)并不是直接指定option编号的。所有的option必须按实际option编号的递增排列,某一个option和上一个option之间的option编号差值为delta;每一个TLV格式的option号都是delta值(数据包中第一个option的delta即它的option编号)。同一个编号的option再次出现时,delta的值为0。

 7   6   5   4   3   2   1   0
+---------------+---------------+
|               |               |
|  Option Delta | Option Length |   1 byte
|               |               |
+---------------+---------------+
\                               \
/         Option Delta          /   0-2 bytes
\          (extended)           \
+-------------------------------+
\                               \
/         Option Length         /   0-2 bytes
\          (extended)           \
+-------------------------------+
\                               \
/                               /
\                               \
/         Option Value          /   0 or more bytes
\                               \
/                               /
\                               \
+-------------------------------+

    图3 option格式

消息模型

CoAP的消息模型是建立在UDP端到端通信的基础上的。

CoAP的头部为固定长度的(4个字节)二进制格式,其后是紧凑的二进制格式的选项部分,然后是数据部分(payload),请求和响应都采用这种格式。每个消息都包含一个消息ID,用于检测重复提供传输可靠性。(这个消息ID是连续的,包含有16位,在默认的协议参数配置下,它允许每秒钟从一端到另一端传输大约250条消息)。

通过把消息标记为CON的,可以保障消息传输的可靠性。如图4所示,在收到一个CON消息之后,接收端会发送一个带有相同消息ID(Message ID)(在这个例子中是0x7d34)的ACK。如果在默认的超时时间之后没有收到带有相同消息ID的ACK,那么它将会被重传,如果仍然没有收到ACK,此后重传超时时间会以指数级递增。当接收端无法处理一个CON消息(也无法返回一个正常的错误响应)时,它将会回应一个RST消息,而不是ACK。

                    Client              Server
                       |                  |
                       |   CON [0x7d34]   |
                       +----------------->|
                       |                  |
                       |   ACK [0x7d34]   |
                       |<-----------------+
                       |                  |

                   图4 可靠消息传输

当消息不需要可靠传输(例如持续不断的读取一个传感器数据)时,可以发送NON的消息。如图5所示,这些消息不需要应答,但它们仍然拥有消息ID,用于检测重复(在这个例子中0x01a0)。当接收端无法处理一个NON消息时,它有可能会返回一个RST消息。

                    Client              Server
                       |                  |
                       |   NON [0x01a0]   |
                       +----------------->|
                       |                  |

                   图5 非可靠消息传输

请求/响应模型

CoAP的请求和响应的语义都包含在CoAP消息中,请求和响应的消息分别带有方法码(Method Code)和响应码(Response Code)。可选的或者是默认的请求和响应信息,例如URI和数据的媒体类型等,都做为协议中的选项部分。CoAP使用一个Token来匹配请求对应的响应。注意,这个Token和消息ID不同。

请求消息分为CON和NON两种。对于CON类型的请求,如果响应数据可以立即生成,那么对于请求消息的ACK就会同时携带响应数据。这就是附带响应,(不需要对附带响应再进行单独的应答,因为假如携带响应数据的ACK丢失,那么客户端会重传请求消息)。图6中展示了对于两个GET请求,服务端返回附带响应的例子,一个成功,一个导致了4.04(资源未找到)响应。

    Client              Server       Client              Server
       |                  |             |                  |
       |   CON [0xbc90]   |             |   CON [0xbc91]   |
       | GET /temperature |             | GET /temperature |
       |   (Token 0x71)   |             |   (Token 0x72)   |
       +----------------->|             +----------------->|
       |                  |             |                  |
       |   ACK [0xbc90]   |             |   ACK [0xbc91]   |
       |   2.05 Content   |             |  4.04 Not Found  |
       |   (Token 0x71)   |             |   (Token 0x72)   |
       |     "22.5 C"     |             |   "Not found"    |
       |<-----------------+             |<-----------------+
       |                  |             |                  |

            图6 对两个GET请求的附带响应

如果请求消息是一个CON类型的,而服务端无法立即响应,那么它就会立即发回一个空的ACK消息,以免客户端重传请求消息。当响应数据准备好了之后,服务器端就会把它组装成一个新的CON类型的消息(这需要客户端的ACK)。这种形式被称为“单独响应”,如图7所示。

                    Client              Server
                       |                  |
                       |   CON [0x7a10]   |
                       | GET /temperature |
                       |   (Token 0x73)   |
                       +----------------->|
                       |                  |
                       |   ACK [0x7a10]   |
                       |<-----------------+
                       |                  |
                       ... Time Passes  ...
                       |                  |
                       |   CON [0x23bb]   |
                       |   2.05 Content   |
                       |   (Token 0x73)   |
                       |     "22.5 C"     |
                       |<-----------------+
                       |                  |
                       |   ACK [0x23bb]   |
                       +----------------->|
                       |                  |

                   图7 GET请求和独立响应

如果一个请求是以NON类型的消息发送的,那么一般来说响应也将是一个NON类型的消息,但服务器也有可能发送一个CON类型的消息作为响应。这种交互如图8所示。

                     Client              Server
                       |                  |
                       |   NON [0x7a11]   |
                       | GET /temperature |
                       |   (Token 0x74)   |
                       +----------------->|
                       |                  |
                       |   NON [0x23bc]   |
                       |   2.05 Content   |
                       |   (Token 0x74)   |
                       |     "22.5 C"     |
                       |<-----------------+
                       |                  |

               图8 NON类型消息的请求和响应

类似HTTP,CoAP协议也使用GET, PUT, POST和DELETE方法。

服务端对于URI的支持是很简单的,因为客户端已经把URI拆分为主机、端口、路径和参数,并可以使用默认值。响应代码是HTTP状态代码的一个子集,但增加了几个CoAP特有的响应码。

接口介绍

作为一个通用的CoAP库,对外接口至关重要,在这方面我们参考了golang标准库net/http库的对外接口设计(CoAP协议同HTTP协议有一定相似性)。

基础数据结构

  1. type Request struct {
  2. // 是否为可靠消息
  3. Confirmable bool
  4. // 请求方法
  5. Method Code
  6. // COAP选项
  7. Options Options
  8. // 目标url
  9. URL *url.URL
  10. // 消息令牌, 消息接收端使用, 发送端不应该使用该字段
  11. Token Token
  12. // 消息负载
  13. Payload []byte
  14. // 远端地址, 消息接收端使用, 发送段不应该使用该字段
  15. RemoteAddr net.Addr
  16. // 请求超时时间, 消息发送端使用
  17. Timeout time.Duration
  18. // contains filtered or unexported fields
  19. }
  20. Request COAP请求
  21. type Response struct {
  22. // 是否为应答附带响应
  23. Ack bool
  24. // 响应码
  25. Status Code
  26. // COAP选项
  27. Options Options
  28. // 消息令牌
  29. Token Token
  30. // 消息负载
  31. Payload []byte
  32. }
  33. Response COAP响应
  34. type ResponseWriter interface {
  35. // Ack 回复空ACK, 服务器无法立即响应的情况下, 可先调用该方法返回一个空的ACK
  36. Ack(Code)
  37. // SetConfirmable 设置响应为可靠消息, 作为单独响应或处理非可靠消息时生效
  38. SetConfirmable()
  39. // Options 返回Options
  40. Options() *Options
  41. // WriteCode 写入响应状态码, 默认为Content
  42. WriteCode(Code)
  43. // Write 写入payload
  44. Write([]byte) (int, error)
  45. }
  46. ResponseWriter 用于构造COAP响应
  47. type Handler interface {
  48. ServeCOAP(ResponseWriter, *Request)
  49. }
  50. Handler 响应COAP请求的接口
  51. type Observer interface {
  52. ServeObserve(*Response)
  53. }
  54. Observer 观察者接口

服务端接口

  1. type Server struct {
  2. Handler Handler // 请求响应接口
  3. Observer Observer // 观察者接口
  4. // contains filtered or unexported fields
  5. }
  6. Server 定义了运行一个COAP Server的参数
  7. func (s *Server) CancelObserve(urlstr string) error
  8. CancelObserve 取消订阅.
  9. func (s *Server) ListenAndServe(address string) error
  10. ListenAndServe 在指定地址端口监听并提供COAP服务.
  11. func (s *Server) Observe(token Token, urlstr string, accept uint32) error
  12. Observe 订阅.
  13. token长度不能大于8个字节.
  14. func (s *Server) SendRequest(req *Request) (*Response, error)
  15. SendRequest 发送COAP请求.
  16. func (s *Server) Serve(scheme string, l net.PacketConn) error
  17. Serve 提供COAP服务.
  18. func ListenAndServe(address string, h Handler, o Observer) error
  19. ListenAndServe 在指定地址端口监听并提供COAP服务.

客户端接口

  1. type Client struct {
  2. }
  3. Client 定义了运行一个COAP Client的参数
  4. func (c *Client) Dial(urlstr string, handler Handler, observer Observer) (*Conn, error)
  5. Dial 建立COAP链接
  6. func (c *Client) SendRequest(req *Request) (*Response, error)
  7. SendRequest 发送COAP请求
  8. type Conn struct {
  9. // contains filtered or unexported fields
  10. }
  11. Conn COAP链接
  12. func (c *Conn) Close() error
  13. Close 关闭COAP链接
  14. func (c *Conn) SendRequest(req *Request) (*Response, error)
  15. SendRequest 发送COAP请求

服务端示例

  1. package main
  2. import (
  3. "log"
  4. "github.com/ironzhang/coap"
  5. )
  6. type Handler struct {
  7. }
  8. func (h Handler) ServeCOAP(w coap.ResponseWriter, r *coap.Request) {
  9. log.Println(r.URL.String())
  10. w.Write(r.Payload)
  11. }
  12. func main() {
  13. coap.Verbose = 2
  14. if err := coap.ListenAndServe(":5683", Handler{}, nil); err != nil {
  15. log.Fatal(err)
  16. }
  17. }

客户端示例

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/ironzhang/coap"
  6. )
  7. func main() {
  8. coap.Verbose = 0
  9. var client coap.Client
  10. req, err := coap.NewRequest(true, coap.POST, "coap://localhost/ping", []byte("ping"))
  11. if err != nil {
  12. log.Printf("new request: %v", err)
  13. return
  14. }
  15. resp, err := client.SendRequest(req)
  16. if err != nil {
  17. log.Printf("send coap request: %v", err)
  18. return
  19. }
  20. fmt.Printf("%s\n", resp.Payload)
  21. }

设计与实现

分层设计

由于CoAP协议的特性众多,我们以分层的方式实现了CoAP协议栈。在协议栈内部,下层协议的Recv接口再处理完收到的消息后,会再调用上层的Recv接口,而上层的Send接口在处理完要下发的消息后会再调用下层的Send接口。图9展示了CoAP协议栈的分层。

|----------------------------------|
| Session.Recv Session.stack.Send  |
|       ^                     |    |
|       |                     v    |
|              block2              |
|       ^                     |    |
|       |                     v    |
|              block1              |
|       ^                     |    |
|       |                     v    |
|            reliability           |
|       ^                     |    |
|       |                     v    |
|           deduplication          |
|       ^                     |    |
|       |                     v    |
| Session.stack.Recv  Session.Send |
|----------------------------------|
           图9 CoAP协议栈

分块传输机制

RFC 7959定义了CoAP传输大块数据的分块传输方案,在我们的协议栈实现中分别实现了支持block1和block2的分块传输机制。图10展示了block1的分块传输机制,图11展示了block2的分块传输机制。

  1. CLIENT SERVER
  2. | |
  3. | CON [MID=1234], PUT, /options, 1:0/1/128 ------> |
  4. | |
  5. | <------ ACK [MID=1234], 2.31 Continue, 1:0/1/128 |
  6. | |
  7. | CON [MID=1235], PUT, /options, 1:1/1/128 ------> |
  8. | |
  9. | <------ ACK [MID=1235], 2.31 Continue, 1:1/1/128 |
  10. | |
  11. | CON [MID=1236], PUT, /options, 1:2/0/128 ------> |
  12. | |
  13. | <------ ACK [MID=1236], 2.04 Changed, 1:2/0/128 |
  14. 10 Simple Atomic Block-Wise PUT
  1. CLIENT SERVER
  2. | |
  3. | CON [MID=1234], GET, /status ------> |
  4. | |
  5. | <------ ACK [MID=1234], 2.05 Content, 2:0/1/128 |
  6. | |
  7. | CON [MID=1235], GET, /status, 2:1/0/128 ------> |
  8. | |
  9. | <------ ACK [MID=1235], 2.05 Content, 2:1/1/128 |
  10. | |
  11. | CON [MID=1236], GET, /status, 2:2/0/128 ------> |
  12. | |
  13. | <------ ACK [MID=1236], 2.05 Content, 2:2/0/128 |
  14. 11: Simple Block-Wise GET

消息可靠性保障

重传由超时时间和重传计数控制。对于每一个CON类型的消息,发送端会一直维护超时时间和重传计数,直到收到对应的ACK或者RST。对于一个新的CON消息,初始的超时时间被设置为介于ACK_TIMEOUT和ACK_TIMEOUT*ACK_RANDOM_FACTOR之间,重传计数被设置为0。当超时发生,且重传计数的值小于MAX_RETRANSMIT,消息被重传,重传计数增加,超时时间变为原来的两倍。如果在超时发生的时候重传计数达到了MAX_RETRANSMIT,或者收到了一个RST消息,那么就会放弃消息的传输,由应用程序来处理这个传输失败;如果在超时之前收到了ACK,就会通知上层传输成功。

消息去重机制

在EXCHANGE_LIFETIME时间之内,当ACK消息丢失或者在第一个超时时间之前没能到达服务端,接收端可能收到多次重复的CON消息。接收端会对每一次收到的重复消息都回以相同的ACK或RST,并只处理一次该请求。

接收端在NON_LIFETIME时间内收到重复的NON消息,则会忽略掉收到的重复消息。

新鲜度缓存模型

我们的实现支持CoAP协议中定义的新鲜度缓存模型,其模型如下:

  1. 发送请求之前,在Cache中查找是否有匹配的响应,若有则直接返回Response,若没有则跳转到第3步。
  2. 发送请求,等待响应,并根据响应设置Cache。

请求放送方会根据响应的Status判断是否可缓存响应,可以缓存的响应如下:

  1. Content(2.05)
  2. 客户端错误(4.xx)
  3. 服务端错误(5.xx)

Request匹配方式:

  1. Method相同
  2. URL相同
  3. Options相同(有NoCacheKey标识的除外)

Response的MaxAge Option指定过期时间。

option处理

选项定义

CoAP选项可按以下几个维度分类

  1. C=Critical, U=Unsafe, N=NoCacheKey, R=Repeatable

其中C,U,N会编码在选项编号中

  1. 0 1 2 3 4 5 6 7
  2. +---+---+---+---+---+---+---+---+
  3. | | NoCacheKey| U | C |
  4. +---+---+---+---+---+---+---+---+
  5. 12 option number maskLSB

所以通过以下代码即可分析出选项的C,U,N特性

  1. Critical = (onum & 1);
  2. UnSafe = (onum & 2);
  3. NoCacheKey = ((onum & 0x1e) == 0x1c);
  4. 13 选项编号的确定特性

而R特性无法在编号中分析得出,需要在端上定义。

处理Critical/Elective选项

根据RFC 72525.4.1. Critical/Elective所描述的四条规则:

  • Upon reception, unrecognized options of class "elective" MUST be silently ignored.
  • Unrecognized options of class "critical" that occur in a Confirmable request MUST cause the return of a 4.02 (Bad Option) response. This response SHOULD include a diagnostic payload describing the unrecognized option(s) (see Section 5.5.2).
  • Unrecognized options of class "critical" that occur in a Confirmable response, or piggybacked in an Acknowledgement, MUST cause the response to be rejected (Section 4.2).
  • Unrecognized options of class "critical" that occur in a Non- confirmable message MUST cause the message to be rejected (Section 4.3).

对Critical/Elective选项按如下方式处理

  1. Unmarshal阶段

    Created with Raphaël 2.1.2start能否识别选项解析选项值,不可识别的选项按Opaque格式解析选项值end是否为重要选项忽略选项yesnoyesno
  2. Process阶段

    • CON消息中含有未能识别的重要选项

      Created with Raphaël 2.1.2start是否为请求return of a 4.02 (Bad Option) responseendrejected (Section 4.2), 响应一个对应的RST消息yesno
    • NON消息中含有未能识别的重要选项

      Created with Raphaël 2.1.2start是否为请求rejected (Section 4.3), 响应一个对应的RST消息endrejected (Section 4.3), 忽略yesno
    • ACK消息中含有未能识别的重要选项

      Created with Raphaël 2.1.2startrejected (Section 4.2), 忽略消息end

不可识别选项定义:

  1. 未注册的选项
  2. 长度不合法的选项
  3. 不可重复选项多次出现,当做不可识别选项处理

对选项的处理主要参考RFC7252以下几节

5.4.1, 重要选项/非重要选项Critical/Elective
5.4.3, 长度
5.4.5, 可重复选项
5.4.6, 选项编号

暂不支持
5.4.2, Proxy Unsafe or Safe-to-Forward and NoCacheKey
5.4.4, 默认值

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注