@ironzhang
2019-07-01T02:48:58.000000Z
字数 10292
阅读 366
工作/coap
本软件以golang库的方式实现了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消息用二进制格式进行编码。这个消息格式以一个固定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协议有一定相似性)。
type Request struct {
// 是否为可靠消息
Confirmable bool
// 请求方法
Method Code
// COAP选项
Options Options
// 目标url
URL *url.URL
// 消息令牌, 消息接收端使用, 发送端不应该使用该字段
Token Token
// 消息负载
Payload []byte
// 远端地址, 消息接收端使用, 发送段不应该使用该字段
RemoteAddr net.Addr
// 请求超时时间, 消息发送端使用
Timeout time.Duration
// contains filtered or unexported fields
}
Request COAP请求
type Response struct {
// 是否为应答附带响应
Ack bool
// 响应码
Status Code
// COAP选项
Options Options
// 消息令牌
Token Token
// 消息负载
Payload []byte
}
Response COAP响应
type ResponseWriter interface {
// Ack 回复空ACK, 服务器无法立即响应的情况下, 可先调用该方法返回一个空的ACK
Ack(Code)
// SetConfirmable 设置响应为可靠消息, 作为单独响应或处理非可靠消息时生效
SetConfirmable()
// Options 返回Options
Options() *Options
// WriteCode 写入响应状态码, 默认为Content
WriteCode(Code)
// Write 写入payload
Write([]byte) (int, error)
}
ResponseWriter 用于构造COAP响应
type Handler interface {
ServeCOAP(ResponseWriter, *Request)
}
Handler 响应COAP请求的接口
type Observer interface {
ServeObserve(*Response)
}
Observer 观察者接口
type Server struct {
Handler Handler // 请求响应接口
Observer Observer // 观察者接口
// contains filtered or unexported fields
}
Server 定义了运行一个COAP Server的参数
func (s *Server) CancelObserve(urlstr string) error
CancelObserve 取消订阅.
func (s *Server) ListenAndServe(address string) error
ListenAndServe 在指定地址端口监听并提供COAP服务.
func (s *Server) Observe(token Token, urlstr string, accept uint32) error
Observe 订阅.
token长度不能大于8个字节.
func (s *Server) SendRequest(req *Request) (*Response, error)
SendRequest 发送COAP请求.
func (s *Server) Serve(scheme string, l net.PacketConn) error
Serve 提供COAP服务.
func ListenAndServe(address string, h Handler, o Observer) error
ListenAndServe 在指定地址端口监听并提供COAP服务.
type Client struct {
}
Client 定义了运行一个COAP Client的参数
func (c *Client) Dial(urlstr string, handler Handler, observer Observer) (*Conn, error)
Dial 建立COAP链接
func (c *Client) SendRequest(req *Request) (*Response, error)
SendRequest 发送COAP请求
type Conn struct {
// contains filtered or unexported fields
}
Conn COAP链接
func (c *Conn) Close() error
Close 关闭COAP链接
func (c *Conn) SendRequest(req *Request) (*Response, error)
SendRequest 发送COAP请求
package main
import (
"log"
"github.com/ironzhang/coap"
)
type Handler struct {
}
func (h Handler) ServeCOAP(w coap.ResponseWriter, r *coap.Request) {
log.Println(r.URL.String())
w.Write(r.Payload)
}
func main() {
coap.Verbose = 2
if err := coap.ListenAndServe(":5683", Handler{}, nil); err != nil {
log.Fatal(err)
}
}
package main
import (
"fmt"
"log"
"github.com/ironzhang/coap"
)
func main() {
coap.Verbose = 0
var client coap.Client
req, err := coap.NewRequest(true, coap.POST, "coap://localhost/ping", []byte("ping"))
if err != nil {
log.Printf("new request: %v", err)
return
}
resp, err := client.SendRequest(req)
if err != nil {
log.Printf("send coap request: %v", err)
return
}
fmt.Printf("%s\n", resp.Payload)
}
由于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的分块传输机制。
CLIENT SERVER
| |
| CON [MID=1234], PUT, /options, 1:0/1/128 ------> |
| |
| <------ ACK [MID=1234], 2.31 Continue, 1:0/1/128 |
| |
| CON [MID=1235], PUT, /options, 1:1/1/128 ------> |
| |
| <------ ACK [MID=1235], 2.31 Continue, 1:1/1/128 |
| |
| CON [MID=1236], PUT, /options, 1:2/0/128 ------> |
| |
| <------ ACK [MID=1236], 2.04 Changed, 1:2/0/128 |
图10 Simple Atomic Block-Wise PUT
CLIENT SERVER
| |
| CON [MID=1234], GET, /status ------> |
| |
| <------ ACK [MID=1234], 2.05 Content, 2:0/1/128 |
| |
| CON [MID=1235], GET, /status, 2:1/0/128 ------> |
| |
| <------ ACK [MID=1235], 2.05 Content, 2:1/1/128 |
| |
| CON [MID=1236], GET, /status, 2:2/0/128 ------> |
| |
| <------ ACK [MID=1236], 2.05 Content, 2:2/0/128 |
图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协议中定义的新鲜度缓存模型,其模型如下:
请求放送方会根据响应的Status判断是否可缓存响应,可以缓存的响应如下:
Request匹配方式:
Response的MaxAge Option指定过期时间。
CoAP选项可按以下几个维度分类
C=Critical, U=Unsafe, N=NoCacheKey, R=Repeatable
其中C,U,N会编码在选项编号中
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| | NoCacheKey| U | C |
+---+---+---+---+---+---+---+---+
图12 option number mask(LSB)
所以通过以下代码即可分析出选项的C,U,N特性
Critical = (onum & 1);
UnSafe = (onum & 2);
NoCacheKey = ((onum & 0x1e) == 0x1c);
图13 选项编号的确定特性
而R特性无法在编号中分析得出,需要在端上定义。
根据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选项按如下方式处理
Unmarshal阶段
Process阶段
CON消息中含有未能识别的重要选项
NON消息中含有未能识别的重要选项
ACK消息中含有未能识别的重要选项
不可识别选项定义:
对选项的处理主要参考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, 默认值