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