目录
一、引言
1.1 什么是 CoAP 协议
1.2 为什么需要 CoAP 协议?
二、CoAP 消息格式
2.1 CoAP 报文头
2.2 Token(令牌)、Option(选项) 与 Payload(负载)
三、CoAP 的消息类型
3.1 重传机制 与 Message ID
3.2 拥塞控制
3.3 多播与闲暇控制
四、请求/响应 模型
4.1 请求方法
4.2 响应
4.3 请求/响应码
4.4 工作流程
五、Option —— 选项
5.1 选项格式
5.2 选项定义
5.3 Uri-Host,Uri-Port,Uri-Path,Uri-Query
5.4 Proxy-Uri and Proxy-Scheme
5.5 Content-Format
5.6 Accept
5.7 Max-Age
5.8 ETag
5.9 Location-Path and Location-Query
5.10 If-Match and If-None-Match
5.11 Size1
六、CoAP 的扩展机制
6.1 观察者模式
6.2 块传输
6.2.1 块选项
6.2.2 拆分块选项
6.2.3 Size2 选项
一、引言
在计算机网络里,协议(Protocol)就是规矩。就像我们写信有信纸格式、发邮件有标题和正文一样,两个设备要交换数据,必须遵守同一个规矩,否则就是对牛弹琴。
CoAP这个规矩,是国际互联网工程任务组(IETF)在2014年正式发布的国际标准官方文档链接:RFC 7252 - The Constrained Application Protocol (CoAP)。 RFC 7252 是编号,这不是某个公司的私有协议,而是全世界工程师公认的、免费公开的通用准则。
1.1 什么是 CoAP 协议
CoAP,全称是Constrained Application Protocol,中文翻译为“受限应用协议”,是专门为物联网(IoT)设备设计的轻量级应用层协议,基于UDP协议开发。
别因为 “受限” 这两个字觉得 CoAP协议很弱,在计算机世界里,“受限”通常不是贬义词,而是代表“精打细算”。如果用一句话给你最直白的定义,那就是:CoAP是一种专门为 “小破烂儿” 电子设备设计的、让它们能像大电脑一样上网聊天的通信规则。
CoAP其实是刻意仿照 HTTP“描”出来的,比如 CoAP 也用 GET 拿数据、用 POST 提交数据、用 PUT 改数据、用 DELETE 删数据。所以有很多人把 CoAP 称为“物联网的 HTTP”。
1.2 为什么需要 CoAP 协议?
一个联网的设备,MCU主频只有几十MHz,内存只有几十KB,用一颗 纽扣电池供电,还让它工作3年。那如何满足这种低功耗、低算力、低带宽的需求呢?
再假设设备要发送一条“温度 26℃”的数据,这条数据本身可能只有 5 个字节。TCP 建立连接和断开连接,就要进行三次握手四次挥手,同时 TCP 头部最少占用 20字节。再使用一些应用层协议,这些额外数据消耗的电量,是传输真实数据的几十倍。
CoAP协议就是为了解决这些问题,CoAP 抛弃了TCP,使用UDP协议作为底座。UDP 是无连接的;UDP 头部固定8字节;CoAP协议头部仅占用 4字节;CoAP 使用请求/响应模式弥补 UDP 不可靠问题。
二、CoAP 消息格式
CoAP消息用二进制格式编码,消息格式以固定大小的4字节头开始,后面是一个可变长度的标记值,长度可以在0到8字节之间。
CoAP 消息格式(重点):
2.1 CoAP 报文头
前面反复提到 CoAP 头部只有 4字节,这 4字节 把所有关键信息浓缩在一起,现在讲讲头部字段的定义:
| 字段名 | 位(bit) | 简要说明 |
| Ver (版本号) | 第 0-1 位 | 固定为01,表示 CoAP 版本 1 |
| T (消息类型) | 第 2-3 位 | 标明本条消息是 CON、NON、ACK 还是 RST |
| TKL (令牌长度) | 第 4-7 位 | 说明后面 “Token” 字段占几个字节(0~8) |
| Code (方法/响应码) | 第 8-15 位 | 相当于 HTTP 的请求方法(GET/POST)或响应状态码(2.xx/4.xx) |
| Message ID (消息ID) | 第 16-31 位 | 每条消息的唯一序列号,用于匹配请求和响应、以及处理重传 |
2.2 Token(令牌)、Option(选项) 与 Payload(负载)
头部的4个字节后面,还跟着Token(令牌)、Option(选项) 与 Payload(负载)这几个“附加组件”,这几个是跟着头部依次排列的。
| Token(令牌) | 可以把它理解为“本次对话的ID”。比如手机发个“把灯打开”的请求,ID是 它的长度由TKL决定。TKL=4,后面就取4个字节当Token;TKL=0,就没有这个字段。 |
| Option(选项) | 类似HTTP的头部,用来携带各种对请求或响应的“补充说明”和“元数据” 它是一串可变长的字段。 |
| Payload(负载) | 负载就是真正要传递的数据。 0xFF只是有效负载的前缀标记,该标记指示选项的结束和有效负载的开始。 只有那些需要带返回数据的操作(比如GET成功的响应),才有Payload,很多简单的ACK确认消息连 |
这里的 Token 跟 报文头的 Message ID 虽然都是担任 “匹配任务”,但是他们是完全不同的两个字段:
| Message ID | 链路层/传输层级别的配对,用于 "重传确认" 和 "网络去重" |
| Token | 应用层级别的配对,用于 "多请求并发匹配" 和 "观察者订阅标识" |
三、CoAP 的消息类型
消息类型就是报文头中的字段T:
| 类型 | 描述 | T 值 |
|---|---|---|
| CON 报文 | Confirmable,需要被确认的报文 | T=00 |
| NON 报文 | Non-Confirmable,不需要被确认的报文 | T=01 |
| ACK 报文 | Acknowledgement,应答报文,用于响应接受到的CON消息 | T=10 |
| RST 报文 | Reset,复位报文,当接收者收到的消息出错时会发送RST | T=11 |
3.1 重传机制 与 Message ID
CON(Confirmable) 消息是需要对方确认的“挂号信”,但是 UDP 又不可靠,于是 CoAP 设计了重传机制,而 Message ID 是贯穿重传全过程的“定海神针”。
Message ID 在重传中的两个核心作用:
| 应答配对 | 客户端发送 CON 消息时设定一个唯一的 Message ID(比如 MID=100)。服务器处理的 ACK 响应中必须包含相同的 MID。客户端只有收到这个相同 MID 的 ACK,才会停止重传计时器。 |
| 去重过滤 | 如果客户端重传了多次 CON 包,MID 都是 100。服务器收到第 1 个包后,看到第 2 个和第 3 个来自相同 IP/端口、且 MID 相同的包时,会直接丢弃,确保应用层只被触发一次,防止重复执行指令。 |
CoAP 重传不是无脑死循环的,该机制规定了一组参数:
| 参数名 | 默认值 | 作用 |
| ACK_TIMEOUT | 2 秒 | 发出一条 CON 后,等 2 秒没回音就重传。 |
| ACK_RANDOM_FACTOR | 1.5 | 每次重传的间隔不是固定翻倍,而是乘以 1.0~1.5 的随机数,防止大量设备同时重传造成"网络风暴"。 |
| MAX_RETRANSMIT | 4 次 | 一条 CON 最多重传 4 次,再收不到就放弃。 |
CoAP 的重传间隔算法是:下次超时时间 = 当前超时时间 × 2 × [1.0 ~ 1.5 的随机数]。
综上所述,只有收到 "相同 Message ID 的 ACK报文" 或 "相同 Message ID 的 RST报文" 后会停止重传
3.2 拥塞控制
如果说重传机制解决的是"单个消息丢了怎么办"的问题,那么拥塞控制解决的是"大家都别发太快,否则网络会堵死"的问题。
一个客户端,对于同一个服务器,在任何时刻,只能有一个尚未收到确认(ACK)的CON消息。
| 参数名 | 默认值 | 作用 |
| NSTART | 1 | 同一时刻,对同一台服务器只能有 1 个 CON 在等待 ACK,防止把自己撑死。 |
| PROBING_RATE | 1 byte/second | 在发送非确认消息(NON)或发起新通信时,以每秒 1 字节的极低速率起步,先"探测"网络是否通畅,确认没问题后再逐步提速。 |
3.3 多播与闲暇控制
在 3.1 和 3.2 中,我们讨论的重传机制和拥塞控制,主要解决的都是单播场景下的问题 —— 也就是“一个客户端对一台服务器”的通信。
但在物联网中,还有一种常见的通信模式:一个设备想要同时询问多个设备。这种“一对多”的通信方式叫做多播或组播。
多播非常高效 —— 网关只发了一条消息,所有传感器都能收到。但问题出在响应环节,如果100个传感器同时回复网关,网关的接收缓冲区会被瞬间塞满,大量响应包会在网络中碰撞、丢失,最终网关一条有效数据都收不到。这就是响应风暴(Response Storm)。为了解决这个“响应风暴”问题,CoAP引入了一个专门的参数 —— DEFAULT_LEISURE。
| 参数名 | 默认值 | 作用 |
| DEFAULT_LEISURE | 5 seconds | 在响应多播请求时,每个设备必须随机等待 0 到 5 秒之间的一个时间,然后再发送响应。 |
四、请求/响应 模型
4.1 请求方法
CoAP完美复刻了HTTP最核心的四个动作:
| GET | 获取资源。用于从服务器读取资源,是安全的,不会对服务器状态产生任何影响 |
| POST | 创建新资源或更新现有资源。用于向服务器提交数据,既不安全也不幂等。 |
| PUT | 更新或创建指定资源。如果资源不存在,服务器也可以根据请求创建一个新资源 |
| DELETE | 删除资源。 |
4.2 响应
在接收请求和解释请求后,服务器会进行CoAP响应(ACK报文),该响应通过客户端生成的"令牌"与"请求"匹配。
ACK报文的响应码一共有三类:
| 2 - 成功 | 请求已成功接收、理解和接受 |
| 4 - 客户端错误 | 请求包含错误语法或无法实现 |
| 5 - 服务器错误 | 服务器未能完成明显有效的请求 |
4.3 请求/响应码
请求/响应码用的都是报文头的Code (方法/响应码)字段,对应的结构图如下:
Code字段占用一个字节(8位)。上三位定义了类别,下五位代表具体细节。
为了便于阅读和文档书写,Code被写成c.dd的格式。其中 “c” 是以十进制表示的类,“dd”是以两位十进制表示的细节。
详细的 CoAP方法代码和响应代码如下两张图所示,不需要强行记忆:
4.4 工作流程
“请求 - 响应” 的模式有多种灵活的方式。
携带模式:ACK 的 payload 部分包含响应负载,此时 ACK 中的 Message ID 与 Token 都与请求报文中的相同。
分离模式:当服务器处理请求非常耗时,无法快速获取结果时使用。
分离模式下,服务器虽然会马上回复 ACK,但不携带负载,单纯为了响应 CON。
等到服务器处理完毕后,会主动发送一条新的 CON 消息将真正的数据发送给客户端,服务器后发的 CON 中的 Token 与 客户端请求的 Token 相同。
非确认模式:客户端发送一个 NON 消息,服务器收到后处理但不回复 ACK。
五、Option —— 选项
请求和响应都可能包括一个或多个选项的列表。例如,请求中的URI通过几个选项传输,HTTP中HTTP头中携带的元数据也作为选项提供。
5.1 选项格式
| Option Delta -- 选项增量 | CoAP 的消息中可能携带多个选项,每个选项都有一个预设的绝对编号,但是发送时不直接发送绝对编号,而是发送相对值 |
| Option Length -- 选项长度 | 用来表示 Option Value(选项值)的实际数据长度 |
| Option Value -- 选项值 | 选项所携带的具体数据 |
Option Delta 和 Option Length 虽然都使用 4bit(值范围0~15) 表示,按理来说 0~15 的数值应该使用同一个含义,但事实并非如此:
| Option Delta | 数值 0~12 直接代表差值为 0~12。13 和 14 触发扩展字节 (Option Delta extended)。 15 是特例,代表选项到此全部结束,紧接着的字节就是 Payload 数据。 |
| Option Length | 数值 0~12 直接代表长度为 0~12。13 和 14 触发扩展字节 (Option Length extended)。 15 是保留值,协议规定发送方不能用,接收方若收到 15,视为格式错误并丢弃该包。 |
Option Delta 和 Option Length 的值为 13 和 14 代表的含义是一样的。
当值 = 13 时:对应的扩展字段为 1byte(8位无符号整数)。实际值 = 13 + 后面这 1个字节的值,最大值可表示 268(13 ~ 255 + 13)。
当值 = 14 时:对应的扩展字段为 2byte(16位无符号整数)。实际值 = 269 + 后面这 2个字节的值,最大值可表示 65804(269 ~ 65535 + 269)。
5.2 选项定义
如下表,CoAP定义了一组用于请求和响应的选项:
选项属性: No.=选项编号,C=关键,U=不安全,N=无缓存键,R=可重复,Format=选项格式,Length=选项值长度范围。
5.3 Uri-Host,Uri-Port,Uri-Path,Uri-Query
本小节讲解"Uri主机、Uri端口、Uri路径、Uri查询",这几个选项用于指定对CoAP源服务器的请求的目标资源。
// URI 语法: coap-URI = "coap:" "//" host [ ":" port ] path-abempty [ "?" query ] // URI 举例 coap://example.com:5683/home/led?state=on&color=red // CoAP 报文里的写法: // 注意路径和查询参数不是一次性写入的,都是拆分的,接收方收到后才拼接 Uri-Host = example.com Uri-Port = 5683 Uri-Path = home Uri-Path = led Uri-Query = state=on Uri-Query = color=redUri-Host 和 Uri-Port 主要是为了解决虚拟主机的问题。
5.4 Proxy-Uri and Proxy-Scheme
本小节讲解"代理Uri 与 代理方案",当 CoAP请求 无法直接发送到目前服务器时,这两个东西就派上用场了。
Proxy-Uri,它的值是一个完整的绝对 URI(简单粗包),用于向转发代理发出请求。转发代理可以将请求转发到另一个代理,也可以直接转发到绝对URI指定的服务器。为了避免请求循环,代理必须能够识别其所有服务器名称,包括任何别名、本地变体和数字IP地址。
Proxy-Scheme,也用于向转发代理发出请求,但是比 Proxy-Uri 更灵活。代理会将原始URI最开头的协议名(coap 或 coaps)替换成 Proxy-Scheme 选项的值(以 Proxy-Scheme=http 举例,coap://example.com/led?state=on 会被替换成 http://example.com/led?state=on)。
5.5 Content-Format
本小节讲解"内容格式",Content-Format 选项用于指示消息有效负载的表示格式。最常用编号如下图:
除了这些基础编号,还有大量为特定应用定义的编号,这个不讲解了。
5.6 Accept
在 5.5 小节中我们学习了Content-Format,它解决的是“我发的是什么”的问题。但通信是双向的,请求发出去之后,服务器返回的数据是什么格式呢?如果服务器返回的是 XML,而你的小设备只能解析 JSON,那就鸡同鸭讲了。
Accept 选项可用于指示"客户端偏好接受的内容格式",它解决的是“我想要收的是什么”的问题。Accept 选项的值就是 5.5 节表格里的那些数字编号。
注意:Accept 只是 “偏好” 而非 “命令”,服务器有自由裁量权。如果无服务器不支持对应内容格式,会返回4.06 Not Acceptable错误码。
5.7 Max-Age
这个是 CoAP 协议中用于缓存控制的核心选项,此选项只能在"响应"中使用,用来告诉客户端(或中间代理)“这份资源数据你可以在本地缓存多久”。
使用场景:如果有多个传感器去读取同一个资源配置(比如同一个温湿度数值),Max-Age可以极大减少不必要的网络传输,减轻服务器压力。
HTTP 协议里的缓存控制 Cache-Control 通常是个很长的字符串(如Cache-Control: max-age=3600, public)。而 CoAP 的Max-Age只有一个纯粹的整数,连单位都不写(就是秒)。
5.8 ETag
这是用于资源版本标识的核心选项。它的作用相当于给服务器上的某一份资源数据打上了一个 “唯一指纹” 或 “版本号”。
| 响应中的 ETag | 当客户端发送 |
| 请求中的 ETag | 当客户端缓存了携带 ETag 数据后,在下次发起 GET 请求去询问数据是否变更时使用。如果服务器发现 ETag 有匹配上的,直接回复 如果没有 ETag 匹配上,回复 |
5.9 Location-Path and Location-Query
本小节讲解"位置路径 与 位置查询",这两个选项主要用于服务器响应客户端,告诉客户端“你刚刚创建或修改的资源,现在在哪个具体的 URI 位置上”。简单来说,它们就是告诉客户端“去哪儿能找到刚才的操作成果”。
假设在某系统中,客户端发出了一个POST请求去创建一盏新灯的配置:
1. 客户端请求:POST coap://lights.example.com/ 2. 服务器处理:服务器成功创建了一个新的灯配置,ID 是 123。 3. 服务器响应:返回 2.01 Created(创建成功),并在响应消息中放入以下选项: Location-Path 选项:值为 "lights"。 Location-Path 选项:值为 "123"。 4. 客户端后续动作:客户端收到并解析这个响应后,立刻知道新创建的灯的资源具体位于 coap://lights.example.com/lights/123。 随后,客户端就可以使用这个URI,去发起 GET请求 来获取这个新灯的状态了。 // 其实也只是拼接罢了,拼接逻辑: // coap:// [Location-Host] : [Location-Port] / [Location-Path拼接] ? [Location-Query拼接]。5.10 If-Match and If-None-Match
本小节讲解"条件匹配 与 条件非匹配",这两个选项这是 CoAP 协议中用来实现并发控制和防重复创建的关键选项。
它们的核心作用是:让客户端在请求中附加前置条件,告诉服务器 “只有在满足特定条件时,才执行我的请求;如果不满足,直接拒绝即可,不要盲目操作”。
| If-Match | 用于“更新或删除(PUT、DELETE)”类的操作,客户端在请求中带上一个或多个之前获取的 ETag(If-Match: "abc123")。服务器收到请求后,会比对当前资源的最新 ETag。只有当服务器当前的 ETag 与客户端提供的 ETag 中的某一个完全匹配时,才会真正执行这个更新或删除操作。 |
| If-None-Match | 用于“创建(POST)”新资源的操作。客户端在请求中带上这个选项后(If-Match: 冒号后面是空的),服务器在当前资源完全不存在的情况下,才执行创建。如果已存在,服务器会拒绝该请求,并返回 4.12 Precondition Failed(前置条件失败)错误。 |
根据表格中的描述,我们可以知道 If-Match 是用来放置多人并发控制的,If-None-Match 是用来防重复创建资源的。
5.11 Size1
本小节讲解“请求中资源大小指示”,在CoAP的基础规范 RFC 7252 中,Size1 的作用很单一,它主要用于请求中,提供关于请求体(Request Payload) 的大小信息。
Size1 的使用:
客户端使用 | 客户端在上传数据的请求(PUT/POST)中携带:“告诉服务器本次请求体的大小” |
服务器使用 | 当服务器发现请求体太大无法处理时,会返回 4.13(Request Entity Too Large 请求实体过大) ,并在响应中携带 Size1 选项,以告知客户端它所能接受的最大请求体大小。 |
简而言之,Size1 只负责 “上传” 相关的大小信息。
六、CoAP 的扩展机制
CoAP 有很多扩展,本文章就挑一部分进行讲解,不全部讲解了。
6.1 观察者模式
在标准 CoAP 机制中,客户端想要获取资源状态,只能不断循环“请求-响应”模式(轮询)。但这会导致巨大的网络开销和电池损耗。国际互联网工程任务组(IETF)发布 RFC 7641 扩展,增加"观察者模式"选项解决此问题。
观察者模式(Observe) 是 CoAP 协议最重要、最实用的一类扩展机制,选项定义:
RFC 7641官方文档链接:RFC 7641 - Observing Resources in the Constrained Application Protocol (CoAP)。
"观察者模式":它允许客户端向服务器"订阅"一个资源,一旦资源状态发生改变,服务器就会主动向客户端推送通知,无需客户端每次都发起请求。
观察者工作流程三步曲:
| 1. 注册 | 客户端发起标准的 GET 请求 在请求选项中带上 Observe=0 注销前客户端都不可忘记 Token | 服务器收到后会为这个客户端和资源建立 “订阅关系”。服务器立即返回第一次 2.05 Content 响应,并在响应中附带 Observe 选项(值随机,代表当前的序列号),同时将当前资源状态发给客户端。 |
2. 持续通知 | 一旦资源的状态发生变化 服务器会自动向客户端发起新的响应消息(2.05 Content) | 每个通知都会携带一个递增的 Observe 序列号。假设订阅时返回 Observe=12,通知的序列号就是 13、14... |
| 3. 注销 | 客户端不需要接收状态更新时 要发起的新的 GET 请求 并带上 Observe=1 | 服务器收到后,会清除该 “订阅关系”,停止发送通知。 |
6.2 块传输
CoAP 协议运行在 UDP 协议之上,UDP 数据报会受到 MTU(最大传输单元)的限制。当响应载荷(Payload)过大,或请求 URI 过长时,单个 UDP 包无法容纳,IP 层被迫分片,这会显著增加丢包概率,从而导致整个事务失败。
为解决此问题,IETF 在RFC 7959中定义了"块传输"扩展机制。该机制允许将大数据拆分为多个较小的“块”,每个块独立传输,从而避免 IP 分片,提升可靠性。官方文档链接:RFC 7959 - Block-Wise Transfers in the Constrained Application Protocol (CoAP)
6.2.1 块选项
这次 CoAP 引入了两个专用选项,Block1用于请求方向(如客户端PUT/POST,上传数据),Block2用于响应方向(如服务器回复 GET 请求,下载数据),选项定义:
6.2.2 拆分块选项
虽然使用场景不同,但是二者结构是相同的,位拆分结构表如下,这里的编码遵循 大端序(网络字节序):
| 选项值总长度 | 字段分配与位结构(从高位到低位) | 各字段的作用与范围 |
|---|---|---|
1 字节 (8 bits) | 位[7:4]=NUM,位[3]=M,位[2:0]=SZX | NUM最大 15 M最大 1 SZX最大 7 |
2 字节 (16 bits) | 位[15:4]=NUM,位[3]=M,位[2:0]=SZX | NUM最大 4095 M最大 1 SZX最大 7 |
| 3 字节 (24 bits) | 位[23:4]=NUM,位[3]=M,位[2:0]=SZX | NUM最大 1,048,575 M最大 1 SZX最大 7 |
三个字段的具体含义:
| NUM(块编号,20位) | 当前发送的是第几块数据。从 0 开始,后续块会递增:NUM=NUM+1 |
| M(More位,1位) | M=1:后面还有更多块,请客户端继续发送请求获取下一块。 M=0:当前是最后一块,传输可以结束了。 |
| SZX(块大小指数,3位) | 决定当前单个数据块的最大载荷字节数。计算公式: 提示:RFC 7959 规定 7 被保留等同于 6,即最大为 |
6.2.3 Size2 选项
RFC 7959中还引入了 Size2 选项,他们的结构是一样的:
Size2 和 Size1 功能互补,分别服务于 "请求" 和 "响应" 这两个方向,为了更好的理解,我们将它们放在一起看:
| 特性 | Size1 选项 | Size2 选项 |
|---|---|---|
| 核心用途 | 指示请求中资源表示的大小 | 指示响应中资源表示的大小 |
| 出现位置 | 主要出现在请求(PUT/POST)中 | 可出现在请求或响应中 |
| 主要场景 | 1. 客户端在上传数据时,告知服务器请求体总大小 2. 服务器因请求过大返回 4.13错误时,告知客户端能处理的最大请求体大小。 | 1.在请求中:客户端用来主动询问服务器某个响应资源的总大小 2.在响应中:服务器告知客户端当前正在传输的响应资源的总大小。 |