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,一定要結合源碼,在理解源碼的基礎上,才能更好的編寫相關代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章