libcoap 接口分析与 CoAP 协议开发

libcoap 作为一个重要的 CoAP 开源实现,完整实现了 RFC 7252。很多优秀的 IoT 产品都用到了 libcoap,libcoap 为资源受限的设备(例如计算能力,射频范围,内存,带宽或网络数据包大小)实施轻量级应用程序协议,是一个非常优秀的开源项目。

网络上并没有过多关于使用 libcoap 开发的相关资料,本文以最新版 libcoap 4.2.1 为基础,从 Socket 开始,以最简单的例子(实现起来并不简单)实现一个简单的 server,达到使用 libcoap 进行开发的目的。

准备工作 链接
文章项目源码 https://github.com/liyansong2018/libcoap-demo
libcoap 库项目地址 https://github.com/obgm/libcoap
CoAP 协议分析与测试 https://blog.csdn.net/song_lee/article/details/105599391

本文还要求你对 CoAP 协议有一个基本的了解,并且已经安装了 libcoap,详情请访问:CoAP 协议分析与测试,其中也有关于 libcoap 的安装和一些常见问题。

1 libcoap

libcoap 库包含太多的数据类型和 API,这里只分析几个较为重要和常见的,更多文档请参考 libcoap 官方 API 文档

1.1 数据类型分析

coap_context_t 结构体存储的是 CoAP 栈的全局状态,可以理解为一个 CoAP 对象,这个对象包括很多 CoAP 属性,是最重要的一个结构体

typedef struct coap_context_t {
    
    
  coap_opt_filter_t known_options;
  struct coap_resource_t *resources; /**< hash table or list of known
                                          resources */
  struct coap_resource_t *unknown_resource; /**< can be used for handling
                                                 unknown resources */
  ...
}

coap_resource_t 存储 CoAP 资源类型,为了节省空间,采用位域的方式,以位为单位来指定其成员所占内存长度,如下所示,每个成员变量占据一比特,例如,我们可以根据 ->dirty 的值来判断资源是否变化

typedef struct coap_resource_t {
    
    
  unsigned int dirty:1;          /**< set to 1 if resource has changed */
  unsigned int partiallydirty:1; /**< set to 1 if some subscribers have not yet
                                  *   been notified of the last change */
  unsigned int observable:1;     /**< can be observed */
  unsigned int cacheable:1;      /**< can be cached */
  unsigned int is_unknown:1;     /**< resource created for unknown handler */
  ...
} coap_resource_t;

1.2 API

coap_make_str_const

coap_str_const_t *coap_make_str_const(const char *string);
  • 作用:将 string 类型转换为 coap_str_const_t 类型的常量字符串
  • 返回:指向 coap_str_const_t 类型的指针
  • 入参:字符串(char 类型的指针)

coap_resource_init

coap_resource_t *coap_resource_init(coap_str_const_t *uri_path,
                                    int flags);
  • 作用:创建新的资源对象,将 uri 路径初始化为字符串
  • 返回: coap_resource_t 对象
  • 入参:uri_path,新资源的 uri 字符串路径
       flags,内存管理,特别是内存释放

coap_register_handler

void coap_register_handler(coap_resource_t *resource,
                           coap_request_t method,
                           coap_method_handler_t handler);
  • 作用:将指定的资源注册为请求类型的消息处理程序
  • 返回:空
  • 入参:resource 资源
  • method:请求方法
  • handler:要向资源注册的处理程序

coap_add_resource

void coap_add_resource(coap_context_t *context, coap_resource_t *resource);
  • 作用: context 注册给定的资源。资源必须是 coap_resource_init() 或者 coap_resource_unknown_init() 创建。为该资源分配的存储空间由 coap_delete_resource() 释放
    返回:空
  • 入参:context 为 coap 全局状态结构体;resource 是新增的资源类型

coap_add_attr

coap_attr_t *coap_add_attr(coap_resource_t *resource,
                           coap_str_const_t *name,
                           coap_str_const_t *value,
                           int flags);
  • 作用:为给定的资源注册一个属性
  • 返回:指向 coap_attr_t 类型的指针
  • 入参:name 属性名
       value 属性值
       flags 内存管理
  • 备注:flag=COAP_ATTR_FLAGS_RELEASE_NAME 时,coap_add_attr_release() name 会被释放
  • flag= COAP_ATTR_FLAGS_RELEASE_VALUE 时,coap_add_attr_release() value 会被释放

coap_resource_set_get_observable

COAP_STATIC_INLINE void
coap_resource_set_get_observable(coap_resource_t *resource, int mode) {
    
    
  resource->observable = mode ? 1 : 0;
}

作用:将 resource 成员变量 observable 设置为 1 或者 0,也就是 CoAP 的观察模式

2 实际开发

终于到大家比较关心的环节了。libcoap 的使用还是稍稍复杂的,新版本又改了某些 API,使用起来与以前的版本稍稍不同,网上又没有实际案例。开发可参考 libcoap 官方 API 文档 ,但是并不是很全面,有很多用法还是要我们自己不断摸索。

这里,我们举个例子,编写一个 CoAP 服务端,当客户端访问 coap://0.0.0.0/hello,输出 Hello world。虽然案例简单,但是如果使用 libcoap 库实现,还是较为繁琐的。

2.1 创建 Socket

初始化 Socket,这里用到了 coap_address_t 类型,该结构体定义在 address.h 头文件中,从 libcoap 源码中分析

typedef struct coap_address_t {
    
    
  uint16_t port;
  ip_addr_t addr;
} coap_address_t;

注意其中的 ip_addr_t,这个类型并不 libcoap 源码中定义,也不在标准的 Unix Socket 中,也就是说内核源码中也没有该结构体的声明。笔者后来发现,该结构体是 LwIP 的一个实现

LwIP 是瑞典计算机科学院(SICS)Adam Dunkels 开发的一个小型开源的 TCP/IP 协议栈,相比于计算机中传统的 TCP 协议,可以减少对 RAM 的占用,也就适合使用在物联网设备中了。

LwIP 官方 / github 镜像 项目

http://git.savannah.gnu.org/cgit/lwip.git
https://github.com/lwip-tcpip/lwip.git

在编译 libcoap 源码的过程中,会自动下载该项目,因此这里不必考虑单独克隆 LwIP,下面是可能用到的一些 LwIP 头文件

/** 255.255.255.255 */
#define INADDR_NONE         IPADDR_NONE
/** 127.0.0.1 */
#define INADDR_LOOPBACK     IPADDR_LOOPBACK
/** 0.0.0.0 */
#define INADDR_ANY          IPADDR_ANY
/** 255.255.255.255 */
#define INADDR_BROADCAST    IPADDR_BROADCAST

代码

coap_address_t  serv_addr;
coap_address_init(&serv_addr);
//serv_addr.port 				   = 5683;
serv_addr.addr.sin.sin_family      = AF_INET;
serv_addr.addr.sin.sin_addr.s_addr = INADDR_NONE;
serv_addr.addr.sin.sin_port        = htons(5683); //default port
ctx                                = coap_new_context(&serv_addr);

coap_address_init 初始化 coap_address_t 对象,分配堆空间,存放 serv_addr 指向的结构体。这里 coap_new_context 用来创建 coap_context_t 对象。INADDR_NONE 是 LwIP 中的一个宏,(0.0.0.0),这里是要结合源码来看,才能写出更好的代码。

2.2 初始化资源

static void
hello_handler(coap_context_t *ctx, 
			  struct coap_resource_t *resource, 
              coap_session_t *session, 
              coap_pdu_t *request, 
              coap_binary_t *token, 
              coap_string_t *query, 
              coap_pdu_t *response) 
{
    
    
	unsigned char buf[3];
	const char* response_data     = "Hello World!";
	response->code           = COAP_RESPONSE_CODE(205);
	coap_add_option(response, COAP_OPTION_CONTENT_TYPE, coap_encode_var_bytes(buf, COAP_MEDIATYPE_TEXT_PLAIN), buf);
	coap_add_data  (response, strlen(response_data), (unsigned char *)response_data);
}

hello_resource = coap_resource_init(coap_make_str_const("hello"), 0);
coap_register_handler(hello_resource, COAP_REQUEST_GET, hello_handler);
coap_add_resource(ctx, hello_resource);

coap_resource_init 用于给资源提供路径,路径名为 hello,即该资源的路径为:coap://0.0.0.0/hello。coap_register_handler 将该资源与特定的请求方法,返回的处理方式绑定,决定了该资源,客户端应该使用 GET 方式获取。服务端正确的响应,交给 hello_handler 处理,该函数就是最终的 CoAP 报文处理函数

2.3 监听端口,等待连接

方法一

unsigned wait_ms = COAP_RESOURCE_CHECK_TIME * 1000;

while (1)
{
    
    
	int result = coap_run_once(ctx, wait_ms);
	if (result < 0)
	{
    
    
		break;
	}
	//coap_read(ctx, now);
}
coap_free_context(ctx); 

coap_run_once 函数会在 wait_ms 时间内,等待一个新的数据包,ctx 发生变化时,返回状态。整个 while 循环用来监听连接。

方法二:Select

select 是 Linux 系统调用,fd_set 是文件描述符集。select 函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供 select函数来实现多路复用输入/输出模型。

int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);

对 于fd_set 类型通过下面四个宏来操作:

  • FD_ZERO(fd_set *fdset) 将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
  • FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一个新的文件描述符。
  • FD_CLR(fd_set *fdset) 用于在文件描述符集合中删除一个文件描述符。
  • FD_ISSET(int fd,fd_set *fdset) 用于测试指定的文件描述符是否在该集合中。
while (1)
{
    
    
	FD_ZERO(&readfds);
	FD_SET(coap_fd, &readfds);
	int result = select( FD_SETSIZE, &readfds, 0, 0, NULL );
	if (result < 0)
	{
    
    
		exit(1);
	}
	else if (result > 0 && FD_ISSET(coap_fd, &readfds))
	{
    
    
		coap_run_once(ctx, COAP_RUN_NONBLOCK);
	}

}

这里的 coap_run_once 参数 wait_ms 有以下两个值

  • COAP_RUN_NONBLOCK(1) 如果没有更多输入数据包,则无需等待;
  • COAP_RUN_BLOCK(0)会一直等待一个新的数据包进入

2.4 编译和运行

make server
./server

运行结果
在这里插入图片描述

3 总结

libcoap 是一个优秀的 CoAP 协议开源项目,但是使用起来较为复杂,本文在这里只是介绍一些常用结构体和 API 的使用,笔者也不是专门的开发人员,只是为了更好的理解 CoAP 协议的实现,才去使用 libcoap。如果你要实现一个功能性的 CoAP server,一定要结合源码,在理解源码的基础上,才能更好的编写相关代码。

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/105653196