「知识图谱生成工具」:一键将文件夹内容变身为交互式知识图谱的免安装桌面工具(文末附免费下载链接)-CSDN博客
网络协议系列 · 第21篇 | 预计阅读时间:15分钟
💡 一句话理解CoAP
CoAP就像给HTTP减肥——HTTP是西装革履的大人,CoAP是穿T恤短裤的小孩,干的事差不多,但轻便多了。DTLS?那就是给小孩配的保镖。
一、为什么需要CoAP?
想象一下这个场景:你有一个纽扣电池供电的温度传感器,需要把数据上传到云端。如果用HTTP:
- 一个HTTP请求头动辄几百字节,而传感器数据可能只有几个字节
- TCP三次握手+TLS握手,电量还没传数据就耗光了
- 设备内存只有几十KB,HTTP库都塞不进去
这就好比让一只蚂蚁去搬大象——不是蚂蚁不努力,是任务本身就不匹配。
CoAP的设计目标(RFC 7252):
- 📡 低功耗:适合电池供电设备,一次通信几毫焦耳
- 📶 低带宽:报文最小仅4字节,比HTTP头部还小
- 🔧 受限设备:几十KB内存就能跑
- 🌐 RESTful:保持HTTP的URI、方法、响应码等概念
- 🔒 安全:基于DTLS,轻量级加密
二、CoAP报文结构解剖
CoAP报文设计得极其紧凑,核心头部只有4个字节。让我们一层层剥开这个洋葱:
2.1 固定头部(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 (Version) | 2 bits | 协议版本,当前固定为1 |
| T (Type) | 2 bits | 消息类型:CON(0), NON(1), ACK(2), RST(3) |
| TKL (Token Length) | 4 bits | Token长度,0-8字节 |
| Code | 8 bits | 请求方法或响应码 |
| Message ID | 16 bits | 消息标识,用于去重和匹配 |
2.2 Code字段详解
Code字段是CoAP的"灵魂",它同时承担了HTTP中"方法"和"状态码"的职责:
Code = c.dd (8 bits)
↑ ↑
│ └── 4位小数部分
└──── 3位类别部分
类别含义: 0.xx = 请求方法 (GET/POST/PUT/DELETE) 2.xx = 成功响应 4.xx = 客户端错误 5.xx = 服务器错误
常用Code对照表:
┌─────────┬─────────────┬─────────────────┐
│ Code │ 名称 │ 对应HTTP │
├─────────┼─────────────┼─────────────────┤
│ 0.01 │ GET │ GET │
│ 0.02 │ POST │ POST │
│ 0.03 │ PUT │ PUT │
│ 0.04 │ DELETE │ DELETE │
│ 2.01 │ Created │ 201 │
│ 2.02 │ Deleted │ 200/204 │
│ 2.04 │ Changed │ 200/204 │
│ 2.05 │ Content │ 200 │
│ 4.00 │Bad Request │ 400 │
│ 4.04 │Not Found │ 404 │
│ 5.00 │Internal Err │ 500 │
└─────────┴─────────────┴─────────────────┘
2.3 选项(Options)
CoAP用Options替代了HTTP的头部,采用TLV(Type-Length-Value)编码,支持高效压缩:
| Option | 编号 | 作用 | HTTP对应 |
|---|---|---|---|
| Uri-Host | 3 | 目标主机 | Host头 |
| Uri-Path | 11 | 资源路径 | URL路径 |
| Uri-Query | 15 | 查询参数 | Query String |
| Content-Format | 12 | 内容格式 | Content-Type |
| Max-Age | 14 | 缓存时间 | Cache-Control |
| Observe | 6 | 观察模式 | WebSocket/长轮询 |
**💡 小知识:**CoAP选项采用"差分编码",后续选项只需要编码与前一选项的编号差值,进一步压缩报文大小。
三、请求/响应模型:四种消息类型
CoAP定义了四种消息类型,对应不同的可靠性需求:
┌─────────────────────────────────────┐
│ CoAP 消息类型 │
└─────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ CON │ │ NON │ │ ACK │
│ (Confirmable)│ │(Non-Confirmable)│ │(Acknowledgement)│
│ 可靠传输 │ │ 不可靠传输 │ │ 确认消息 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 需要确认 │ 无需确认 │ 对CON的回应
│ 类似TCP │ 类似UDP │
│ 重传机制 │ 即发即忘 │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ RST │
│ (Reset) │ 典型场景:
│ 重置消息 │ - 传感器周期性上报(NON)
└─────────────┘ - 关键命令下发(CON)
│ - 心跳检测(CON)
│ - 实时通知(NON)
│
└── 拒绝处理或会话重置
3.1 CON(Confirmable)- 可靠传输
CON消息需要接收方返回ACK确认,类似于TCP的可靠传输:
Client Server
│ │
│ CON [0x7d34] GET /temperature │
│ ───────────────────────────────────────> │
│ │
│ ACK [0x7d34] 2.05 Content │
│ "22.5°C" │
│ │
│ │
│ [无ACK返回,即发即忘] │
│ │
│ 适用场景: │
│ - 周期性传感器数据上报 │
│ - 高频监控数据 │
│ - 实时性优先于可靠性 │
3.3 分离模式(Separate Response)
当服务器需要较长时间处理请求时,可以先回ACK,再单独发响应:
Client Server
│ │
│ CON [0x1234] GET /complex-query │
│ ───────────────────────────────────────> │
│ │
│ ACK [0x1234] [Empty] │
│ │ 客户端确认
四、观察者模式(Observe)
这是CoAP最酷的特性之一!它让服务器能主动推送资源变化给客户端,而不需要客户端轮询。
🎯 类比理解
传统HTTP轮询就像你每隔5分钟给餐厅打电话问"我的外卖好了吗"。 CoAP Observe就像你下单时跟餐厅说"做好了直接打我电话",省时省力。
4.1 Observe工作流程
Client Server
│ │
│ CON [0x0001] GET /temperature │
│ Observe: 0 ← 注册观察 │
│ ───────────────────────────────────────> │
│ │
│ ACK [0x0001] 2.05 Content │
│ Observe: 12 ← 当前序列号 │
│ "22.5°C" │
│ │
│ │
│ CON [0x5a40] 2.05 Content │
│ Observe: 14 │
│ "23.8°C" │
│ │
│ │
│ HelloVerifyRequest (防DoS) │
│ │
│ │
│ ServerHello, Certificate* │
│ ServerKeyExchange*, CertificateRequest* │
│ ServerHelloDone │
│ │
│ │
│ [ChangeCipherSpec], Finished │
│ │
6.2 DTLS vs TLS对比
| 特性 | TLS 1.2 | DTLS 1.2 |
|---|---|---|
| 底层协议 | TCP | UDP |
| 记录层 | 流式 | 报文式(带序列号) |
| 握手 | 3次握手 | 4次握手(加Cookie防DoS) |
| 重传机制 | TCP处理 | DTLS自己处理 |
| 报文大小 | 无限制 | 需处理UDP分片 |
**⚠️ 注意:**DTLS握手比TLS更复杂,因为UDP不保证顺序和到达。DTLS需要处理丢包、重排序、重放攻击等问题。
七、Python aiocoap实战
理论讲完了,让我们动手写代码!这里使用Python的aiocoap库,它是目前最成熟的CoAP实现之一。
7.1 安装依赖
# 安装aiocoap
pip install aiocoap
# 如果需要DTLS支持
pip install aiocoap[dtls]
7.2 简单CoAP服务器
import asyncio
import aiocoap
import aiocoap.resource as resource
from aiocoap import Message, Code
class TemperatureResource(resource.Resource):
"""温度传感器资源"""
def __init__(self):
super().__init__()
self.temperature = 22.5
async def render_get(self, request):
"""处理GET请求"""
print(f"收到GET请求: {request.opt.uri_path}")
payload = f'{{"temperature": {self.temperature}, "unit": "C"}}'.encode('utf-8')
return Message(
code=Code.CONTENT,
payload=payload,
content_format=50 # application/json
)
async def render_put(self, request):
"""处理PUT请求(更新温度)"""
try:
data = json.loads(request.payload.decode('utf-8'))
self.temperature = data.get('temperature', self.temperature)
print(f"温度更新为: {self.temperature}°C")
return Message(code=Code.CHANGED)
except Exception as e:
return Message(
code=Code.BAD_REQUEST,
payload=f'{{"error": "{str(e)}"}}'.encode()
)
class ObservableTemperatureResource(resource.ObservableResource):
"""可观察的温度资源(支持Observe)"""
def __init__(self):
super().__init__()
self.temperature = 22.5
self._task = None
async def render_get(self, request):
payload = f'{{"temperature": {self.temperature}, "unit": "C"}}'.encode('utf-8')
msg = Message(
code=Code.CONTENT,
payload=payload,
content_format=50
)
# 如果是Observe请求,设置observe选项
if request.opt.observe is not None:
msg.opt.observe = self._get_next_observe_sequence()
return msg
def _get_next_observe_sequence(self):
"""获取下一个observe序列号(实际实现需要维护计数器)"""
import time
return int(time.time()) % (1
7.3 CoAP客户端
import asyncio
from aiocoap import Context, Message, Code
async def coap_get(uri):
"""发送CoAP GET请求"""
protocol = await Context.create_client_context()
request = Message(code=Code.GET, uri=uri)
try:
response = await protocol.request(request).response
print(f"响应码: {response.code}")
print(f"响应内容: {response.payload.decode('utf-8')}")
return response
except Exception as e:
print(f"请求失败: {e}")
finally:
await protocol.shutdown()
async def coap_put(uri, payload):
"""发送CoAP PUT请求"""
protocol = await Context.create_client_context()
request = Message(
code=Code.PUT,
uri=uri,
payload=payload.encode('utf-8'),
content_format=50 # application/json
)
try:
response = await protocol.request(request).response
print(f"响应码: {response.code}")
return response
except Exception as e:
print(f"请求失败: {e}")
finally:
await protocol.shutdown()
async def coap_observe(uri, duration=30):
"""使用Observe模式订阅资源变化"""
protocol = await Context.create_client_context()
request = Message(code=Code.GET, uri=uri)
request.opt.observe = 0 # 注册观察
observation_is_over = asyncio.Future()
def observation_callback(response):
if response.code.is_successful():
observe_seq = response.opt.observe if response.opt.observe else 'N/A'
print(f"[Observe {observe_seq}] 收到更新: {response.payload.decode('utf-8')}")
else:
print(f"观察出错: {response.code}")
observation_is_over.set_result(None)
def error_callback(exception):
print(f"观察出错: {exception}")
observation_is_over.set_result(None)
observation = protocol.request(request)
observation.observation.register_callback(observation_callback)
observation.observation.register_errback(error_callback)
# 等待指定时间后取消观察
await asyncio.sleep(duration)
observation.observation.cancel()
await protocol.shutdown()
print("观察已取消")
async def discover_resources(uri="coap://localhost/.well-known/core"):
"""资源发现"""
protocol = await Context.create_client_context()
request = Message(code=Code.GET, uri=uri)
try:
response = await protocol.request(request).response
print("发现的资源:")
print(response.payload.decode('utf-8'))
except Exception as e:
print(f"发现失败: {e}")
finally:
await protocol.shutdown()
async def main():
server_uri = "coap://localhost:5683"
print("=== 1. 资源发现 ===")
await discover_resources(f"{server_uri}/.well-known/core")
print("\n=== 2. GET请求 ===")
await coap_get(f"{server_uri}/temperature")
print("\n=== 3. PUT请求 ===")
await coap_put(f"{server_uri}/temperature", '{"temperature": 25.0}')
print("\n=== 4. 再次GET验证更新 ===")
await coap_get(f"{server_uri}/temperature")
print("\n=== 5. Observe模式(观察15秒) ===")
await coap_observe(f"{server_uri}/observable-temp", duration=15)
if __name__ == '__main__':
asyncio.run(main())
7.4 运行测试
# 终端1:启动服务器
python coap_server.py
# 终端2:运行客户端测试
python coap_client.py
# 或使用coap-client命令行工具(需要安装libcoap)
coap-client -m get coap://localhost:5683/temperature
coap-client -m put -e '{"temperature":25}' coap://localhost:5683/temperature
coap-client -m get -s 15 coap://localhost:5683/observable-temp # 观察15秒
📦 源码获取
完整代码已包含在本文中,你也可以通过以下方式获取:
- GitHub Gist: 搜索 “CoAP Python Examples”
- aiocoap官方文档:
https://aiocoap.readthedocs.io/- RFC 7252: Constrained Application Protocol
🤔 思考题
- CoAP的四种消息类型(CON/NON/ACK/RST)分别适用于什么场景?能否举出具体的物联网应用例子?
- 为什么CoAP选择基于UDP而不是TCP?这种设计带来了哪些优势和挑战?
- 在Observe模式中,如果客户端网络断开后恢复,如何确保不会错过重要的状态更新?
- 对比MQTT和CoAP,它们各自适合什么样的物联网场景?能否设计一个同时支持两种协议的系统架构?
- DTLS握手比TLS多了一次往返(Cookie交换),这种设计解决了什么问题?
📚 系列文章预告
网络协议系列持续更新中,下一篇预告:
- 第22篇:《MQTT协议深度解析——物联网的消息总线》
- 第23篇:《LwM2M协议详解——设备管理的瑞士军刀》
- 第24篇:《HTTP/3与QUIC——下一代Web协议》
点击关注,第一时间获取更新通知!
CoAP 物联网 IoT 嵌入式 低功耗 RESTful 网络协议
如果觉得本文对你有帮助,欢迎点赞、收藏、转发!
有任何问题或建议,欢迎在评论区留言讨论。让我们一起探索技术的无限可能!🚀
标签: IoT, 嵌入式, 物联网, RESTful, CoAP, 网络协议, 低功耗

1556

被折叠的 条评论
为什么被折叠?



