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

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种,取值如下:
CON0x00
NON0x01
ACK0x10
RST0x11
  • 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.01GET方法——用于获得某资源
0.02POST方法——用于创建某资源
0.03PUT方法——用于更新某资源
0.04DELETE方法——用于删除某资源
2.01Created
2.02Deleted
2.03Valid
2.04Changed
2.05Content。类似于HTTP 200 OK
4.00Bad Request 请求错误,服务器无法处理。类似于HTTP 400
4.01Unauthorized 没有范围权限。类似于HTTP 401
4.02Bad Option 请求中包含错误选项
4.03Forbidden 服务器拒绝请求。类似于HTTP 403
4.04Not Found 服务器找不到资源。类似于HTTP 404
4.05Method Not Allowed 非法请求方法。类似于HTTP 405
4.06Not Acceptable 请求选项和服务器生成内容选项不一致。类似于HTTP 406
4.12Precondition Failed 请求参数不足。类似于HTTP 412
4.15Unsuppor Conten-Type 请求中的媒体类型不被支持。类似于HTTP 415
5.00Internal Server Error 服务器内部错误。类似于HTTP 500
5.01Not Implemented 服务器无法支持请求内容。类似于HTTP 501
5.02Bad Gateway 服务器作为网关时,收到了一个错误的响应。类似于HTTP 502
5.03Service Unavailable 服务器过载或者维护停机。类似于HTTP 503
5.04Gateway Timeout 服务器作为网关时,执行请求时发生超时错误。类似于HTTP 504
5.05Proxying 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 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要基于Spring Boot搭建物联网平台并实现COAP协议接入,你需要遵循以下步骤: 1. 首先需要了解COAP协议COAP是Constrained Application Protocol(受限应用协议)的缩写,它是一种轻量级的Web传输协议,专门用于连接受限环境下的设备。COAP基于UDP协议,支持多播和组播,具有低延迟和低能耗等特点。在Spring Boot中可以使用Eclipse Californium库来实现COAP协议的接入。 2. 在Spring Boot项目中引入Eclipse Californium库,可以通过Maven或Gradle来引入,具体方法可以参考Eclipse Californium的官方文档。 3. 实现COAP协议的服务端,可以在Spring Boot项目中创建一个COAP服务端类,并添加COAP资源。在COAP资源中定义资源路径、请求方法和响应内容等信息。COAP服务端类需要继承Californium的CoapServer类。 4. 实现COAP协议的客户端,可以通过Eclipse Californium提供的CoapClient类来实现。在Spring Boot项目中创建一个COAP客户端类,通过CoapClient类发送COAP请求,获取响应信息。 5. 在物联网平台中使用COAP协议进行设备接入,可以通过将设备与COAP服务端进行绑定,实现设备信息的采集和控制。在平台中定义COAP资源路径和请求方法,实现设备信息的获取和控制。 综上所述,基于Spring Boot搭建物联网平台并实现COAP协议接入需要掌握COAP协议的基本知识,熟悉Eclipse Californium库的使用方法,并具备Java编程能力。 ### 回答2: 物联网平台是一种用于连接和管理物联网设备的软件平台,它允许设备之间相互通信,并与云端应用进行数据交互和控制操作。基于Spring Boot搭建的物联网平台可以实现COAP协议的接入。 COAP(Constrained Application Protocol)是一种轻量级的应用层协议,专为物联网设备设计。它具有低开销、低带宽和低功耗的特点,适用于资源受限的设备和网络环境。COAP协议可以通过UDP和DTLS(Datagram Transport Layer Security)进行数据传输。 为了在Spring Boot中实现COAP协议接入,可以使用Eclipse Californium项目作为COAP协议实现库。该项目提供了COAP协议的Java实现,可以方便地嵌入到Spring Boot应用中。 首先,在Spring Boot项目的依赖管理文件(例如pom.xml)中添加Eclipse Californium库的依赖。然后,在Spring Boot的配置文件中设置COAP服务器的监听端口和相关参数。 在Spring Boot中编写COAP的处理器类,用于处理COAP请求和响应。可以定义不同的URI来映射到不同的处理器方法,根据具体需求进行业务逻辑处理和数据交互。处理器方法可以使用COAP的API来处理COAP消息,例如解析请求、发送响应等。 另外,在物联网平台中,还可以与数据库进行交互,将设备数据进行持久化存储和查询。在Spring Boot中,可以使用Spring Data库来简化数据库访问的操作。可以定义实体类来表示设备数据,使用Spring Data提供的注解和API来进行数据库的操作。 通过以上步骤,基于Spring Boot搭建的物联网平台就可以实现COAP协议的接入。该平台可以接收来自物联网设备的COAP请求,处理请求并返回相应的COAP响应。同时,可以将设备数据存储到数据库中,并提供API接口供云端应用访问和控制。这样,可以实现物联网设备的远程监控和管理。 ### 回答3: 基于Spring Boot搭建的物联网平台可以实现COAP(Constrained Application Protocol)协议的接入,以下是该过程的简要解释。 首先,Spring Boot是一个开源的Java框架,用于快速构建基于Java的应用程序。它提供了一种简单易用的方式搭建RESTful风格的Web服务,并且具有良好的扩展性和模块化的特性,非常适合用于构建物联网平台。 COAP是一种专为物联网设备设计的应用层协议,它基于HTTP协议,但比HTTP更适合于资源受限的设备。COAP协议可以实现对设备的低功耗连接、高效的数据传输和灵活的资源管理。 在基于Spring Boot搭建的物联网平台中,要实现COAP协议的接入,首先需要引入COAP协议相关的依赖。这可以通过在项目的pom.xml文件中添加COAP协议的Java实现库,如Eclipse Californium,来实现。 接下来,可以创建COAP服务器端的资源。在Spring Boot中,可以使用@Controller和@RequestMapping注解来定义COAP资源的访问路径和处理方法。通过处理方法,可以实现对设备的读取、修改、删除等操作。 另外,在COAP协议中,通信的双方都有一个COAP客户端和COAP服务器的身份。因此,物联网平台也需要实现COAP客户端,用于与COAP服务器进行通信。可以使用RestTemplate类或者其他COAP客户端工具来发送COAP请求和接收COAP响应。 最后,基于Spring Boot搭建的物联网平台可以实现COAP协议的接入,通过COAP服务器和客户端的交互,实现物联网设备的管理和控制。此外,Spring Boot还提供了丰富的特性和扩展性,可以方便地与其他模块进行集成,为物联网平台的开发提供更多的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值