Python用paho-mqtt记录详细日志

Python的第三方库paho-mqtt提供了与mqtt broker通信的方法,笔者在它的基础上定义一个MqttClient类,以实现以下需求:

  • 定制一个MQTT通信程序。
  • 更改paho-mqtt生成的日志,使日志更详细(包括发送、接收的消息的payload)。

准备工作

  • 在编写一个实现mqtt通信的程序之前,首先得搭建一个mqtt broker。
    快速试装一个mqtt broker,参考:https://blog.csdn.net/qq_35952638/article/details/88897089

  • 在编写程序收发mqtt信息时,建议同时运行一个MQTT.fx进行抓包,检查通信过程。
    MQTT.fx的用法,参考:https://blog.csdn.net/qq_35952638/article/details/89000241

踩坑的经验

  • 如果程序进行MQTT通信时,没有正常发布或订阅消息,请用MQTT.fx试试发布或订阅该消息,从而判断是程序的问题还是mqtt broker的问题。
  • 检查程序的日志,就能判断程序是否发布或订阅了某条消息。
  • 如果程序不能订阅某个topic,可能是因为mqtt broker限制了权限。

主要代码

import json
from paho.mqtt.client import *


class MqttClient:
    """
    创建一个mqtt client,实现底层的mqtt通信功能。
      - 实例化该类之后,首先,要调用`mqtt_connect()`连接到mqtt broker。
      - 连接成功后,可以调用公有方法进行通信:`subscribe()`、`publish()`
      - 用户应该根据自身需求,重载该类的回调函数:`connect_callback()`、
      `receive_callback()`、`publish_callback()`
    """
    log = print  # 设置记录日志的函数

    def __init__(self, name):
        self.name = str(name)
        self.is_connected = False

        # 创建MQTT连接的客户端
        self.client = _Client(client_id=self.name)
        self.client.name = self.name

        # 设置MQTT的回调函数(mqtt模块会为每个client创建一个线程,来启动回调函数)
        self.client.on_connect = self.__connect_callback
        self.client.on_message = self.__receive_callback
        self.client.on_publish = self.__publish_callback
        self.client.on_log = self.__log_callback

        self.msg_id = 0  # 每个消息中包含一个id,依次递增,便于判断消息是否过时

        self.published_count = 0  # 发送成功的次数
        self.published_failed = 0  # 发送失败的次数
        self.received_count = 0  # 接收消息的次数

    def get_user_pwd(self):
        """ 获取mqtt连接的帐号和密码,用户应该自行重载该函数。 """
        # user, pwd = "admin", "123456"
        # return user, pwd

    def get_addr_port(self):
        """ 获取mqtt broker的ip地址和端口号,用户应该自行重载该函数。 """
        # addr, port = "127.0.0.1", 1883
        # return addr, port

    def mqtt_connect(self):
        self.log("DEBUG", "connecting to mqtt broker ...")
        self.client.username_pw_set(*self.get_user_pwd())
        self.client.connect(*self.get_addr_port())
        self.loop_start()

    def __connect_callback(self, client, userdata, flags, rc):
        """
        客户端连接到MQTT后的回调函数,被mqtt.Client调用。
        详见mqtt.Client.on_connect的定义。
        """
        replies = {0: "Connection successful",
                   1: "Connection refused - incorrect protocol version",
                   2: "Connection refused - invalid client identifier",
                   3: "Connection refused - server unavailable",
                   4: "Connection refused - bad username or password",
                   5: "Connection refused - not authorised"
                   # 6-255: Currently unused.
                   }
        self.log("INFO", replies[rc])

        if rc == 0:
            self.is_connected = True

        self.connect_callback(client, userdata, flags, rc)

    def connect_callback(self, client, userdata, flags, rc):
        """ 供子类重载 """
        pass

    def disconnect(self):
        self.client.disconnect()

    def loop_start(self):
        """ 开启mqtt的通信循环线程 """
        self.client.loop_start()
        self.client._thread.setName(self.name + "(mqtt)")
        self.log("INFO", "mqtt loop starts")

    def loop_stop(self, force=False):
        """ 关闭mqtt的通信循环线程 """
        self.client.loop_stop(force)
        self.log("INFO", "mqtt loop end")

    def subscribe(self, topic, qos=0):
        """ 订阅一个topic。 """
        self.client.subscribe(topic, qos)

    def __receive_callback(self, client, userdata, message):
        """
        客户端收到消息后的回调函数。
        message is a class with members topic, payload, qos, retain.
        详见mqtt.Client.on_message的定义。
        """
        self.received_count += 1
        self.receive_callback(client, userdata, message)

    def receive_callback(self, client, userdata, message):
        """ 供子类重载 """
        pass

    def publish(self, topic, payload, qos=0, retain=False):
        """
        发送一条消息到指定的topic。
          `payload`: 要发送的消息内容,为字典类型。
          qos=0 时,消息最多发布一次,可能一次都没收到。
          qos=1 时,消息至少发布一次,可能收到多次。
          qos=0 时,消息仅发布一次,保证收到且只收到一次。
        """
        self.msg_id += 1
        try:
            payload["id"] = str(self.msg_id)  # 设置消息的id,转换成字符串类型
            # 将payload转换成json格式发布,并获得这次publish的信息
            info = self.client.publish(topic, json.dumps(payload), qos, retain)

            # 同步工作,等待消息成功发出或超过timeout
            info.wait_for_publish()
            if info.is_published():
                self.published_count += 1
            else:
                raise RuntimeError("waiting for publish but timeout")

        except RuntimeError as e:
            self.msg_id -= 1
            self.published_failed += 1
            self.log("ERROR", "publish failed, {}".format(str(e)))

    def __publish_callback(self, client, userdata, mid):
        """
        客户端发布消息后的回调函数。
          - 对于qos为0的消息,这只意味着消息离开了客户端。
          - 对于qos为1或2的消息,这意味着已经完成了与mqtt broker的握手。
        """
        self.publish_callback(client, userdata, mid)

    def publish_callback(self, client, userdata, mid):
        """ 供子类重载 """
        pass

    def __log_callback(self, client, userdata, level, buf):
        """
        定义记录日志的函数,每次mqtt模块产生日志时会自动调用该函数。
          - mqtt模块生成的日志级别与logging模块不同,要进行转换。
        """
        self.log(LOGGING_LEVEL[level], buf)
        # mqtt模块生成的日志格式大致为
        # "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)",
        # dup, qos, retain, mid, topic, payloadlen


# sample
if __name__ == "__main__":

    class Device(MqttClient):

        def get_user_pwd(self):
            user, pwd = "admin", "123456"
            return user, pwd

        def get_addr_port(self):
            addr, port = "127.0.0.1", 1883
            return addr, port

        def run(self):
            self.log("DEBUG", "start threading: {}".format(self.name))

            # 连接到mqtt broker,并计算连接耗时
            start_time = time.time()
            self.mqtt_connect()
            while not self.is_connected:
                time.sleep(0.01)
            self.log("DEBUG", "This connection takes {}s".format(
                time.time() - start_time))

            # 发布和订阅
            msg = {"text": "This is for test."}
            self.publish("/test/post", msg)
            self.subscribe("/test/reply")

            # self.disconnect()

            # 保持循环
            while 1:
                time.sleep(1)

    # 创建一个设备
    d1 = Device("d1")
    d1.run()

测试运行

  • 运行该脚本,可见记录了详细的日志。程序运行一会之后,还会自动发出心跳请求PINGREQ。
    在这里插入图片描述
  • 虽然日志里记录了消息已被Publish,但不一定真的发送成功了。在MQTT.fx上检测到该消息,才说明该消息已发送到mqtt broker。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值