关闭

网络抓包学MQTT物联网协议

标签: 物联网网络技术mqtt-v3-1
1870人阅读 评论(2) 收藏 举报
分类:

GitChat 作者:冰尘
原文: 网络抓包学MQTT物联网协议
关注微信公众号:GitChat 技术杂谈 ,一本正经的讲技术

前言

MQTT (Message Queue Telemetry Transport),翻译成中文就是,遥测传输协议,其主要提供了订阅/发布两种消息模式,更为简约、轻量,易于使用,特别适合于受限环境(带宽低、网络延迟高、网络通信不稳定)的消息分发,属于物联网(Internet of Thing)的一个标准传输协议。

MQTT协议的设计思想是开放、简单、轻量、易于实现。现在国内很多企业都已经广泛使用 MQTT 作为手机客户端与服务器端推送消息的协议,除此之外,由于其协议的特别针对受限环境(带宽低、网络延迟高、网络通信不稳定),在物联网(Internet of Thing)应用中已经大展拳脚,因为 很多物联网的设备都是一些计算和储存能力受限的嵌入式设备。

那么应该如何学习这个协议呢? 笔者提供了一种深入浅出的方法,通过使用 WireShark 进行网络抓包,结合真实存在网络字节,深入其中,来一场栩栩如生的学习体验。另作者为了照顾没有基础背景的读者,将会安装下面的顺序循序渐进的进行说明和分享。

一、为什么使用MQTT 协议?

MQTT(英语全称,Message Queue Telemetry Transport),中文翻译过来就是遥测传输协议:其主要提供订阅/发布模式,更为简约、轻量,易于使用,针对受限环境(带宽低、网络延迟高、网络通信不稳定),属于物联网(Internet of Thing)的一个传输协议。设计思想是开放、简单、轻量、易于实现。这些特点使它适用于受限环境。例如,但不仅限于此:

  • 特别适合于网络代价昂贵,带宽低、不可靠的环境。

  • 能在处理器和内存资源有限的嵌入式设备中运行。

  • 使用发布/订阅消息模式,提供一对多的消息发布,从而解除应用程序耦合。

  • 使用 TCP/IP 提供网络连接。

  • 提供Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。

对消息中间件,估计大家不得不关心的就是消息的可靠性,也就是消息的发布服务质量,可喜的是,MQTT支持三种消息发布服务质量(QoS):

  • “至多一次”(QoS==0),消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。

  • “至少一次”(QoS==1),确保消息到达,但消息重复可能会发生。

  • “只有一次”(QoS==2),确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量。

二、支持MQTT协议消息中间件产品

目前有很多的MQTT消息中间件服务器,如下,都是MQTT协议的服务器端的实现。

  • IBM Websphere

  • MQ Telemetry

  • IBM MessageSight

  • Mosquitto

  • Eclipse Paho

  • emqttd Xively

  • m2m.io

  • webMethods

  • Nirvana Messaging

  • RabbitMQ

  • Apache ActiveMQ

  • Apache Apollo

  • Moquette

  • HiveMQ

  • Mosca

  • Litmus Automation Loop

  • JoramMQ

  • ThingMQ

  • VerneMQ

因为Mosquitto 是当前用户最多的一款产品,所以本文将选择以Mosquitto为MQTT的服务器来和大家分享MQTT协议。大家可以到此网站地址下载。 其支持几乎市面上所有的操作系统,而且更为神奇的是,其还提供了javascript版的Mosquitto服务器,简直要逆天的节奏,笔者是在Windows机器上安装Mosquitto的,所以下载一个Windows的二进制的安装包安装即可,很简单,作者就不再啰嗦了。

三、 什么是MQTT 协议?

我们知道MQTT是一个物联网协议的一个规范,MQTT的协议最新的三个版本是:3.1.1,3.1.0和5.0的协议。

其中5.0还只是一个提案(2017年7月13日发布的一个草稿版的提案),所以本篇文章暂时不涉及MQTT 5.0的协议,考虑到目前主流和成熟的MQTT协议的应用都是MQTT 3.1.1的协议,所以笔者还是以MQTT 3.1.1的协议为基准,给大家分享,只要这个会了,其他的自然不在话下。需要注意的是,MQTT 3.1.0和3.1.1 规范,还是有小部分的区别的。比如可变头部中,在3.1.0的规范中,其关键字是,“MQISdP”,

表 1 MQTT V3.1规范可变消息头的前8个字节

enter image description here

而在MQTT V3.1.1的规范中,其可变消息的头的协议名称的关键字是“MQTT”.如下图所示意。

表2 MQTT V3.1.1可变消息头的前6个字节

enter image description here

MQTT的协议说明文档主要描述了MQTT消息的通用格式,消息的命令,消息流动等等的规范和原则。

在前面的3个章节,我们对MQTT已经有了一个大概的感性认识,在接下来的系列文章,我将带领大家从MQTT的14种消息类型中,挑出2种比较经典的消息格式,不管是发布还是订阅都要用到,那就是MQTT连接的请求和相应的消息类型。 并通过抓包软件(WireShark)结合MQTT 3.1.1 定义的具体规范和大家走一遍。

注意,因为我用的Eclipse Paho API的Java库,支持MQTT 3.1.1 的版本,且mosquitto也支持3.1.1的版本,所以本系列所有的抓包都会是基于MQTT 3.1.1的规范,而不是MQTT 3.1的规范,这点敬请大家注意。

下面的表格列出了MQTT的14中不同的消息类型,其中0,15都属于保留字段,不算消息类型,所以总共有16-2=14 种消息类型。

表3 MQTT的14种消息类型(Command message)

enter image description here

四、如何抓取MQTT 协议包?

在进行协议分析之前,我们先考虑一下下面2个问题:

  • 如何产生MQTT的数据包?

  • 如何抓取MQTT的包?

首先看如何产生MQTT的数据包,因为MQTT协议被几乎编程界所有的主流语言支持,为了照顾所有的读者,笔者就不选任何一门语言来做示范,而是用一款工具软件来进行MQTT数据包的发送和订阅。服务器端选择当前用户最多的服务器,Mosquitto; 客户端选择Eclipse Paho 提供的MQTT的UI客户端,这个客户端工具是一个基于Java的Eclipse桌面客户端程序,其底层的和MQTT服务器进行的交互的java类库就是Eclipse Paho java库。假设我们在本机(127.0.0.1)已经启动了一个mosquitto MQTT服务器,其端口为1883。如何使用 Eclipse Paho MQTT工具?

  1. 下载Eclipse Paho MQTT 工具

    图1:Eclipse Paho下载地址

    enter image description here

  2. 解压缩后,双击paho.exe,打开后的对界面如下。

    图2:Eclipse Paho UI开始界面

    enter image description here

  3. 点击上图中的 十字图标,就能新建一个MQTT的客户端的连接,输入正确的MQTT服务端的连接地址,比如,本例中的连接地址是tcp://localhost:1883,然后点击“Connect”按钮,这个时候,如果MQTT服务端没有设置密码(默认情况是没有密码的)的话,这个时候,我们就能看到连接得到状态(status)是“Connected”。

    图3 Eclipse Paho UI开始界面

    enter image description here

  4. 这个时候我们就能订阅消息了。选择“Subscription”下方的绿色十字图标,就可以输入订阅的主题(topic)的名字,比如我们设置主题名称为“test”,并点击 “Subscribe”按钮。

    图4 Eclipse Paho UI订阅界面

    enter image description here

  5. 往MQTT服务发送一条消息主题为“test”,内容为“大家好,这是我一条消息。”的MQTT消息。然后点击“Publish”按钮,这个时候,我们就能看到消息已经发送成功,且在步骤(4)订阅的同一主题也收到了消息。

    图5 Eclipse Paho UI消息显示界面

    enter image description here

神奇吧,恭喜你,已经基本学会了如何用Eclipse Paho MQTT工具作为客户端来发送和接收消息了。

MQTT消息的模拟,已经用一种最简单的方法来模拟了,那么应该用什么工具来抓包呢? 当然是WireShark了,在Wireshark中,有capture filter和Display Filer,我们只需要在WireShark软件中的capture filter 输入下面的过滤条件,则与MQTT服务交互的相关TCP的数据包就能顺利抓取到。如下图所示意,注意,下面的1883端口是MQTT服务器的默认推荐端口,如果读者改变这个默认端口,请把1883端口改成相应的端口。

图6 WireShark转包配置界面

enter image description here

五、如何分析MQTT协议包?

万事具备了,环境准备好了,抓包工具也准备好了,是时候通过WireShark抓取到的TCP包来分析MQTT协议了,考虑到MQTT协议有多个版本,本文采用的MQTT的协议是MQTT 3.1.1的版本,如果不做特别的说明,服务器的地址应该是192.168.80.196,用的是Mosquitto MQTT服务器。

另外,因为牵涉到最基础的计算机知识,二进制,16进制以及10进制的转换,笔者想只要是参加Gitchat的读者朋友都是对技术有追求的朋友,笔者在这就不在赘述,默认大家都会啊,如果有不会的同学,请单独与我联系啊。

5.1 CONNECT 消息类型分析

假设有这么一个场景,MQTT的客户端需要连接MQTT的服务器端,这个时候,TCP的三次握手协议完成后,MQTT的客户端就会马上发送一条MQTT CONNECT消息帧。

咱们还是以Phao UI工具为例子,当我们点击“Connect”按钮的时候,究竟在TCP协议层发生了什么?

其实MQTT的CONNECT消息主要用来在客户端和服务端建立一个TCP的通信连接,这个CONNECT的发起者肯定是客户端。因为要建立一个连接,所以客户端在这个消息里面需要提供Client ID, 如果服务端设置了用户名和密码认证,在这个消息里面还必须要包括用户名和密码的相关信息,且如果设置了连接断开的最后遗言(Last Will and Testament),则在这个消息里面还必须包含和最后遗言相关的信息。下面是实验的具体步骤:

首先,在点击Eclipse Phao UI客户端工具上的“Connect”按钮之前,先在客户端把WireShark软件打开,并按照上一个章节(第4章节)的最后一部分关于WireShark的Capture Filter的设置把协议和端口设置好(TCP and port 1883)。

然后,打开Eclipse Paho的UI客户端,在本实例中,用于连接MQTT服务器的相关配置信息如下图7所示意(为了显示方便,笔者把Eclipse Paho对MQTT 连接的配置信息的不同配置页面整合到了一个图里面了,就是图7),请大家记住下面用于建立MQTT连接的相关信息,然后点击“Connect ”按钮。

图7 Eclipse Paho UI的连接信息配置页面

enter image description here

这个时候WireShark就能抓取到类似于下面的TCP数据包。具体的包如下:

图8 Eclipse Paho UI的WireShark抓包数据

enter image description here

从上面的抓包可知,TCP的三次握手连接之后,MQTT的客户端会发送第一条MQTT的CONNECT消息格式的数据给MQTT服务器。 其具体数据,见上图左下角,10 4e 00 4d 51 54 54 04 ee 00 3c 00 1e 31 31 31 ……….

根据MQTT v3.1.1的协议,首先咱们来看头部信息。

5.1.1 CONNECT 固定头部

WireShark抓包抓取的固定头部(Fix head)的16进制的数据为:10 4e, 那10 4e这两个字节代表什么意思呢?

首先来看一下,MQTT 3.1.1的协议对CONNECT 消息固定头部的2个字节的规范和定义。

表4 CONNECT 报文的固定头部(Fixed Header)格式

enter image description here

从这张图片可以知道,MQTT的Connect消息格式中,有一个固定的头部,其是由2个二进制的8位(bit)字节来表示的。

10—表示发送的Connect的请求(也就是二进制的0001 0000, 详细信息,请参考
表3 MQTT的14种消息类型(Command message) )** ,前4个的二进制位是0001,我们可以知道其标识的是一个MQTT CONNECT 消息类型,后面的4个bit是保留位,默认为4个二进制位的0000),MQTT CONNECT的消息格式的第一个字节分析完成。

4e—表示后面将跟着4*16+14=78个字节,也就这个MQTT的消息帧,后面部分还有78个字节,包括10个字节的可变头部(规定是10个字节)和 68个字节的负载(payload)。具体什么是可变头部和什么是消息负载,请继续看下面的分析。

5.1.2 CONNECT 可变头部(10个字节)

接下来的10个字节是:00 04 4d 51 54 54 04 ee 00 3e, 这10个字节,根据MQTT的3.1.1的规范,是10个字节的可变头部,其MQTT的CONNECT的可变头部主要由四大部分按顺序组成:

  • 协议名称(Protocol Name,)

  • 协议级别(Protocol Level)

  • 连接标志(Connect Flags)

  • 保持心跳(Keep Alive)

下面笔者就把上面通过WireShark抓包抓到的10个字节填充到MQTT的3.1.1x协议可变头部的模板表格中,可变头部的规范马上就一目了然了。

表5 固定头部格式重放

enter image description here

下面对这4部分做进一步的详细分析,首先看协议名称。

5.1.2.1 协议名称

协议名称由6个字节组成,16进制数据为:00 04 4d 51 54 54,其代表的是MQTT协议。对于一些第三方的网络监测软件,可以通过分析TCP的包,从而知道这是MQTT协议。

enter image description here

接下来看协议级别。

5.1.2.2 协议级别

协议级别只有1个字节,我们这个实验中抓取到的16进制数据为:04,其实代表的就是协议的修订级别(revision level),说白了,就是修订版本,04 代表的是MQTT 3.1.1的修订协议;而03 代表的是MQTT 3.1.0的修订协议,下面来看连接标志。

5.1.2.3 连接标志

对于连接标志这一部分,还是挺有意思的,其对应了一个字节。并用字节中不同位(bit)是true(二进制 1)或者false(二进制 0)来表示不同的业务意义, 其本质就是定义了7种类型的连接标志位(包括最后一位的保留标志位)。

那为什么是7种呢?8个位应该是8种啊?原来其中一种定义的是QoS(质量服务标准)占据了2个位(bit),其有三种情况(具体参考第一章节的MQTT支持三种消息发布服务质量(QoS)

表6 连接标志位数据重放

enter image description here

上面这张表也许看的还不是特别清楚,如果把上面的表转个维度和方向查看,其对应的意义就一目了然了。

表7 连接标志位数据格式换角度重放

enter image description here

其中我们可以看到,因为在Eclipse Paho UI 工具中,我们选择了认证且输入了用户名和密码,所以User Name和Password的标志位都置成了1。

下面来看最后的遗愿(Last Will and Testament),在Eclipse Paho的连接配置上,对应有2个设置(QoS,Retained),如下图9。

图9 Eclipse Paho UI的连接配置设置中的最后遗愿(Will)配置

enter image description here

Will Retained 因为在Eclipse Paho UI工具上勾选了,所以其连接的标准为为1。

Will QoS的值为01,刚好和Eclipse Phao的UI工具上的设置(1-At Least Once)对应了起来。

对于Clean Session的设置,请见上图7 Eclipse Paho UI的连接信息配置页面Clean Session的单选框被勾上了,所以Clean Session对应的标志位(bit)为1。

最后剩下的保留位(bit),保留位(bit)默认值为0。

5.1.2.4 保持心跳间隔

根据MQTT协议,每隔固定的时间,如果没有业务相关的消息发送的话,MQTT的客户端必须在1.5倍的Keep Alive的时间内,向MQTT的服务器发送一个PINGREQ的消息帧,以表示我们的客户端还是在线的,还活着。否则服务器则任务网络发生了故障,将会强制断掉于客户端的连接。

表8 保持在线配置协议格式定义

enter image description here

其中:

00 Keep alive的时间的高位(MSB):0。

3c 代表Keep alive时间的低位(LSB): 3*16+12=60秒,刚好和图7 Eclipse Paho UI的连接信息配置页面 中Keep Alive的数字输入框对应起来。10个字节的可变消息头分析完了,下面来看消息负载(Payload),但是奇怪的是消息负载没有和可变消息头直接连起来,而是中间隔了2个字节,00 1e,这就是传说中的分隔标志字节,请看下面的分析。

5.1.3 分隔标志字节

实际上,在MQTT的协议里面也没有找到对其的描述,其实就是一个分割标志字节,在可变消息头和消息负载(payload)之间,用00 1e进行分割;后面还能遇到00 3c; 00 05 都是分隔标志符号,用于分隔不同的数据类型;好了,下面去看望一下消息负载吧(Payload)。

5.1.4 消息负载(Payload)

前面的固定消息头,可变消息头,以及分隔标志字节已经分析完了,下面正式进入消息的负载(Payload)的字节分析。

本实验中的消息负载包括了MQTT的客户端的ID,遗愿的主题(Will Topic),遗愿的消息内容(Will Topic)以及为了通过MQTT服务器的Basic认证而要提供的用户名和密码。下面逐一进行分析。

5.1.4.1 客户端ID

首先看客户端的ID,16进制为: 31 31 31 31 31 31 31 31 31 31 31 31 31 31 3131 31 31 31 31 31 31 31 31 31 31 31 31 31 31,如果翻译成ASCII的表示,其就是(111111111111111111111111111111),其对应的就是,图7 中的Client ID的输入,不好意思,有点调皮了,输入了这么多个1 作为MQTT的Client ID,其实笔者就是想看看Eclipse Paho这个Client ID字段最多能让我输入多个字符,好奇害死猫啊。

接下来,看看最后的遗愿(Last Will and Testament)在消息负载中放入什么东东。

5.1.4.2 遗愿(Will)主题

最后的遗愿(Last Will and Testament)的主题中放入的还是图7中输入Topic。

Will Topic: 16进制数据为:6c 77 74, 翻译成人能懂的ASCII码就是: lwt。

后面紧跟的00 0c,是遗愿主题和遗愿消息之间的分隔标志符,具体说明请参看5.1.3.

5.1.4.3 遗愿(Will)消息体

后面的16进制数: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 代表的就是人能懂得ASCII码Hello World!。 这个就是最后的遗愿(Last Will and Testament)的消息体(Message)

后面紧跟的00 05是遗愿(Topic,Message) 和认证的用户名密码之间的分隔标志符号。

5.1.4.4 连接MQTT服务端的用户名和密码

下面看用户名和密码:

16进制: 61 64 6d 69 6e 代表的ASCII码就是用户名:admin

16进制: 70 6173 73 77 6f 72 64 代表的ASCII码就是用户名 :password

中间用 00 08标志分隔符分开了。

恭喜你,整个的Connect的消息格式就分析完毕了。安奈不住激动心情的你,是不是想跃跃欲试了! 通过对抓取的包和MQTT 3.1.1协议标准对比,我们非常容易掌握了其MQTT CONNECT消息类型背后的规范和定义,这个感觉是不是非常的好啊。

5.2 CONNACK 消息类型分析

在上节中我们分析了MQTT CONNECT消息类型的格式,我们知道CONNECT消息是客户端发送出去的,作为对客户端的连接请求,服务器端同样会有一个消息的返回,这个消息就是MQTT CONNACK的消息类型。我们在发出去CONNECT消息后,如果WireShark抓包工具依然开启的话,将会抓到类似于下面的TCP消息,其16进制为:20 02 00 00,那么其代表什么意思呢?如下图10所示意。

图10 服务器连接应答抓包

enter image description here

5.2.1 连接应答固定消息头

在分析之前,我们先回到MQTT3.1.1的协议中对客户端连接应答(CONNACK)消息帧的定义和描述。

表9 连接应答的固定消息头

enter image description here

其和表4的连接消息帧(CONNECT)固定消息头定义是一样的,只不过,对于第1个字节,10表示的是Connect的消息帧类型;而20表示的是连接应答(CONNACK)的消息帧类型。对于第2个字节,02 表示后面将跟着2个字节. 其意义和前一节的连接消息帧(CONNECT)定义的规范是类似的,表示后面还跟有多少个字节(剩下多少个字节,既包括消息头也包括消息的负载)。

02 表示后面将跟着2个字节。

5.2.2 连接应答的可变消息头

5.2.1章节提到了,连接应答的固定消息头的第2个字节是02,表示后面还跟着2个字节,这两个字节就是连接应答的可变消息头。

表10 连接应答的可变消息头

enter image description here

面我们来分析其可变头的2个字节: 00 00

  1. 其中第一个00,目前没有什么特别的含义,是MQTT协议的保留字段,也许在将来的协议版本中会用到。

  2. 第二个字节的00 表示连接成功。MQTT协议对于返回的结果总共定义了6种类型(请见下面的表3): 连接接受,连接拒绝(不可接受的版本),连接拒绝(Client ID服务器不允许),连接拒绝(服务器不可达),连接拒绝(错误的用户名和密码),连接拒绝(客户端没有通过授权认证)。具体含义请见下表11。

表11 连接应答返回值状态速查表

enter image description here

这个消息格式简单吧,嘿嘿,恭喜读者在学习MQTT消息格式的旅途中又往前快速前进了一步。

剩下的12种MQTT消息类型(请见,表3 MQTT的14种消息类型(Command message)),分析过程其实也是类似的,笔者就不在啰嗦赘述了,大家只要把抓到的协议包然后对照MQTT 3.1.1协议规范做一个对比,就能非常容易的把MQTT的底层协议分析出来。

六、总结

在这篇Chat中,笔者首先分享了为什么使用MQTT协议,然后列出了当前那些开源产品和框架支持MQTT协议,紧接着为了照顾一些没有接触过MQTT的读者,笔者对MQTT协议做了一个扫盲;工欲善其事必先利其器,在给大家做了理论铺垫之后,笔者又给大家分享了如何不用写代码,也能模拟MQTT消息的发送,且如何通过WireShark来抓取MQTT的TCP的协议帧;接下的章节,笔者详细的给大家分享了MQTT连接请求和连接应答包两种MQTT消息类型的格式,具体是如何定义的,也分析了每个字节对应的含义;希望能够给读者抛砖引玉石,举一反三,这样大家就会在以后的工作和学习过程中,对MQTT协议的本质有一个非常深入的理解。

最后,提一下,如果读者对MQTT的协议的代码的实现感兴趣的话,可以参考Eclipse Paho MQTT jar包实现。代码写的非常的好,如果读者看过了这篇文章的话,我相信,读者肯定就会非常容易看懂其代码的实现原理,对于研究开源软件的实现也是一个好的例子,而且其代码的实现,写的非常的简洁,代码量也不太,特别适合初学者和进阶者学习和研究。

谢谢大家把文章读完,如果大家感觉需要进一步补充什么,请进一步与笔者联系,希望这篇文章能给大家带来一点启发和收获。

彩蛋

重磅 Chat 分享:

《高效学习,快速变现:不走弯路的五大学习策略》

分享人:
一名会在 B 站直播写代码,会玩杂耍球、弹 Ukulele、极限健身、跑步、写段子、画画、翻译、写作、演讲、培训的程序员。喜欢用编程实现自己的想法,在 Android 市场上赚过钱,有多次创业经历。擅长学习,习惯养成,时间管理。身体力行地影响他人做出积极的改变!目前就职于 ThoughtWorks,致力于传播快乐高效的编程理念。业余创立软件匠艺社区 CodingStyle.cn,组织超过30场技术活动。

Chat简介:
说到学习呀,真是头大哟:碎片化,没有较长的连续时间来学习难专注,捧起书,手机却在召唤:来呀,快活呀~ 反正有,大把时光~做不到,看了很多书,生活中却做不到然并卵,学了方法和工具,找不到使用场景效率低,学习速度跟不上知识产生的速度记不牢,学习速度赶不上遗忘速度在这个知识泛滥、跨界竞争的年代,学习能力才是核心竞争力。你想想,过去一周,有没有哪一件工作是不需要学习就能完成的?尽管如此重要,大部分人却没研究过学习这件事,以为上下班路上打开「得到」听本书,就是碎片时间终身学习者了。

我是程序员,咨询师,培训师,这几个角色都要求我必须学得又快又好。本场 Chat 将分析学习的「趋势,原则,策略」,帮你站在更高的视角看待学习,从「内容,动机,交互,收益,资源」五方面制定策略,解决学习痛点,助你成为高效学习者!

想要免费参与本场 Chat ?很简单,「GitChat技术杂谈」公众号后台回复「高效学习」

这里写图片描述

2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:114062次
    • 积分:1932
    • 等级:
    • 排名:千里之外
    • 原创:70篇
    • 转载:0篇
    • 译文:0篇
    • 评论:155条
    文章存档
    最新评论