CoAP 物联网应用层协议
+-----------------+ | Application | +-----------------+ | CoAP | +-----------------+ | UDP | +-----------------+ | IPv6 / 6LoWPAN | +-----------------+ | IEEE 802.15.4 | +-----------------+
### 报文格式
CoAP 报文最小长度仅为 **4 字节**(不包括选项),头部固定字段如下:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Ver| T | TKL | Code | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Token (if any, TKL bytes) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |1 1 1 1 1 1 1 1| Payload (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- **Ver (2 bits)**:版本号,当前为 1。
- **T (2 bits)**:消息类型(见下文)。
- **TKL (4 bits)**:Token 长度,0-8 字节。
- **Code (8 bits)**:格式为 `c.dd`,表示请求方法或响应码。
- **Message ID (16 bits)**:用于检测重复报文和匹配 ACK/RST。
- **Token**:用于匹配请求与响应(可选,长度由 TKL 指定)。
- **Options**:紧凑的二进制选项,如 Uri-Path、Content-Format 等。
- **Payload**:实际数据,前面有 1 字节分隔符 `0xFF`。
---
## CoAP 消息类型与可靠传输
CoAP 定义四种消息类型,解决 UDP 不可靠的问题:
| 类型 | 缩写 | 说明 |
|------|------|------|
| **可确认** | CON | 要求接收方回复 ACK,用于可靠传输 |
| **不可确认** | NON | 不需要确认,适合重复性读数 |
| **确认** | ACK | 对 CON 消息的确认,也可捎带响应 |
| **复位** | RST | 表示无法处理某报文(如上下文缺失) |
### 可靠传输示例(CON-ACK)
Client Server | CON [0x7a34] | | GET /temperature | |------------------>| | | | ACK [0x7a34] | | 2.05 Content | | "23.5 C" | |<------------------|
客户端发送带有 Message ID 的 CON 报文,若服务器正确接收,则回复 ACK,并可同时携带响应数据(**捎带响应**)。若客户端在一定时间内未收到 ACK,会进行指数退避重传,直到达到重传次数上限。
### 不可靠传输示例(NON)
Client Server | NON [0x23a1] | | GET /temperature | |------------------>| (无 ACK)
NON 报文不会触发重传,适用于传感器周期性上报无需每包确认的场景。
### 分离响应
当服务器无法立即给出响应时(如需要等待传感器读取),可以先回复空 ACK,稍后再用 CON 或 NON 主动返回结果,通过 Token 关联原始请求。
Client Server | CON [0x7b11] | | GET /temperature | (Token 0x12) |------------------>| | ACK [0x7b11] | (空确认,无响应) |<------------------| ... 一段时间后 ... | CON [0x8a2c] | | 2.05 Content | (Token 0x12) | "24.0 C" | |<------------------| | ACK [0x8a2c] | |------------------>|
这种机制让 CoAP 既能满足即时响应,也能适应设备唤醒调度等异步场景。
---
## 请求方法与响应码
CoAP 方法码定义在 Code 字段的 c 部分,常用的有:
- **0.01 GET** – 获取资源表示
- **0.02 POST** – 创建或处理资源
- **0.03 PUT** – 更新资源
- **0.04 DELETE** – 删除资源
另有 **FETCH**、**PATCH** 及 **iPATCH** 等扩展方法。
响应码则分为几类:
- **2.xx** 成功:如 2.05 Content(类似 HTTP 200)
- **4.xx** 客户端错误:如 4.04 Not Found
- **5.xx** 服务器错误:如 5.00 Internal Server Error
---
## 核心特性:资源发现与观察
### 资源发现
CoAP 服务器可以在路径 `/.well-known/core` 上提供资源描述(符合 RFC 6690 Link Format)。客户端发起 GET 请求即可获得服务列表:
Client: GET /.well-known/core Server: 2.05 Content </sen/temp>;rt="temperature-c";if="sensor", </sen/light>;rt="light-lux";if="sensor"
这省去了手动配置端点地址的麻烦,利于自组网。
### 资源观察(Observing Resources)
RFC 7641 定义了观察模式,允许客户端订阅某个资源的状态变化。一旦订阅,服务器会在状态更新时主动推送通知(NON 或 CON 报文):
Client Server | GET /temperature | | Observe: 0 | |------------------>| | 2.05 Content | | Observe: 12 | | "23.0" | |<------------------| ... 温度变化时 ... | 2.05 Content | | Observe: 14 | | "23.5" | |<------------------|
观察模式极大减少了轮询开销,降低能耗。
---
## CoAP 安全:基于 DTLS 的防护
CoAP 原生安全基于 **DTLS**(Datagram TLS),它针对 UDP 数据报做了适配,提供与 TLS 类似的机密性、完整性和认证功能。根据安全级别,有四种模式:
- **NoSec**:不加密,无安全保护。
- **PreSharedKey**:基于预共享密钥,适合资源极受限设备。
- **RawPublicKey**:使用原始公钥,无需证书。
- **Certificate**:使用 X.509 证书,功能最全但开销较大。
安全 URI scheme 为 `coaps://`,默认端口 5684。实现时需要权衡安全强度与设备计算能力。
---
## 实战示例:用 Python 搭建一个微型 CoAP 服务与客户端
下面用 Python 库 `aiocoap` 快速体验 CoAP。确保 Python 3.7+,安装:
```bash
pip install aiocoap
服务端(server.py)
import asyncio
import aiocoap.resource as resource
import aiocoap
class TemperatureResource(resource.Resource):
async def render_get(self, request):
payload = b"23.5"
return aiocoap.Message(payload=payload, content_format=0)
async def main():
root = resource.Site()
root.add_resource(['sen', 'temp'], TemperatureResource())
await aiocoap.Context.create_server_context(root, bind=('::', 5683))
await asyncio.get_running_loop().create_future() # 永远运行
asyncio.run(main())
客户端(client.py)
import asyncio
from aiocoap import *
async def main():
protocol = await Context.create_client_context()
request = Message(code=GET, uri='coap://localhost/sen/temp')
response = await protocol.request(request).response
print(f"Result: {response.code}, Payload: {response.payload.decode()}")
asyncio.run(main())