MQTT 协议入门及mosquitto示例

简介

MQTT 全称为 Message Queuing Telemetry Transport(消息队列遥测传输)是一种基于发布/订阅范式的“轻量级”消息协议,由 IBM 发布。

  • MQTT 可以被解释为一种低开销,低带宽占用的即时通讯协议,可以用极少的代码和带宽的为连接远程设备提供实时可靠的消息服务,它适用于硬件性能低下的远程设备以及网络状况糟糕的环境下,因此 MQTT 协议在 IoT(Internet of things,物联网),小型设备应用,移动应用等方面有较广泛的应用。
  • IoT 设备要运作,就必须连接到互联网,设备才能相互协作,以及与后端服务协同工作。而互联网的基础网络协议是 TCP/IP,MQTT 协议是基于 TCP/IP 协议栈而构建的,因此它已经慢慢的已经成为了 IoT 通讯的标准。

在简介完 MQTT 协议后,作者将从其一些基本特点和基本概念为两部分,介绍 MQTT 协议。

基本特点

1、MQTT是一种发布/订阅传输协议,基本原理和实现如下;

 

MQTT 协议提供一对多的消息发布,可以解除应用程序耦合,信息冗余小。该协议需要客户端和服务端,而协议中主要有三种身份:发布者(Publisher)、代理(Broker,服务器)、订阅者(Subscriber)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,而消息发布者可以同时是订阅者,实现了生产者与消费者的脱耦。

2、使用 TCP/IP 提供网络连接,提供有序、无损、双向连接;

MQTT 是一种连接协议,它指定了如何组织数据字节并通过 TCP/IP 网络传输它们。设备联网,也需要连接到互联网中,在大万维的世界中,TCP 如同汽车,有轮子就能用来运输数据,MQTT 就像是交通规则。在网络模型中,TCP是传输层协议,而 MQTT是在应用层,在 TCP 的上层,因此MQTT 也是基于这个而构建的,提高了可靠性。

3、对负载内容屏蔽的消息传输;

可以对消息订阅者所接受到的内容有所屏蔽。

  • 4、具体有三种消息发布的服务质量:

    • 至多一次,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
    • 至少一次,确保消息到达,但消息重复可能会发生。
    • 只有一次,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。
  • 5、小型传输,开销小,固定长度的头部是 2 字节,协议交换最小化,以降低网络流量;

  • 整体上协议可拆分为:固定头部+可变头部+消息体,这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集"。

  • 6、使用Last Will和Testament特性通知有关各方客户端异常中断的机制;

    Last Will:用于通知同一主题下的其他设备发送遗言的设备已经断开了连接。

    Testament:功能类似于Last Will。

  • 基本概念

  • 1、MQTT 客户端

  • 一个使用 MQTT 协议的设备、应用程序等,它总是建立到服务器的网络连接。

  • 可以发布信息,其他客户端可以订阅该信息
  • 订阅其它客户端发布的消息
  • 退订或删除应用程序的消息
  • 断开与服务器连接
  • 2、MQTT 服务器

  • MQTT 服务器以称为 Broker(消息代理),是一个应用程序或一台设备。它是位于消息发布者 和订阅者之间。

  • 接受来自客户端的网络连接
  • 接受客户端发布的应用信息
  • 处理来自客户端的订阅和退订请求
  • 向订阅的客户转发应用程序消息

3、主题(Topic)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。

  • 1️⃣、要订阅的主题。一个主题可以有多个级别,级别之间用斜杠字符分隔。例如/worldemq/emqtt/emqx 是有效的主题。

  • 2️⃣、订阅者的Topic name支持通配符#和+ :

    • # 支持一个主题内任意级别话题
    • +只匹配一个主题级别的通配符
  • 3️⃣、客户端成功订阅某个主题后,代理会返回一条 SUBACK 消息,其中包含一个或多个 returnCode 参数。

  • 4、主题筛选器(Topic Filter)

  • 一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。

  • 5、QoS(消息传递的服务质量水平)

  • 服务质量,标志表明此主题范围内的消息传送到客户端所需的一致程度。

  • 值 0:不可靠,消息基本上仅传送一次,如果当时客户端不可用,则会丢失该消息。
  • 值 1:消息应传送至少 1 次。
  • 值 2:消息仅传送一次。

6、会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。

7、订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。

  • 客户端在成功建立TCP连接之后,发送CONNECT消息,在得到服务器端授权允许建立彼此连接的CONNACK消息之后,客户端会发送SUBSCRIBE消息,订阅感兴趣的Topic主题列表(至少一个主题)
  • 订阅的主题名称采用UTF-8编码,然后紧跟着对应的QoS值

8、发布(publish)

控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息,MQTT 客户端发送消息请求,发送完成后返回应用程序线程。

  • 比如安卓的推送服务,还有一些即时通信软件如微信等也是采用的推送技术。

9、负载(Payload)

消息订阅者所具体接收的内容

简单示例

MQTT 协议主要是根据以下情况设计的:

  • M2M(Machine to Machine),机器或设备间端到端通信,比如传感器之间的数据通讯。
  • 设备(Machine)中,例如传感器,硬件能力很弱,协议要考虑尽量小的资源消耗,比如计算能力和存储等。

根据 MQTT 的基础了解后并结合简单的架构,在这里做一个简单的示例图,可以更直观的理解MQTT协议的通信模型。MQTT Broker 就选择 EMQ 作为示范。比如有1个温度传感器(1个Machine),1个移动设备,1个电脑,一个服务器(3个Machine),都可以得到或者显示温度传感器的温度值,需要先通过 MQTT 协议subscribe(订阅)一个比如叫 temperature 的 topic(主题)如下:

图中移动设备,服务器,电脑需要先通过 EMQ subscribe 一个叫 temperature 的 topic,当温度传感器 publish 温度数据,三个设备就可以收到了。

  • 进一步了解MQTT 3

    MQTT 3 (当前版本3.1.1)是目前使用的最为广泛的MQTT协议标准。尽管MQTT5标准已经发布,并且带来了一些令人振奋的新特性,但是在整个应用场景上,从后台服务到消息中间件再到客户端SDK等环节上的产品升级并没有都完成,再加上既有部署的维护,业界从版本3到5的过渡可能会持续相当长一段时间,所以,对于刚加入物联网行业的生力军来说,现在来学习MQTT 3依然是一件很有意义的事情。

    MQTT协议的工作方式

    前面简介中讲到,在一个QMTT协议中有三个角色会参与到整个通信过程,发布者(publisher)、代理(broker)和订阅者(subscriber)。有别于传统的客户端/服务器通讯协议,MQTT协议并不是端到端的,消息传递通过代理,包括会话(session)也不是建立在发布者和订阅者之间,而是建立在端和代理之间。代理解除了发布者和订阅者之间的耦合。

    除了发布者和订阅者之间传递普通消息,代理还可以为发布者处理保留消息和遗愿消息,并可以更改服务质量(QoS)等级。

    MQTT控制报文

    MQTT协议工作在TCP之上,端和代理之间通过交换预先定义的控制报文来完成通信。MQTT报文有3个部分组成,并按下表顺序出现:

    固定报头(fixed header)可变报头(variable header)荷载(payload)
    所有报文都包含部分报文包含部分报文包含

    所有的MQTT控制报文都有一个固定报头,其格式如下:


  • 协议版本3定义了14种MQTT报文,用于建立/断开连接、发布消息、订阅消息和维护连接。固定报头的第一字节的4-7位的值指定了报文类型,其取值如下表。0和15为系统保留值;0-3位为标志位,依照报文类型有不同的含义,事实上,除了PUBLISH报文以外,其他报文的标志位均为系统保留。如果收到报文的标志位无效,代理应断开连接。

    报文类型描述
    CONNECT1客户端向代理发起连接请求
    CONNACK2连接确认
    PUBLISH3发布消息
    PUBACK4发布确认
    PUBREC5发布收到(QoS2)
    PUBREL6发布释放(QoS2)
    PUBCOMP7发布完成(QoS2)
    SUBSCRIBE8客户端向代理发起订阅请求
    SUBACK9订阅确认
    UNSUBSCRIBE10取消订阅
    UNSUBACK11取消订阅确认
    PINGREQ12PING请求
    PINGRESP13PING响应
    DISCONNECT14断开连接

    固定报头的第二字节起表示报文的剩余长度。最大4个字节,每字节可以编码至127,并含有一位继续位,如继续位非0,则下一字节依然为剩余长度。由此,理论上一个控制报文最长可以到256MB

    一些报文在固定报头和荷载之间可以有一个可变报头。可变报头的内容根据报文类型不同而不同。最常见的可变报头是报文标识符(PacketIdentifier)。

    一些报文可以在最后携带一个荷载。不同的报文可以无荷载,可选荷载,或必须带有荷载。

    限于篇幅,在这里我们仅以CONNECT和CONNACK为例解释一下MQTT报文的构成和报文响应行为。其他报文请查阅MQTT标准文档。

    CONNECT报文

    限于篇幅,在这里我们仅以CONNECT为例解释一下MQTT报文的构成。其他报文请查阅MQTT标准文档。
    CONNECT是客户端连接到代理的第一个报文,如果在连接已经存在,代理收到该报文将会断开现有连接。

    CONNECT报文的固定报头

  •  

     

    CONNECT报文的可变报头

    CONNECT报文的可变报头由4部分组成:

  • 协议名。协议名是UTF-8编码的大写的MQTT。
  • 协议级别。MQTT 3.1.1的协议级别为4.
  • 连接标志位。定义连接行为的参数。见下表。
  • Keep Alive。2字节,客户端和代理之间的无活动时间超过该值后,应关闭连接。如果该值置0表示客户端不要求代理启用KEEPALIVE功能。

连接标志位:

76543210
 用户名密码保留遗愿遗愿QoS遗愿QoS遗愿清除会话保留(0)

清除会话标志位
这个标志位定义了如何处理会话状态。如果设置为0,客户端和代理可以恢复上一次连接时的会话状态,如果上一次连接的会话状态不存在,代理将会为客户端建立一个新的会话。如果该位设置为1,则双方将清除掉上一次连接的会话状态并建立一个新的会话。

遗愿标志位
如果遗愿标志为1,则遗愿消息会被存储在代理上,当连接关闭时,代理将发布这个消息,除非在客户端断开连接时把遗愿消息清除了。

遗愿QoS标志位
指定了遗愿消息的服务质量等级。

保留遗愿消息标志位
指定在发布遗愿消息的时候,是否把该消息作为保留消息存储在代理。

用户名标志位
如果设置为1,则用户名必须出现在荷载中,反之,用户名不允许出现在荷载中。

密码标志位
如果该位为1,则密码必须出现在荷载中;如果该位为0,则密码不允许出现在荷载中。如果用户名标志位为0,则该位必须也为0。

CONNECT报文的荷载

CONNECT报文的荷载由一个或者多个字段组成,这些字段是否出现由可变报头中的标志位决定。字段总是以长度开始。字段出现的顺序必须是:客户端标识符,遗愿主题,遗愿消息,用户名,密码。

CONNECT报文的响应

在代理在为MQTT协议开放的端口上接收到TCP连接请求并建立连接后应该会收到CONNECT报文,如果在一定时间内代理没有收到CONNECT报文,则应该关闭这个TCP连接。
在收到CONNECT报文后,代理应该检查报文格式是否符合协议标准。如果不符合协议标准,代理应关闭连接,且不发送CONNACK报文给客户端。
代理可以检查CONNECT报文的内容并执行响应的认证和鉴权。如果这些检查没有通过,代理应该向客户端发送一个带有非0返回码的CONNACK报文。

CONNACK报文

CONNACK是代理用来响应客户端CONNECT的报文。代理向客户端发送的第一个报文必须是CONNACT。CONNACK有一个固定报头,一个可变报头,但是不带有荷载。

CONNACK的固定报头

CONNACT报文只有固定报头和一个2字节的可变报头,所以它的剩余长度总是2。

CONNACK报文的可变报头

CONNACK报文的可变报头为定长2字节。第一字节的0位表示是否有会话存在。如果代理上已经有请求连接的客户端的会话,且连接请求的清除会话标识为0,则该位为1,否则该位为0。客户端可以根据这一位的值采取响应行为,比如(重新)订阅主题等。

CONNACK报文的可变报头的第二字节为返回码。如果CONNECT请求的格式正确,但是代理依然不能允许客户端连接,则返回码为一个非零值。如果连接成功,则返回0。

返回码的定义:

返回码含义
0成功,连接请求被接受。
1拒绝连接,不可接受的协议版本。
2拒绝连接,不被允许的身份识别符(Client Identifier)。
3拒绝连接,服务器不可用。
4拒绝连接,无效的用户名和密码。
5拒绝连接,客户端无授权。
6-255系统保留。

客户端接受到代理的CONNACK的返回码为0,则连接建立完成,双方可以开始通信。

清除会话、保留消息和QoS的组合

清除会话、保留消息等概念,在传统的客户端/服务器方式的通信中不一定会出现,这些概念有时候不太容易理解,特别是当他们被组合起来用的时候。

下面的表格汇总了当一个客户端连接上来时,它能收到消息的各种情况。

清除会话位保留位订阅QoS发布QoS可收到的消息
YN00N
YN01N
YN10N
YN11N
NN00N
NN01N
NN10N
NN11Y,会话全部消息
YY00Y,最后一条消息
YY01Y,最后一条消息
YY10Y,最后一条消息
YY11Y,最后一条消息
NY00Y,最后一条消息
NY01Y,最后一条消息
NY10Y,最后一条消息
NY11Y,会话全部消息

mosquitto示例

市面上有相当多的高质量MQTT代理,其中mosquitto是一个开源的轻量级的C实现,完全兼容了MQTT 3.1和MQTT 3.1.1。下面我们就以mosquitto为例演示一下MQTT的使用。

环境是百度开放的云服务器以及Ubuntu 14.04.1 LTS,简单起见MQTT代理和客户端都安装在同一台云服务器上了。

方法一

首先SSH到云服务器,安装mosquitto以及搭配的客户端:

apt-get install mosquitto
apt-get install mosquitto-clients
现在在云端模拟云服务,订阅某办公楼5层的温度作为主题:

mosquitto_sub -d -t 'floor-5/temperature'
Received CONNACK
Received SUBACK
Subscribed (mid: 1): 0

然后另外打开一个SSH连接,模拟温度计发送温度消息:

mosquitto_pub -d -t 'floor-5/temperature' -m '15'
Received CONNACK
Sending PUBLISH (d0, q0, r0, m1, 'floor-5/temperature', ... (2 bytes))

此时回到第一个SSH客户端可以看到信息已经接收到了,之后便是心跳消息:

Received PUBLISH (d0, q0, r0, m0, 'floor-5/temperature', ... (2 bytes))
15
Sending PINGREQ
Received PINGRESP

需要注意的是mosquitto客户端默认使用QoS 0,下面我们使用QoS 2订阅这个主题:

mosquitto_sub -d -q 2 -t 'floor-5/temperature'
Received CONNACK
Received SUBACK
Subscribed (mid: 1): 2

切换到另外SSH连接然后在这个主题里面发送温度消息:

mosquitto_pub -d -q 2 -t 'floor-5/temperature' -m '15'
Received CONNACK
Sending PUBLISH (d0, q2, r0, m1, 'floor-5/temperature', ... (2 bytes))
Received PUBREC (Mid: 1)
Sending PUBREL (Mid: 1)
Received PUBCOMP (Mid: 1)

此时回到第一个SSH客户端可以看到信息已经接收到了,以及相应的多次握手消息:

Received PUBLISH (d0, q2, r0, m1, 'floor-5/temperature', ... (2 bytes))
Sending PUBREC (Mid: 1)
Received PUBREL (Mid: 1)
15
Sending PUBCOMP (Mid: 1)

方法二

下载

对于Ubuntu系统,可以使用sudo apt-get 来安装mosquitto,但是这种方法虽然简单,但是对于配置文件的修改和管理比较麻烦,配置文件需要自己写好然后启动时载入,因此不太推荐。

本人更推荐的是第二种方法,也就是本文重点要讲的,下载tar.gz安装包,进行安装。

首先,到mosquitto的官方网站(http://mosquitto.org/files/source/)下载,tar.gz安装包。本人使用的是1.4.15版本。如果你使用的是服务器版本,你可以使用wget  http://mosquitto.org/files/source/mosquitto-1.4.15.tar.gz 这个命令直接下载。

wget  http://mosquitto.org/files/source/mosquitto-1.4.15.tar.gz

解压

tar -xvf mosquitto-1.4.15.tar.gz

安装

cd mosquitto-1.4.15

make

make install

1)提示:缺少ssl.h

解决办法:使用 sudo apt-get install libssl-dev

2)提示:缺少ares.h

解决办法:使用 sudo apt-get install libc-ares-dev

打开config.mk文件。

把WITH_SRV:=yes 改成WITH_SRV:=no 就可以编译了

3)提示:没有UUID

解决办法: 使用  sudo apt-get install uuid-dev

首先要注意的是,安装完后的mosquitto的配置文件在根目录下的etc下的mosquitto,使用 cd /etc/mosquitto 就可以找到相应的位置

使用  cp mosquitto.conf.example mosquitto.conf 命令,复制一份新的conf配置文件

入门的话,首先修改这几个地方:

1) user :默认是mosquitto,要更换成你当前的用户

2)port:打开1883端口,如果使用ssl或tls打开8883端口

3)protocol:mqtt

修改好后保存,回到安装目录

启动

使用, mosquitto -c /etc/mosquitto/mosquitto.conf  开启服务器。 

在新建一个端口,使用, netstat -tunlp|grep 1883 查看端口是否被监听

使用 mosquitto_sub -t 'topic' 请求一个订阅 

再新建一个终端,使用 mosquitto_pub -t 'topic' -m 'msg1'发布一个订阅,这时候即可查看到订阅消息

【-t】指定主题

    【-m】指定消息内容

     【-f】 是可以发送文件,通过-f传递的文件上限默认是256M。逻辑中有对文件大小的判断,超过256M的文件则不传。

【测试说明】

    测试环境:ubuntu 14.04 虚拟机

    在本例中,发布者、代理和订阅者均为localhsot,但是在实际的情况下三种并不是同一个设备,在mosquitto中可通过-h(--host)设置主机名称(hostname)。

    如果不是在同一个设备,可通过-h指定代理的IP。

(另外还有一个mosquitto_passwd,用于管理密码,应该是关闭SSL的原因)

为了实现这个简单的测试案例,需要在linux中打开三个控制台,分别代表代理服务器、发布者和订阅者

#cd /usr/local/bin/
cmake             ctest             curve_keygen      mosquitto_sub     
cpack             curl              mosquitto_passwd  openssl           
c_rehash          curl-config       mosquitto_pub

# ls -l /usr/local/sbin/
total 152
-rwxr-xr-x 1 root root 155320 Dec  9 07:47 mosquitto
root@intest:/home/intest# 

mosquitto -c /etc/mosquitto/mosquitto.conf

1607473014: mosquitto version 1.4.15 (build date 2020-12-09 07:47:19+0800) starting
1607473014: Config loaded from /etc/mosquitto/mosquitto.conf.
1607473014: Opening ipv4 listen socket on port 1883.
1607473014: Opening ipv6 listen socket on port 1883.
1607473014: Warning: Mosquitto should not be run as root/administrator.
1607473069: New connection from ::1 on port 1883.
1607473069: New client connected from ::1 as mosqsub|14968-intest (c1, k60).
1607473082: New connection from ::1 on port 1883.
1607473082: New client connected from ::1 as mosqpub|14970-intest (c1, k60).
1607473082: Client mosqpub|14970-intest disconnected.
1607473094: New connection from ::1 on port 1883.
1607473094: New client connected from ::1 as mosqpub|14971-intest (c1, k60).
1607473094: Client mosqpub|14971-intest disconnected.
1607473112: New connection from ::1 on port 1883.
1607473112: New client connected from ::1 as mosqpub|14972-intest (c1, k60).
1607473112: Client mosqpub|14972-intest disconnected.

1607473140: New connection from ::1 on port 1883.
1607473140: New client connected from ::1 as mosqpub|14983-intest (c1, k60).
1607473140: Client mosqpub|14983-intest disconnected.
1607474265: Socket error on client mosqsub|14968-intest, disconnecting.
^C1607474267: mosquitto version 1.4.15 terminating

mosquitto_pub -t 'topic' -m 'msg1'
mosquitto_pub -t 'topic' -m '11111'
mosquitto_pub -t 'topic' -m '22222222'
mosquitto_pub -t 'topic' -m '0000000'

mosquitto_sub -t 'topic'
msg1
11111
22222222
0000000
^C

 

项目示例

mosquitto部分,嵌入音箱端及手机微信端,可实现广域网内手机微信对音箱的控制

服务器部分

mosquitto服务器部分。音箱端与手机端pub和sub都是通过服务器进行消息传递。IP地址为"120.76.30.18";

音箱部分

进入网络模式下,则需要开启一个线程,用来开启mosquitto subscribe 订阅一个主体。一个音箱实体对应一个主体。其他客户端可通过这个主题,来与音箱端实现链接。

手机微信部分

手机客户端如何得知音箱端这个主题,方法一,是微信客户端有提供对应开发包,局域网设备发现,但是这个功能依赖微信端,尚不稳定,最后并没采用;

方法二:将主题生成二维码,贴在音箱上。目前采用方法二。

而协议约定客户端名字,可以是b_MacAddress。

手机微信部分是客户端IOS/Android同仁负责,这里不作介绍。

下面主要介绍音箱部分移植mosquitto进行应用开发的内容:

开启订阅主题线程

pthread_create(&mosquitto_client_thread_id, NULL, mosquitto_sub_start, NULL)

mosquitto_sub_start 是利用mosquitto sub的源码加以修改。

核心内容是,

两个回调函数的注册

mosquitto_connect_callback_set(mosq, my_connect_callback);    
mosquitto_message_callback_set(mosq, my_message_callback);

其中my_connect_callback是mosquitto_loop_forever这个一直存在的循环函数在收到服务器传递回来CONNACK信息(见_mosquitto_handle_connack),则会被触发,用来呼叫mosquitto_subscribe完成订阅。

其中my_message_callback则是用于音箱端应用程序的开发,用来接收手机微信端传递过来的内容。与手机客户端定义交互的协议,用于控制音箱的推歌、上下首切换、音源切换、网络模式的切换、音量大小的改变等等。

 交互的内容采用json包,因此需要进行json包的解析和封装。

正确完成音箱端的订阅,

注意设定好:

cfg.port = 1883;

cfg.topics[cfg.topic_count-1] = strdup(deviceName);

cfg.host = strdup(serverIp);  //服务器地址

这里初始化了一个属于音箱端全局的mosq,可用它关联pub和sub;(pub和sub共用一个全局mosq即可,并不需要分别申请一个mosq)

音箱端部分,需要pub消息给手机微信端。采用mosquitto_pub_start发送消息。这个不同于sub是一个一直存在的线程,pub属于一次性,消息传递完毕即结束。

mosquitto_pub_start 中 全局变量pub_topic ,在mosquitto_sub_start里面有初始化。

  • 9
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值