# 構建基于CoAP協議的物聯網系統
## 17.1 CoAP: 嵌入式系統的REST
引自維基百科上的介紹,用的是谷歌翻譯。。。
> 受約束的應用協議(COAP)是一種軟件協議旨在以非常簡單的電子設備,使他們能夠在互聯網上進行交互式通信中使用。它特別針對小型低功率傳感器,開關,閥門和需要被控制或監督遠程,通過標準的Internet網絡類似的組件。 COAP是一個應用層協議,該協議是用于在資源受限的網絡連接設備,例如無線傳感器網絡節點使用。 COAP被設計為容易地轉換為HTTP與Web簡化集成,同時也能滿足特殊的要求,例如多播支持,非常低的開銷,和簡單性。多播,低開銷,以及簡單性是因特網極其重要物聯網(IOT)和機器對機器(M2M)設備,這往往是積重難返,有太多的內存和電源,比傳統的互聯網設備有。因此,效率是非常重要的。 COAP可以在支持UDP或UDP的模擬大多數設備上運行。
簡單地來說,CoAP是簡化了HTTP協議的RESTful API,因而也只提供了REST的四個方法,即PUT,GET,POST和DELETE。對于微小的資源受限,在資源受限的通信的IP的網絡,HTTP不是一種可行的選擇。它占用了太多的資源和太多的帶寬。而對于物聯網這種嵌入式設備來說,關于資源與帶寬,是我們需要優先考慮的內容。
* CoAP采用了二進制報頭,而不是文本報頭(text header)
* CoAP降低了頭的可用選項的數量。
* CoAP減少了一些HTTP的方法
* CoAP可以支持檢測裝置
## 17.2 CoAP 命令行工具
為了測試測試我們的代碼是否是正確工作,我們需要一個CoAP的命令行工具。目前有兩個不錯的工具可以使用。
* CoAP-cli,一個基于NodeJS的CoAP命令行工具,其核心是基于Node-CoAP庫。
* libcooap,一個用C寫的CoAP命令行工具。
* FireFox Copper, 一個Firefox的插件。
### 17.2.1 Node CoAP CLI
安裝命令如下
~~~
npm install coap-cli -g
~~~
#### 17.2.1.1 CoAP命令行
在coap-cli中,一共有四個方法。分別表示REST的四種不同的方式:
~~~
Commands:
get performs a GET request
put performs a PUT request
post performs a POST request
delete performs a DELETE request
~~~
在這里,我們用[coap://vs0.inf.ethz.ch/](coap://vs0.inf.ethz.ch/)來作一個簡單的測試
~~~
coap get coap://vs0.inf.ethz.ch/
(2.05) ************************************************************
I-D
~~~
測試一下現在的最小的物聯網系統CoAP版
~~~
coap get coap://iot-coap.phodal.com/id/1
(2.05) [{"id":1,"value":"is id 1","sensors1":19,"sensors2":20}]
~~~
### 17.2.2 libcoap
#### 17.2.2.1 mac os libcoap安裝
Mac OS下可以直接用
~~~
brew install libcoap
~~~
#### 17.2.2.2 Ubuntu libcoap安裝
Ubuntu GNU/Linux下
#### 17.2.2.3 Windows libcoap安裝
Windows 下
安裝完libcoap,我們可以直接用自帶的兩個命令
~~~
coap-client
coap-server
~~~
1.用coap-server啟一個CoAP服務
~~~
coap-server
~~~
2.客戶端獲取數據
~~~
coap-client -m get coap://localhost
~~~
返回結果
~~~
v:1 t:0 tkl:0 c:1 id:37109
This is a test server made with libcoap (see http://libcoap.sf.net)
Copyright (C) 2010--2013 Olaf Bergmann <bergmann@tzi.org>
~~~
### 17.2.3 Firefox Copper
為了能訪問[coap://localhost/](coap://localhost/),于是我們便需要安裝一個Firefox并安裝一個名為Copper的插件。
1. 下載地址:?[https://addons.mozilla.org/en-US/firefox/addon/copper-270430/](https://addons.mozilla.org/en-US/firefox/addon/copper-270430/)
2. 作為測試我們同樣可以訪問?[coap://vs0.inf.ethz.ch:5683/](coap://vs0.inf.ethz.ch:5683/)
## 17.3 CoAP Hello,World
接著我們便開始試試做一個簡單的CoAP協議的應用:
這里用到的是一個Nodejs的擴展Node-CoAP
> node-coap is a client and server library for CoAP modelled after the http module.
Node-CoAP是一個客戶端和服務端的庫用于CoAP的模塊建模。創建一個package.json文件,添加這個庫
~~~
{
"dependencies":{
"coap": "0.7.2"
}
}
~~~
接著執行
~~~
npm install
~~~
就可以安裝好這個庫。如果遇到權限問題,請用
~~~
sudo npm install
~~~
接著,創建這樣一個app.js
~~~
const coap = require('coap')
, server = coap.createServer()
server.on('request', function(req, res) {
res.end('Hello ' + req.url.split('/')[1] + '\n')
})
server.listen(function() {
console.log('server started')
})
~~~
執行
~~~
node app.js
~~~
便可以在瀏覽器上訪問了,因為現在什么也沒有,所以什么也不會返回。
接著下來再創建一個client端的js,并運行之
~~~
const coap = require('coap')
, req = coap.request('coap://localhost/World')
req.on('response', function(res) {
res.pipe(process.stdout)
})
req.end()
~~~
就可以在console上輸出
~~~
Hello World
~~~
也就達到了我們的目的,用CoAP協議創建一個服務,接著我們應該用它創建更多的東西,如產生JSON數據,以及RESTful。和HTTP版的最小物聯網系統一樣,CoAP版的最小物聯網系統也是要返回JSON的。
## 17.3 CoAP 數據庫查詢
### 17.3.1 Node Module
這說里NodeJS Module的意義是因為我們需要在別的地方引用到db_helper這個庫,也就是下一小節要的講的內容。
這樣我們就可以在server.js類似于這樣去引用這個js庫。
~~~
var DBHelper = require('./db_helper.js');
DBHelper.initDB();
~~~
而這樣調用的前提是我們需要去聲明這樣的module,為了方便地導出函數功能調用。
~~~
function DBHelper(){
}
DBHelper.initDB = function(){};
module.exports = DBHelper;
~~~
### 17.3.2 Node-Sqlite3
這次我們用的是SQLite3(你可以用MySQL,出于安全考慮用SQLite3,SQLite3產生的是一個文件)。一個簡單的initDB函數
~~~
var db = new sqlite3.Database(config["db_name"]);
var create_table = 'create table if not exists basic (' + config["db_table"] + ');';
db.serialize(function() {
db.run(create_table);
_.each(config["init_table"], function(insert_data) {
db.run(insert_data);
});
});
db.close();
~~~
首先從配置中讀取db_name,接著創建table,然后調用underscore的each方法,創建幾個數據。配置如下所示
~~~
config = {
"db_name": "iot.db",
"db_table": "id integer primary key, value text, sensors1 float, sensors2 float",
"init_table":[
"insert or replace into basic (id,value,sensors1,sensors2) VALUES (1, 'is id 1', 19, 20);",
"insert or replace into basic (id,value,sensors1,sensors2) VALUES (2, 'is id 2', 20, 21);"
],
"query_table":"select * from basic;"
};
~~~
而之前所提到的url查詢所做的事情便是
~~~
DBHelper.urlQueryData = function (url, callback) {
var db = new sqlite3.Database("iot.db");
var result = [];
console.log("SELECT * FROM basic where " + url.split('/')[1] + "=" + url.split('/')[2]);
db.all("SELECT * FROM basic where " + url.split('/')[1] + "=" + url.split('/')[2], function(err, rows) {
db.close();
callback(JSON.stringify(rows));
});
};
~~~
將URL傳進來,便解析這個參數,接著再放到數據庫中查詢,再回調回結果。這樣我們就可以構成之前所說的查詢功能,而我們所謂的post功能似乎也可以用同樣的方法加進去。
### 17.3.3 查詢數據
簡單地記錄一下在IoT-CoAP中一次獲取數據地過程。
先看看在示例中的Get.js的代碼,這關乎在后面server端的代碼。
~~~
const coap = require('coap')
,requestURI = 'coap://localhost/'
,url = require('url').parse(requestURI + 'id/1/')
,req = coap.request(url)
,bl = require('bl');
req.setHeader("Accept", "application/json");
req.on('response', function(res) {
res.pipe(bl(function(err, data) {
var json = JSON.parse(data);
console.log(json);
}));
});
req.end();
~~~
const定義數據的方法,和我們在其他語言中有點像。只是這的const主要是為了程序的健壯型,減少程序出錯,當然這不是javascript的用法。
我們構建了一個請求的URL
~~~
coap://localhost/id/1/
~~~
我們對我們的請求添加了一個Header,內容是Accept,值是'application/json'也就是JSON格式。接著,便是等待請求回來,再處理返回的內容。
**判斷請求的方法**
在這里先把一些無關的代碼刪除掉,并保證其能工作,so,下面就是簡要的邏輯代碼。
~~~
var coap = require('coap');
var server = coap.createServer({});
var request_handler = require('./request_handler.js');
server.on('request', function(req, res) {
switch(req.method){
case "GET": request_handler.getHandler(req, res);
break;
}
});
server.listen(function() {
console.log('server started');
});
~~~
創建一個CoAP服務,判斷req.method,也就是請求的方法,如果是GET的話,就調用request_handler.getHandler(req, res)。而在getHandler里,判斷了下請求的Accept
~~~
request_helper.getHandler = function(req, res) {
switch (req.headers['Accept']) {
case "application/json":
qh.returnJSON(req, res);
break;
case "application/xml":
qh.returnXML(req, res);
break;
}
};
~~~
如果是json剛調用returnJSON,
**Database與回調**
而這里為了處理回調函數剛分為了兩部分
~~~
query_helper.returnJSON = function(req, res) {
DBHelper.urlQueryData(req.url, function (result) {
QueryData.returnJSON(result, res);
});
};
~~~
而這里只是調用了
~~~
DBHelper.urlQueryData = function (url, callback) {
var db = new sqlite3.Database(config["db_name"]);
console.log("SELECT * FROM basic where " + url.split('/')[1] + "=" + url.split('/')[2]);
db.all("SELECT * FROM basic where " + url.split('/')[1] + "=" + url.split('/')[2], function(err, rows) {
db.close();
callback(JSON.stringify(rows));
});
};
~~~
這里調用了node sqlite3去查詢對應id的數據,用回調處理了數據無法到外部的問題,而上面的returnJSON則只是返回最后的結果,code以及其他的內容。
~~~
QueryData.returnJSON = function(result, res) {
if (result.length == 2) {
res.code = '4.04';
res.end(JSON.stringify({
error: "Not Found"
}));
} else {
res.code = '2.05';
res.end(result);
}
};
~~~
當resulst的結果為空時,返回一個404,因為沒有數據。這樣我們就構成了整個的鏈,再一步步返回結果。
在[IoT-CoAP](https://github.com/phodal/iot-coap)中我們使用到了一個Block2的東西,于是便整理相關的一些資料,作一個簡單的介紹,以及在代碼中的使用。
## 17.4 CoAP Block
CoAP是一個RESTful傳輸協議用于受限設備的節點和網絡。基本的CoAP消息是一個不錯的選擇對于小型載荷如
* 溫度傳感器
* 燈光開關
* 樓宇自動化設備
然而,有時我們的應用需要傳輸更大的有效載荷,如——更新固件。與HTTP,TCP做繁重工作將大型有效載荷分成多個數據包,并確保他們所有到達并以正確的順序被處理。
CoAP是同UDP與DLTS一樣是基于數據報傳輸的,這限制了資源表示(resource representation)的最大大小,使得傳輸不需要太多的分割。雖然UDP支持通過IP分片傳輸更大的有效載荷,且僅限于64KiB,更重要的是,并沒有真正很好地約束應用和網絡。
而不是依賴于IP分片,這種規范基本COAP了對“塊”選項,用于傳輸信息從多個資源區塊的請求 - 響應對。在許多重要的情況下,阻止使服務器能夠真正無狀態:服務器可以處理每塊分開傳輸,而無需建立連接以前的數據塊傳輸的其他服務器端內存。
綜上所述,塊(Block)選項提供了傳送一個最小的在分塊的方式更大的陳述。
### 17.4.1 CoAP POST
看看在IoT CoAP中的post示例。
~~~
const coap = require('coap')
,request = coap.request
,bl = require('bl')
,req = request({hostname: 'localhost',port:5683,pathname: '',method: 'POST'});
req.setOption('Block2', [new Buffer('1'),new Buffer("'must'"), new Buffer('23'), new Buffer('12')]);
req.setHeader("Accept", "application/json");
req.on('response', function(res) {
res.pipe(bl(function(err, data) {
console.log(data);
process.exit(0);
}));
});
req.end();
~~~
Block2中一共有四個數據,相應的數據結果應該是
~~~
{ name: 'Block2', value: <Buffer 31> }
{ name: 'Block2', value: <Buffer 27 6d 75 73 74 27> }
{ name: 'Block2', value: <Buffer 32 33> }
{ name: 'Block2', value: <Buffer 31 32> }
~~~
這是沒有解析的Block2,簡單地可以用
~~~
_.values(e).toString()
~~~
將結果轉換為
Block2,1 Block2,'must' Block2,23 Block2,12
接著按","分開,
~~~
_.values(e).toString().split(',')[1]
~~~
就有
~~~
[ '1', '\'must\'', '23', '12' ]
~~~
便可以很愉快地將其post到數據庫中了,
在做IoT-CoAP的過程中只支持JSON,查閱CoAP的草稿時發現支持了諸多的Content Types。
### 17.4.2 CoAP Content Types
以下文字來自谷歌翻譯:
> 互聯網媒體類型是通過HTTP字符串標識,如“application/xml”。該字符串是由一個頂層的類型“applicaion”和子類型的“XML”。為了盡量減少使用這些類型的媒體類型來表示的開銷消息有效載荷,COAP定義一個標識符編碼方案互聯網媒體類型的子集。預計這桌將可擴展標識符的值的IANA維護。內容類型選項被格式化為一個8位無符號整數。初始映射到一個合適的互聯網媒體類型標識符表所示。復合型高層次類型(multipart和不支持消息)。標識符值是從201-255保留的特定于供應商的,應用程序特定的或實驗使用和不由IANA。
下面是HTTP的標識符及類型
|
Internet media type
|
Identifier
|
| --- | --- |
|
text/plain (UTF-8)
|
0
|
|
text/xml (UTF-8)
|
1
|
|
text/csv (UTF-8)
|
2
|
|
text/html (UTF-8)
|
3
|
|
image/gif
|
21
|
|
image/jpeg
|
22
|
|
image/png
|
23
|
|
image/tiff
|
24
|
|
audio/raw
|
25
|
|
video/raw
|
26
|
|
application/link-format [I-D.ietf-core-link-format]
|
40
|
|
application/xml
|
41
|
|
application/octet-stream
|
42
|
|
application/rdf+xml
|
43
|
|
application/soap+xml
|
44
|
|
application/atom+xml
|
45
|
|
application/xmpp+xml
|
46
|
|
application/exi
|
47
|
|
application/x-bxml
|
48
|
|
application/fastinfoset
|
49
|
|
application/soap+fastinfoset
|
50
|
|
application/json
|
51
|
而在CoAP中只有簡單地幾個
|
Media type
|
Encoding
|
Id.
|
Reference
|
| --- | --- | --- | --- |
|
text/plain;
|
-
|
0
|
[RFC2046][RFC3676][RFC5147]
|
|
charset=utf-8
| | | |
|
application/
|
-
|
40
|
[RFC6690]
|
|
link-format
| | | |
|
application/xml
|
-
|
41
|
[RFC3023]
|
|
application/
|
-
|
42
|
[RFC2045][RFC2046]
|
|
octet-stream
| | | |
|
application/exi
|
-
|
47
|
[EXIMIME]
|
|
application/json
|
-
|
50
|
[RFC4627]
|
簡單地說就是:
`諸如application/json的Content Types在CoAP中應該是50`。如上表所示的結果是其對應的結果,這樣的話可以減少傳遞的信息量。
## 17.5 CoAP JSON
于是在一開始的時候首先支持的便是"application/json"這樣的類型。
首先判斷請求的header
~~~
request_helper.getHandler = function(req, res) {
switch (req.headers['Accept']) {
case "application/json":
qh.returnJSON(req, res);
break;
case "application/xml":
qh.returnXML(req, res);
break;
}
};
~~~
再轉至相應的函數處理,而判斷的依據則是Accept是不是"application/json"。
~~~
registerFormat('text/plain', 0)
registerFormat('application/link-format', 40)
registerFormat('application/xml', 41)
registerFormat('application/octet-stream', 42)
registerFormat('application/exi', 47)
registerFormat('application/json', 50)
~~~
對應地我們需要在一發出請求的時候設置好Accept,要不就沒有辦法返回我們需要的結果。
~~~
req.setHeader("Accept", "application/json");
~~~
**返回JSON**
在給IoT CoAP添加了JSON支持之后,變得非常有意思,至少我們可以獲得我們想要的結果。在上一篇中我們介紹了一些常用的工具——[CoAP 命令行工具集](http://www.phodal.com/blog/coap-command-line-tools-set/)。
**CoAP客戶端代碼**
開始之前我們需要有一個客戶端代碼,以便我們的服務端可以返回正確的數據并解析
~~~
var coap = require('coap');
var requestURI = 'coap://localhost/';
var url = require('url').parse(requestURI + 'id/1/');
console.log("Request URL: " + url.href);
var req = coap.request(url);
var bl = require('bl');
req.setHeader("Accept", "application/json");
req.on('response', function(res) {
res.pipe(bl(function(err, data) {
var json = JSON.parse(data);
console.log(json);
}));
});
req.end();
~~~
代碼有點長內容也有點多,但是核心是這句話:
~~~
req.setHeader("Accept", "application/json");
~~~
這樣的話,我們只需要在我們的服務端一判斷,
~~~
if(req.headers['Accept'] == 'application/json') {
//do something
};
~~~
這樣就可以返回數據了
**CoAP Server端代碼**
Server端的代碼比較簡單,判斷一下
~~~
if (req.headers['Accept'] == 'application/json') {
parse_url(req.url, function(result){
res.end(result);
});
res.code = '2.05';
}
~~~
請求的是否是JSON格式,再返回一個205,也就是Content,只是這時設計是請求一個URL返回對應的數據。如
~~~
coap://localhost/id/1/
~~~
這時應該請求的是ID為1的數據,即
~~~
[ { id: 1, value: 'is id 1', sensors1: 19, sensors2: 20 }]
~~~
而parse_url只是從數據庫從讀取相應的數據。
~~~
function parse_url(url ,callback) {
var db = new sqlite3.Database(config["db_name"]);
var result = [];
db.all("SELECT * FROM basic;", function(err, rows) {
callback(JSON.stringify(rows));
})
}
~~~
并且全部都顯示出來,設計得真是有點不行,不過現在已經差不多了。
## 17.6 使用IoT-CoAP構建物聯網
(`注意`:windows系統npm install失敗時,需要自己建立一個C:\Documents and Settings[USERNAME]\Application Data\npm 文件)
~~~
npm install iot-coap
~~~
1.新建**index.js**
`注意`: 如果已經存在一個index.js文件,請將下面內容添加到文件末尾(create index.js, and add)
~~~
var iotcoap = require('iot-coap');
iotcoap.run();
iotcoap.rest.run();
~~~
`注意`:在db配置可以選擇mongodb和sqlite3,替換所需要的數據庫即可。(you can choice db on iot.js with 'sqlite' or 'mongodb')
2.創建**iot.js**
~~~
exports.config = {
"db_name": "iot.db",
"mongodb_name": "iot",
"mongodb_documents": "iot",
"db": "mongodb",
"table_name": "basic",
"keys":[
"id",
"value",
"sensors1",
"sensors2"
],
"db_table": "id integer primary key, value text, sensors1 float, sensors2 float",
"mongodb_init":[
{
id: 1,
value: "is id 1",
sensors1: 19,
sensors2: 20
},
{
id: 2,
value: "is id 2",
sensors1: 20,
sensors2: 21
}
],
"init_table":[
"insert or replace into basic (id,value,sensors1,sensors2) VALUES (1, 'is id 1', 19, 20);",
"insert or replace into basic (id,value,sensors1,sensors2) VALUES (2, 'is id 2', 20, 21);"
],
"query_table":"select * from basic;",
"rest_url": "/id/:id",
"rest_post_url": "/",
"rest_port": 8848
};
~~~
3.運行(run)
~~~
node index.js
~~~
show:
~~~
coap listening at coap://0.0.0.0:5683
restify listening at http://0.0.0.0:8848
~~~