1、概述
CoAP(Constrained Application Protocol)受限应用协议是一种物联网通信协议。顾名思义,其目标是面向资源受限物理设备通信而设计的。同时,CoAP支持RESTful(Representational State Transfer)表征性状态转移架构,实现CoAP物联网对象通过GET、POST、PUT和DELETE四种统一方式接入。本文从CoAP报文解析开始到RESTful实现提供一套基础开源框架实现奇辰Open-API。
2、CoAP开源架构
设计CoAP开源架构如下图所示:
接入CoAP物联网的对象包括各种物理设备和应用终端,比如微信小程序。相比于MQTT等其它物联网协议采用TCP作为底层协议,CoAP为了实现受限资源对象接入通常选择UDP无连接协议作为底层协议。从顶层应用调用底层协议技术实现CoAP通信是个复杂过程,本文实现的CoAP开源框架奇辰Open-API将其分成两层:
- 协议处理层:负责CoAP报文按照 RFC 7252规范进行编码和解码,调用底层基础协议进行发送和接收;
- RESTful层:在协议处理层基础上为应用层暴露统一的GET、POST、PUT和DELETE操作接口,实现物联网对象的类Web访问。
3、CoAP协议处理实现
3.1报文格式
CoAP报文格式如下:
报文分4个部分:
1)Head报文头:
报文头包含4个字节,可知一条最简单的CoAP报文即为4字节。其由4个部分构成:
- Ver:2bit,版本信息,目前为固定0x01;
- T:2bit,消息类型,包括 CON, NON, ACK, RST4种,取值如下:
CON | 0x00 |
NON | 0x01 |
ACK | 0x10 |
RST | 0x11 |
- TKL:4bit,token长度,取值范围0-8字节;
- Code:8bit,功能码/响应码。Code被分成前3位和后5位两部分,两部分构成一个小数,功能划分如下:
0.00 | 空报文 |
0.01-0.31 | 请求报文 |
1.00-1.31 | 保留 |
2.00-5.31 | 响应报文 |
6.00-7.31 | 保留 |
具体含义为:
0.01 | GET方法——用于获得某资源 |
0.02 | POST方法——用于创建某资源 |
0.03 | PUT方法——用于更新某资源 |
0.04 | DELETE方法——用于删除某资源 |
2.01 | Created |
2.02 | Deleted |
2.03 | Valid |
2.04 | Changed |
2.05 | Content。类似于HTTP 200 OK |
4.00 | Bad Request 请求错误,服务器无法处理。类似于HTTP 400 |
4.01 | Unauthorized 没有范围权限。类似于HTTP 401 |
4.02 | Bad Option 请求中包含错误选项 |
4.03 | Forbidden 服务器拒绝请求。类似于HTTP 403 |
4.04 | Not Found 服务器找不到资源。类似于HTTP 404 |
4.05 | Method Not Allowed 非法请求方法。类似于HTTP 405 |
4.06 | Not Acceptable 请求选项和服务器生成内容选项不一致。类似于HTTP 406 |
4.12 | Precondition Failed 请求参数不足。类似于HTTP 412 |
4.15 | Unsuppor Conten-Type 请求中的媒体类型不被支持。类似于HTTP 415 |
5.00 | Internal Server Error 服务器内部错误。类似于HTTP 500 |
5.01 | Not Implemented 服务器无法支持请求内容。类似于HTTP 501 |
5.02 | Bad Gateway 服务器作为网关时,收到了一个错误的响应。类似于HTTP 502 |
5.03 | Service Unavailable 服务器过载或者维护停机。类似于HTTP 503 |
5.04 | Gateway Timeout 服务器作为网关时,执行请求时发生超时错误。类似于HTTP 504 |
5.05 | Proxying Not Supported 服务器不支持代理功能 |
- Message ID:消息ID。
2)token
token用于标识消息唯一性和安全性,长度由TKL定义,可占0-8字节。
3)option:选项
可以0个或者多个,用于描述请求或者响应对应的各个属性。所有的option必须按实际option编号的递增排列,某一个option和上一个option之间的option编号差值为delta;数据包中第一个option的delta即它的option编号,同一个编号的option再次出现时,delta的值为0。每个选项格式如下:
一个option之中的各个字段的含义如下:
- Option Delta:
表示Option的增量,当前的Option的具体编号。 4-bit无符号整型。值0-12代表option delta。其它3个值作为特殊情况保留:- 当值为13:有一个8-bit无符号整型(extended)跟随在第一个字节之后,本option的实际delta是这个8-bit值加13。
- 当值为14:有一个16-bit无符号整型(网络字节序)(extended)跟随在第一个字节之后,本option的实际delta是这个16-bit值加269。
- 当值为15:为payload标识符而保留。如果这个字段被设置为值15,但这个字节不是payload标识符,那么必须当作消息格式错误来处理。
- Option Length:
表示Option Value的具体长度。4-bit无符号整数。值0-12代表这个option值的长度,单位是字节。其它3个值是特殊保留的:- 当值为13:有一个8-bit无符号整型跟随在第一个字节之后,本option的实际长度是这个8-bit值加13。
- 当值为14:一个16-bit无符号整型(网络字节序)跟随在第一个字节之后,本option的实际长度是这个16-bit值加269。
- 当值为15:保留为将来使用。如果这个字段被设置为值15,必须当作消息格式错误来处理。
- Option Value 共(option Length)个字节。
CoAP选项编号定义如下:
1 | IfMatch |
3 | UriHost |
4 | ETag |
5 | IfNoneMatch |
7 | UriPort |
8 | LocationPath |
11 | UriPath |
12 | ContentFormat |
14 | MaxAge |
15 | UriQuery |
17 | Accept |
20 | LocationQuery |
35 | ProxyUri |
39 | ProxyScheme |
60 | Sizel |
4) payload(可选)
实际携带数据内容,用“0xFF”标识内容的开始,如果没有payload标识符,那么就代表这是一个0长度的payload。如果存在payload标识符但其后跟随的是0长度的payload,那么必须当作消息格式错误处理。
3.2CoAP报文编码
前端微信小程序采用javascript开发语言实现CoAP客户端如下:
export class CoapClient {
constructor(host, port) {
this.host = host
this.port = port
this.message_count = 0
this.udp = wx.createUDPSocket()
this.udp.bind()
this.udp.onMessage(msg => {
console.log(String.fromCharCode.apply(null, new Uint8Array(msg.message)))
})
}
get(resource) {
this.message_count += 1
if(this.message_count > 65535) {
this.message_count = 0
}
var packet = new Packet(this.message_count)
let option = new Option(E_OPTION.UriPath, resource)
packet.addOption(option)
let buffer = packet.writeBuffer()
this.udp.send({
address: this.host,
port: this.port,
message: buffer.buffer
})
}
}
第2-7行进行Client初始化,设置Server端host、port,然后调用微信小程序udp接口进行UDP绑定;第14-18行对CoAP资源进行GET访问,第19-21行进行报文初始化,第22行对报文进行编码写入待发送buffer,然后调用微信小程序UDP的发送接口发送CoAP报文。
第22行的编码过程如下:
writeBuffer() {
let buffer = Buffer.allocUnsafe(this.getLength())
buffer.writeUInt8(parseInt(this.getVer() << 6) + parseInt(this.getT() << 4) + parseInt(this.getTKL()), 0)
buffer.writeUInt8(this.getCode, 1)
buffer.writeUInt16BE(this.getMessageID(), 2)
let offset = 4
this.options.forEach(element => {
element.writeBuffer(buffer, offset)
offset += element.getLength()
})
return buffer
}
第3-5行完成Head报文头部分编码,第3行进行Ver、T和TKL的编码,第4行进行Code编码,第5行进行Message ID编码。第7-10行进行option编码,如下:
writeBuffer(buffer, offset) {
if(this.getLength() > 0) {
if(this.delta < 13) {
if(this.value.length < 13) {
buffer.writeUInt8(parseInt(this.delta << 4) + parseInt(this.value.length), offset)
} else if (this.value.length < 269) {
buffer.writeUInt8(parseInt(this.delta << 4) + 13, offset)
} else {
buffer.writeUInt8(parseInt(this.delta << 4) + 14, offset)
}
} else if (this.delta < 269) {
if(this.value.length < 13) {
buffer.writeUInt8(parseInt(13 << 4) + parseInt(this.value.length), offset)
} else if (this.value.length < 269) {
buffer.writeUInt8(parseInt(13 << 4) + 13, offset)
} else {
buffer.writeUInt8(parseInt(13 << 4) + 14, offset)
}
} else {
if(this.value.length < 13) {
buffer.writeUInt8(parseInt(14 << 4) + parseInt(this.value.length), offset)
} else if (this.value.length < 269) {
buffer.writeUInt8(parseInt(14 << 4) + 13, offset)
} else {
buffer.writeUInt8(parseInt(14 << 4) + 14, offset)
}
}
offset += 1
if(this.delta >= 13 && this.delta < 269) {
buffer.writeUInt8(this.delta - 13, offset)
offset += 1
} else if (this.delta >= 269) {
buffer.writeUInt16BE(this.delta - 269, offset)
offset += 2
}
if(this.value.length >= 13 && this.value.length < 269) {
buffer.writeUInt8(this.value.length - 13, offset)
offset += 1
} else if (this.value.length >= 269) {
buffer.writeUInt16BE(this.value.length - 269, offset)
offset += 2
}
buffer.write(this.value, offset)
}
}
根据delta的值判断delta和扩展delta的编码;根据value的长度自动识别length和扩展length的编码。
3.3CoAP解码
在CoAP服务端对接收的CoAP请求报文进行解析和响应。服务端采用Java Netty框架实现如下:
public class CoapServer {
public void run(int port)throws Exception{
EventLoopGroup bossGroup=new NioEventLoopGroup();
try
{
//通过NioDatagramChannel创建Channel,并设置Socket参数支持广播
//UDP相对于TCP不需要在客户端和服务端建立实际的连接,因此不需要为连接(ChannelPipeline)设置handler
Bootstrap b=new Bootstrap();
b.group(bossGroup)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new UdpServerHandler());
b.bind(port).sync().channel().closeFuture().await();
}
catch (Exception e)
{
e.printStackTrace();
}
finally{
bossGroup.shutdownGracefully();
}
}
}
初始化服务端,在第12行指定解析Handler为UdpServerHandler对UDP报文进行解析,解析过程如下:
public class UdpServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
Packet coap_packet = new Packet();
int b1 = Byte.toUnsignedInt(packet.content().getByte(0));
coap_packet.setVar(b1 >>> 6);
coap_packet.setT((b1 % 64) >>> 4);
coap_packet.setTKL(b1 % 16);
coap_packet.setCode(Byte.toUnsignedInt(packet.content().getByte(1)));
int b3 = Byte.toUnsignedInt(packet.content().getByte(2));
int b4 = Byte.toUnsignedInt(packet.content().getByte(3));
coap_packet.setMessageID((b3 << 8) + b4);
if(coap_packet.getTKL() > 0) {
packet.content().toString(4, coap_packet.getTKL(), CharsetUtil.UTF_8);
}
int offset = 4 + coap_packet.getTKL();
while(true) {
int b = Byte.toUnsignedInt(packet.content().getByte(offset));
if(b == 0) {
break;
} else if(b < 240) {
int delta = b >> 4;
if (delta == 13) {
delta += Byte.toUnsignedInt(packet.content().getByte(offset + 1));
offset += 1;
} else if (delta == 14) {
delta += (Byte.toUnsignedInt(packet.content().getByte(offset + 1)) << 8) + Byte.toUnsignedInt(packet.content().getByte(offset + 2));
offset += 2;
}
offset += 1;
int length = b % 16;
if(length == 13) {
length += Byte.toUnsignedInt(packet.content().getByte(offset));
offset += 1;
} else if (length == 14) {
length += (Byte.toUnsignedInt(packet.content().getByte(offset)) << 8) + Byte.toUnsignedInt(packet.content().getByte(offset + 1));
offset += 2;
}
String value = packet.content().toString(offset, length, CharsetUtil.UTF_8);
Option option = new Option(delta, value);
coap_packet.addOption(option);
offset += length;
} else if(b >= 240 && b < 255 ) {
log.info("package format error!");
break;
} else {
// parse payload
break;
}
}
Set<Class<?>> classes = ClassUtil.getClasses("cn.lokei");
for (Class<?> class1 : classes) {
if(class1.getAnnotation(CoapResource.class) != null) {
Method[] methods = class1.getMethods();
for (Method method : methods) {
if(method.getAnnotation(CoapGetMapping.class) != null) {
if(method.getAnnotation(CoapGetMapping.class).value().equals(coap_packet.getOptions().get(0).getValue())) {
method.invoke(class1.getDeclaredConstructor().newInstance(), ctx, packet);
}
}
}
}
}
}
}
第6-16行对报文Head进行解析,第18-51行的循环对报文的option和payload进行解析。
3.4RESTful实现
完成CoAP报文解析后,为了实现CoAP协议的RESTful架构,采用Java的annotation注解机制进行实现。首先定义两个注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface CoapResource {
@AliasFor(annotation = Component.class)
String value() default "";
}
CoapResource用于注解CoAP资源类;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CoapGetMapping {
String value() default "";
}
CoapGetMapping用于注解CoAP的GET方法。
采用注解实现CoAP服务端的一个自定义GET访问如下:
@CoapResource
public class SensorController {
@CoapGetMapping("temperature")
public void get(ChannelHandlerContext ctx, DatagramPacket packet) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("temperature", "36");
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(
jsonObject.toJSONString(),CharsetUtil.UTF_8), packet.sender()));
}
}
第1行的CoapResource注解表示下面的SensorController是一个CoAP资源类;第4行的CoapGetMapping注解表示下面的方法在收到资源名为“template”的请求将由第5行的get函数处理,第8行将温度值返回给客户端。
这样就实现了CoAP资源请求的RESTful实现,基于此框架的开发只需要实现自定义Controller和相应的Method。
为了在底层协议处理模块识别到CoAP请求后调用RESTful相应方法进行响应,其实现逻辑在UdpServerHandler里面实现如下:
Set<Class<?>> classes = ClassUtil.getClasses("cn.lokei");
for (Class<?> class1 : classes) {
if(class1.getAnnotation(CoapResource.class) != null) {
Method[] methods = class1.getMethods();
for (Method method : methods) {
if(method.getAnnotation(CoapGetMapping.class) != null) {
if(method.getAnnotation(CoapGetMapping.class).value().equals(coap_packet.getOptions().get(0).getValue())) {
method.invoke(class1.getDeclaredConstructor().newInstance(), ctx, packet);
}
}
}
}
}
采用Java的机制在当前应用包里搜寻被CoapResource注解的类如第3行所示,然后在类里面查找被CoapGetMapping注解的方法,通过匹配请求的资源名称调用具体的方法。
4、更多
开源项目:Open-Api
更多信息:www.lokei.cn