【开源物联网】CoAP协议解析和RESTful开源实现

发布于:2022-12-23 ⋅ 阅读:(789) ⋅ 点赞:(0)

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 

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到