一、边缘网关接入ThingsBoard的常见坑:重复遥测问题

做物联网项目的开发者,大概率都遇到过这么个糟心事儿:边缘网关通过CoAP协议连ThingsBoard时,明明只发了一次温湿度、设备状态这类遥测数据,后台却存了好几条一模一样的。更气人的是,要是网络差、丢包多,这种重复的情况还会翻倍,不仅占存储空间,还会给后续的数据分析添乱——比如统计设备每小时的平均温度时,重复数据会直接把结果拉偏。

很多人一开始会觉得是网关的代码写烂了,或者ThingsBoard的接口有bug,查来查去发现都不是问题,最后卡在一个叫“CoAP重传机制”的东西上。其实要解决这个问题,核心是搞懂CoAP里的Observe通知和确认类型,这俩东西是专门针对丢包网络设计的,但很多人没摸透,反而踩了坑。

1.1 先搞懂:什么是CoAP的重传机制?

CoAP是专门给物联网设备设计的轻量级协议,和HTTP不一样,它的重传逻辑是自带的,不是靠应用层自己实现的。简单说,就是如果发出去的消息没收到对方的确认,CoAP会自动重发,直到收到确认或者超过最大重传次数(默认是4次)。

举个例子:网关发一条温度数据给ThingsBoard,要是中间丢包了,ThingsBoard没收到,网关的CoAP客户端就会在2秒后重发一次;如果还没收到,4秒后再重发;8秒、16秒各一次,总共重4次。要是四次都没收到,就认为连接断了。

但这里有个大问题:如果第一次发的消息其实到了ThingsBoard,只是确认包(ACK)丢了,那网关还是会重发,相当于给ThingsBoard发了两次一模一样的消息,这就是重复遥测的根源。

1.2 为什么ThingsBoard的CoAP接口会存重复数据?

ThingsBoard的CoAP接口默认是这么处理的:收到一条消息,就把它当成新的遥测数据存起来,不管这条消息是不是之前已经处理过的。因为普通的CoAP消息是“无状态”的,每条消息都有自己的唯一ID(Token),但ThingsBoard默认不会去缓存已经处理过的Token,也就没法判断这条消息是不是重复的。

比如网关发的消息Token是“abc123”,第一次到了ThingsBoard,存了数据,但ACK丢了,网关重发Token还是“abc123”的同一条消息,ThingsBoard又存了一遍,这就出现了重复。

二、解决重复问题的核心:Observe通知与确认类型

要解决这个问题,不能关掉CoAP的重传(关了重传丢包时数据会丢,更糟),得换一种消息交互模式,就是CoAP里的Observe机制,同时搭配正确的确认类型。

2.1 先搞懂CoAP的四种消息类型

CoAP的消息分四种,很多人只知道发数据,不知道选哪种类型,这是踩坑的关键:

  1. CON(确认型):必须收到对方的ACK才会停止重传,适合重要的、不能丢的消息,比如遥测数据。
  2. NON(非确认型):发出去就不管了,不会重传,适合不重要的、可以丢的消息,比如心跳包。
  3. ACK(确认包):专门用来回复CON消息的确认,告诉对方“我收到了”。
  4. RST(重置包):用来拒绝收到的消息,比如对方发的消息我处理不了。

2.2 Observe机制到底是什么?

Observe机制是CoAP专门为“客户端需要持续获取服务器数据”或者“服务器需要持续向客户端推送数据”设计的模式,核心有两个点: 第一,订阅机制:客户端(这里是边缘网关)先给服务器(ThingsBoard)发一个带Observe选项的CON消息,告诉服务器“我要订阅你这里的某类数据”,服务器收到后会返回一个带Observe序列号的ACK。 第二,序列号校验:之后服务器给客户端推送数据,或者客户端给服务器发订阅的消息时,都会带一个递增的序列号,服务器和客户端都会缓存最近的几个序列号,要是收到的序列号和之前的重复,就知道是重传的消息,直接丢弃,不会重复处理。

2.3 用Observe+CON解决重复问题的逻辑

我们把边缘网关和ThingsBoard的交互逻辑换成Observe模式,流程就变成这样:

  1. 网关先给ThingsBoard发一个带Observe选项的CON消息,内容是“我要上传遥测数据”,同时带上自己的设备ID、Token等身份信息。
  2. ThingsBoard收到后,验证身份没问题,返回一个带序列号(比如1)的ACK,告诉网关“我收到你的订阅了”。
  3. 之后网关每次发遥测数据,都发带Observe序列号(比如2、3、4…)的CON消息。
  4. ThingsBoard收到消息后,先检查序列号:如果是之前没处理过的,就存数据,然后返回带相同序列号的ACK;如果是已经处理过的(比如重传的序列号2),就直接返回带相同序列号的ACK,不存数据。
  5. 网关收到ACK后,知道消息已经被处理,就不会再重传;如果没收到ACK,再重传带相同序列号的消息,ThingsBoard还是会丢弃重复的,不会重复存。

这样一来,就算丢包导致重传,也不会产生重复遥测,同时还能保证数据不会丢(因为CON的重传机制还在)。

三、完整示例:边缘网关用Observe接入ThingsBoard

为了让大家更清楚,我们做一个完整的示例,技术栈统一用Python(用CoAPthon3库,这是Python里常用的CoAP客户端库)。

首先,先准备环境:

  1. 安装CoAPthon3:bash pip install coapthon3
  2. 确保ThingsBoard的CoAP接口是开启的(默认端口是5683),并且已经创建好一个设备,拿到设备的Access Token(比如这里假设是“my_test_token_123”)。

3.1 示例代码:边缘网关的CoAP客户端

# 导入CoAPthon3的相关类
from coapthon.client.helperclient import HelperClient
from coapthon.messages.message import Message
from coapthon.messages.request import Request
from coapthon.messages.response import Response
import time

# 配置ThingsBoard的CoAP地址和端口
THINGSBOARD_HOST = "127.0.0.1"  # 换成你的ThingsBoard服务器地址
THINGSBOARD_PORT = 5683
# ThingsBoard的遥测数据接口路径,默认是"/api/v1/{ACCESS_TOKEN}/telemetry"
TELEMETRY_PATH = "/api/v1/my_test_token_123/telemetry"

# 初始化CoAP客户端
client = HelperClient(server=(THINGSBOARD_HOST, THINGSBOARD_PORT))

# 全局变量:记录当前的Observe序列号,初始为1
current_observe_seq = 1

def handle_response(response: Response):
    """处理ThingsBoard返回的ACK"""
    global current_observe_seq
    if response is None:
        print("没有收到响应,可能网络异常")
        return
    # 检查响应是不是ACK类型
    if response.type == Message.ACK:
        # 从响应中获取Observe序列号(如果是Observe模式的响应)
        if response.observe is not None:
            # 收到ACK后,序列号加1,下次发消息用新的序列号
            current_observe_seq = response.observe + 1
            print(f"收到ACK,当前序列号更新为:{current_observe_seq}")
        else:
            print("收到普通ACK,不是Observe模式的响应")
    elif response.type == Message.RST:
        print("收到重置包,消息被拒绝")

def send_telemetry(temperature: float, humidity: float):
    """发送遥测数据的函数"""
    global current_observe_seq
    # 构造CoAP请求:CON类型,POST方法,路径是遥测接口
    request = Request()
    request.type = Message.CON  # 用CON类型,必须收到ACK才停止重传
    request.code = Message.POST
    request.uri_path = TELEMETRY_PATH
    # 设置Observe选项,带上当前的序列号
    request.observe = current_observe_seq
    # 构造遥测数据的JSON内容
    payload = f'{{"temperature": {temperature}, "humidity": {humidity}}}'
    request.payload = payload
    # 发送请求,设置超时时间为5秒,超时后会自动重传
    client.send_request(request, handle_response, timeout=5)

if __name__ == "__main__":
    # 先发送订阅请求:告诉ThingsBoard我们要使用Observe模式上传遥测
    print("发送Observe订阅请求...")
    subscribe_request = Request()
    subscribe_request.type = Message.CON
    subscribe_request.code = Message.POST
    subscribe_request.uri_path = TELEMETRY_PATH
    subscribe_request.observe = 0  # 订阅请求的序列号一般设为0
    subscribe_request.payload = '{"subscribe": "telemetry"}'  # 订阅请求的内容,ThingsBoard会处理
    client.send_request(subscribe_request, handle_response, timeout=5)
    
    # 等待订阅请求的响应,确保订阅成功
    time.sleep(2)
    
    # 模拟每10秒发送一次遥测数据
    for i in range(5):
        temp = 25.0 + i * 0.5  # 模拟温度变化
        hum = 60.0 + i * 1.0   # 模拟湿度变化
        print(f"发送第{i+1}次遥测:温度{temp},湿度{hum},序列号{current_observe_seq}")
        send_telemetry(temp, hum)
        time.sleep(10)
    
    # 关闭客户端
    client.stop()

3.2 示例的运行逻辑说明

  1. 第一步先发订阅请求:序列号设为0,告诉ThingsBoard我们要使用Observe模式上传遥测,ThingsBoard收到后会返回序列号为0的ACK,客户端收到后把当前序列号更新为1。
  2. 之后每次发遥测,都用当前的序列号(1、2、3…),ThingsBoard收到后检查序列号:
    • 如果是新的序列号,就存数据,返回带相同序列号的ACK,客户端收到后把序列号加1。
    • 如果是重复的序列号(比如重传的1),就直接返回带序列号1的ACK,不存数据。
  3. 就算网络丢包,网关重传带相同序列号的消息,ThingsBoard也不会重复存,解决了重复遥测的问题。

四、技术分析:应用场景、优缺点、注意事项

4.1 应用场景

这种Observe+CON的模式,最适合的场景是:

  • 边缘网关和ThingsBoard之间的网络不稳定,经常丢包、延迟高(比如用2G、LoRaWAN、卫星网络的场景)。
  • 对数据完整性要求高,不能丢数据,但也不能有重复数据(比如工业设备的状态监控、环境监测的数据分析)。
  • 网关需要持续上传遥测数据,不是偶尔发一次。

4.2 技术优缺点

优点:

  1. 解决了重复遥测的问题:序列号校验机制可以过滤重传的消息。
  2. 保证了数据完整性:CON的重传机制可以在丢包时重发数据,不会丢数据。
  3. 适配不稳定网络:专门针对丢包、延迟高的网络设计,比普通的CoAP模式更可靠。

缺点:

  1. 增加了一点点复杂度:需要额外处理Observe的订阅和序列号管理,代码比普通的CoAP发送多了几个步骤。
  2. 服务器需要缓存序列号:ThingsBoard需要缓存每个设备最近的几个序列号,虽然缓存的数量不多(一般缓存最近5个就够了),但对服务器的性能有一点点影响(可以忽略不计)。
  3. 不适合非持续的场景:如果网关只是偶尔发一次数据,用Observe模式反而麻烦,这种情况可以用普通的CON模式,然后在应用层自己做去重(比如给每条消息加一个唯一的ID,ThingsBoard缓存最近的ID)。

4.3 注意事项

  1. 序列号的处理:序列号必须是递增的,不能重复,客户端要在收到ACK后及时更新序列号,不然会导致后续的消息被当成重传的。
  2. 超时时间的设置:CoAP的重传超时时间要根据网络情况调整,比如卫星网络的延迟高,超时时间可以设为10秒以上,不然会频繁重传。
  3. 订阅请求的处理:订阅请求必须成功,不然ThingsBoard不会启用序列号校验,还是会存重复数据,所以代码里要加订阅请求的错误处理(比如订阅失败就重发订阅请求)。
  4. ThingsBoard的配置:有些版本的ThingsBoard可能需要额外配置才能支持Observe模式,要确保你的ThingsBoard版本是3.3以上(这个版本开始默认支持CoAP的Observe)。

五、文章总结

边缘网关通过CoAP接入ThingsBoard时的重复遥测问题,本质是CoAP的重传机制和ThingsBoard默认的消息处理逻辑不匹配导致的。要解决这个问题,不能关掉重传(会丢数据),也不能只靠应用层去重(太麻烦),最适合的方案是用CoAP的Observe机制,搭配CON类型的消息。

Observe机制的核心是序列号校验,通过递增的序列号,服务器可以判断收到的消息是不是重传的,从而过滤重复的,同时CON类型的消息保证了丢包时会重传,不会丢数据。这种模式完美适配了不稳定网络下的遥测数据传输,兼顾了数据完整性和不重复的要求。

最后要提醒大家,技术选型要根据实际场景来:如果网络稳定,偶尔发一次数据,用普通的CON模式就够了;如果网络差,需要持续传数据,一定要用Observe+CON的模式,不然重复遥测的问题会一直困扰你。