JMS

JMS即Java消息服务(JavaMessage Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。

 

使用JMS的原因:

  1、松散耦合但是高内聚。使用消息服务的客户机不需要实现通用接口,不需要了解对方。消息服务提供了标准接口。

  2、不直接通信。客户机不直接对话,而通过中间媒介,消息服务扮演 缓冲区,并提供 安全控制。

  3、保证消息传递。JMS的提供者保持消息持久,直到客户机接受为止。

  4、异步通信。

  5、一对多、多对多和多对一通信。

Message Queue把请求的压力保存一下,逐渐释放出来,让处理者按照自己的节奏来处理。
Message Queue引入一下新的结点,让系统的可靠性会受Message Queue结点的影响。
Message Queue是异步单向的消息。发送消息设计成是不需要等待消息处理的完

简单说来,需要进程之间异步通信的时候即可使用。一般可以用于分布式协作。一种典型的方式就是实现Pub/Sub模式,即发布与订阅可以异步进行。

我们项目中的一个应用场景:应用程序服务器需要产生多个Job,然后将Job放入数据库,然后由一个Worker在后台执行这些Job,问题在于 Worker执行一个Job很耗时,导致大量Job堆积。需求是让大量Job能够在短时间内执行完。很容易想到的解决方案是让多个Woker并发运行,这就需要一个分发机制,那么Message Queue就是一个很好的平台,应用程序进程为Pub,Worker进程为Sub,他们之间通过Message很方便的通信,同时支持多个Sub并发运行,以缩短所有Job的执行时间。

PRC的特点
同步调用,对于要等待返回结果/处理结果的场景,RPC是可以非常自然直觉的使用方式。由于等待结果,Consumer(Client)会有线程消耗。
RPC也可以是异常调用。

如果以异步RPC的方式使用,Consumer(Client)线程消耗可以去掉。但不能做到像消息一样暂存消息/请求,压力会直接传导到服务 Provider。随着业务增长,有的处理端处理量会成为瓶颈,会进行同步调用到异步消息的改造。


消息是JMS 中的一种类型对象,由两部分组成:报头和消息主体。报头由路由信息以及有关该消息的元数据组成。消息主体则携带着应用程序的数据或有效负载。

JMS有以下元素组成。

  JMS提供者

  连接面向消息中间件的,JMS接口的一个实现。提供者可以是Java平台的JMS实现,也可以是非Java平台的面向消息中间件的适配器。

  JMS客户

  生产或消费基于消息的Java的应用程序或对象。

  JMS生产者

  创建并发送消息的JMS客户。

  JMS消费者

  接收消息的JMS客户。

  JMS消息

  包括可以在JMS客户之间传递的数据的对象

  JMS队列

  一个容纳那些被发送的等待阅读的消息的区域。队列暗示,这些消息将按照顺序发送。一旦一个消息被阅读,该消息将被从队列中移走。

  JMS主题

  一种支持发送消息给多个订阅者的机制。

 

JMS提供者实现

Apache ActiveMQ

JBoss 社区所研发的HornetQ

IBM的WebSphereMQ

BEA的BEAWebLogic Server JMS

 

Java消息服务应用程序结构支持两种模型:

  点对点或队列模型

  发布者/订阅者模型

  在点对点或队列模型下,一个生产者向一个特定的队列发布消息,一个消费者从该队列中读取消息。这里,生产者知道消费者的队列,并直接将消息发送到消费者的队列。这种模式被概括为:

  只有一个消费者将获得消息

  生产者不需要在接收者消费该消息期间处于运行状态,接收者也同样不需要在消息发送时处于运行状态。

  每一个成功处理的消息都由接收者签收

  发布者/订阅者模型支持向一个特定的消息主题发布消息。0或多个订阅者可能对接收来自特定消息主题的消息感兴趣。在这种模型下,发布者和订阅者彼此不知道对方。这种模式好比是匿名公告板。这种模式被概括为:

  多个消费者可以获得消息

  在发布者和订阅者之间存在时间依赖性。发布者需要建立一个订阅(subscription),以便客户能够购订阅。订阅者必须保持持续的活动状态以接收消息,除非订阅者建立了持久的订阅。在那种情况下,在订阅者未连接时发布的消息将在订阅者重新连接时重新发布。

 

JMS现在有两种传递消息的方式。

标记为NON_PERSISTENT的消息最多投递一次,而标记为PERSISTENT的消息将使用暂存后再转送的机理投递。如果一个JMS服务离线,那么持久性消息不会丢失但是得等到这个服务恢复联机时才会被传递。所以默认的消息传递方式是非持久性的。即使使用非持久性消息可能降低内务和需要的存储器,并且这种传递方式只有当你不需要接收所有的消息时才使用。

  虽然JMS规范并不需要JMS供应商实现消息的优先级路线,但是它需要递送加快的消息优先于普通级别的消息。JMS定义了从0到9的优先级路线级别,0是最低的优先级而9则是最高的。更特殊的是0到4是正常优先级的变化幅度,而5到9是加快的优先级的变化幅度。

举例来说:topicPublisher.publish (message, DeliveryMode.PERSISTENT, 8, 10000); //Pub-Sub 或queueSender.send(message,DeliveryMode.PERSISTENT, 8, 10000);//P2P 这个代码片断,有两种消息模型,映射递送方式是持久的,优先级为加快型,生存周期是10000(以毫秒度量)。如果生存周期设置为零,这则消息将永远不会过期。当消息需要时间限制否则将使其无效时,设置生存周期是有用的。

  JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。

  ·StreamMessage -- Java原始值的数据流

  ·MapMessage--一套名称-值对

  ·TextMessage--一个字符串对象

  ·ObjectMessage--一个序列化的Java对象

  ·BytesMessage--一个未解释字节的数据流

 

ConnectionFactory 接口(连接工厂)

  用户用来创建到JMS提供者的连接的被管对象。JMS客户通过可移植的接口访问连接,这样当下层的实现改变时,代码不需要进行修改。 管理员在JNDI名字空间中配置连接工厂,这样,JMS客户才能够查找到它们。根据消息类型的不同,用户将使用队列连接工厂,或者主题连接工厂。

Connection 接口(连接)

  连接代表了应用程序和消息服务器之间的通信链路。在获得了连接工厂后,就可以创建一个与JMS提供者的连接。根据不同的连接类型,连接允许用户创建会话,以发送和接收队列和主题到目标。

Destination 接口(目标)

  目标是一个包装了消息目标标识符的被管对象,消息目标是指消息发布和接收的地点,或者是队列,或者是主题。JMS管理员创建这些对象,然后用户通过JNDI发现它们。和连接工厂一样,管理员可以创建两种类型的目标,点对点模型的队列,以及发布者/订阅者模型的主题。

MessageConsumer 接口(消息消费者)

  由会话创建的对象,用于接收发送到目标的消息。消费者可以同步地(阻塞模式),或异步(非阻塞)接收队列和主题类型的消息。

MessageProducer 接口(消息生产者)

  由会话创建的对象,用于发送消息到目标。用户可以创建某个目标的发送者,也可以创建一个通用的发送者,在发送消息时指定目标。

Message 接口(消息)

  是在消费者和生产者之间传送的对象,也就是说从一个应用程序传送到另一个应用程序。一个消息有三个主要部分:

  消息头(必须):包含用于识别和为消息寻找路由的操作设置。

  一组消息属性(可选):包含额外的属性,支持其他提供者和用户的兼容。可以创建定制的字段和过滤器(消息选择器)。

  一个消息体(可选):允许用户创建五种类型的消息(文本消息,映射消息,字节消息,流消息和对象消息)。

  消息接口非常灵活,并提供了许多方式来定制消息的内容。

Session 接口(会话)

  表示一个单线程的上下文,用于发送和接收消息。由于会话是单线程的,所以消息是连续的,就是说消息是按照发送的顺序一个一个接收的。会话的好处是它支持事务。如果用户选择了事务支持,会话上下文将保存一组消息,直到事务被提交才发送这些消息。在提交事务之前,用户可以使用回滚操作取消这些消息。一个会话允许用户创建消息生产者来发送消息,创建消息消费者来接收消息。

在非事务Session中,JMS消息传递的方式有三种:

Session.AUTO_ACKNOWLEDGE :当客户机调用的receive方法成功返回,或当MessageListenser成功处理了消息,session将会自动接收消息的收条。

Session.CLIENT_ACKNOWLEDGE :客户机通过调用消息的acknowledge方法来接收消息。接收发生在session层。接收到一个被消费的消息时,将自动接收该session已经 消费的所有消息。例如:如果消息的消费者消费了10条消息,然后接收15个被传递的消息,则前面的10个消息的收据都会在这15个消息中被接收。

Session.DUPS_ACKNOWLEDGE :指示session缓慢接收消息。

 

一个JMS应用是几个JMS客户端交换消息,开发JMS客户端应用由以下几步构成:

      1) 用JNDI 得到ConnectionFactory对象;

      2) 用ConnectionFactory创建Connection对象;

      3) 用Connection对象创建一个或多个JMSSession;

      4) 用JNDI 得到目标队列或主题对象,即Destination对象;

      5) 用Session和Destination创建MessageProducer和MessageConsumer;

      6) 通知Connection开始传递消息。

JMS Parent

PTP Domain

Pub/Sub Domain

ConnectionFactory

QueueConnectionFactory

TopicConnectionFactory

Connection

QueueConnection

TopicConnection

Destination

Queue

Topic

Session

QueueSession

TopicSession

MessageProducer

QueueSender

TopicPublisher

MessageConsumer

QueueReceiver

TopicSubscriber

     

消息生产者程序

public class Sender{

public void send()

                InitialContextinitContext = new InitialContext();

                ConnectionFactoryfactory = (ConnectionFactory) initContext.lookup(factoryName);

                Destinationdestination = (Destination) initContext.lookup(destinationName);

                initContext.close();

                //CreateJMS objects

                Connectionconnection = factory.createConnection();

                Sessionsession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

                MessageProducersender = session.createProducer(destination);

                //Sendmessages

                StringmessageText = null;

                while(true) {

                    messageText = reader.readLine();

                    if ("quit".equals(messageText))

                        break;

                    TextMessage message =session.createTextMessage(messageText);

                    sender.send(message);

                }

}

}

消息消费者程序

 

public class Receiver implements MessageListener{

                publicvoid receive() {

     BufferedReader reader = new BufferedReader(newInputStreamReader(System.in));

      try {

          StringfactoryName = reader.readLine();

          StringdestinationName = reader.readLine();

         reader.close();

         InitialContext initContext = new InitialContext();

         ConnectionFactory factory =(ConnectionFactory)initContext.lookup(factoryName);

         Destination destination = (Destination) initContext.lookup(destinationName);

         initContext.close();

          //CreateJMS objects

         Connection connection = factory.createConnection();

          Sessionsession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

         MessageConsumer receiver = session.createConsumer(destination);

         receiver.setMessageListener(this);

         connection.start();

          //Waitfor stop

          while(!stop) {

             Thread.sleep(1000);

          }

      } catch(Exception e) {

 

      }

  }

 

  public voidonMessage(Message message) {

      try {

          StringmsgText = ((TextMessage) message).getText();

         System.out.println(msgText);

          if("stop".equals(msgText))

              stop= true;

      } catch(JMSException e) {

      }

  }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

WebSphere MQ 安装

使用以下步骤来安装队列管理器和队列:

创建名为venus.queue.manager 的缺省队列管理器。在窗口的命令提示符下,输入以下命令:

crtmqm -q venus.queue.manager

启动队列管理器。输入以下命令:

strmqm venus.queue.manager

启用MQSC 命令。输入以下命令:

runmqsc venus.queue.manager

定义名为ORANGE.QUEUE 的本地队列。输入以下命令:

define qlocal (orange.queue)

MQSC 中的任何小写字母都将自动转换成大写,除非用单引号将它们括起来。这意味着如果用名称orange.queue 创建了队列,则记住在MQSC 以外的其它命令中必须使用ORANGE.QUEUE。

停止MQSC。输入以下命令:End

现在已经定义了以下对象:

名为venus.queue.manager 的缺省队列管理器

名为ORANGE.QUEUE 的队列

 

 

 

MQ中,队列分为很多种类型,其中包括:本地队列、远程队列、模板队列、动态队列、别名队列等。

 

本地队列又分为普通本地队列和传输队列,普通本地队列是应用程序通过API对其进行读写操作的队列;传输队列可以理解为存储-转发队列,比如:我们将某个消息交给MQ系统发送到远程主机,而此时网络发生故障,MQ将把消息放在传输队列中暂存,当网络恢复时,再发往远端目的地。

 

远程队列是目的队列在本地的定义,它类似一个地址指针,指向远程主机上的某个目的队列,它仅仅是个定义,不真正占用磁盘存储空间。

 

模板队列和动态队列是MQ的一个特色,它的一个典型用途是用作系统的可扩展性考虑。我们可以创建一个模板队列,当今后需要新增队列时,每打开一个模板队列,MQ便会自动生成一个动态队列,我们还可以指定该动态队列为临时队列或者是永久队列,若为临时队列我们可以在关闭它的同时将它删除,相反,若为永久队列,我们可以将它永久保留,为我所用

 

通道是MQ系统中队列管理器之间传递消息的管道,它是建立在物理的网络连接之上的一个逻辑概念,也是MQ产品的精华。

 

在MQ中,主要有三大类通道类型,即消息通道,MQI通道和Cluster通道。消息通道是用于在MQ的服务器和服务器之间传输消息的,需要强调指出的是,该通道是单向的,它又有发送(sender),接收(receive),请求者(requestor),服务者(server)等不同类型,供用户在不同情况下使用。MQI通道是MQClient和MQServer之间通讯和传输消息用的,与消息通道不同,它的传输是双向的。群集(Cluster)通道是位于同一个MQ群集内部的队列管理器之间通讯使用的。

 

MQ Server和MQ Client

MQ产品分为Server和Client 两种版本,在MQ服务器的运行环境下,有队列管理器、队列、消息通道等对象,它提供全面的消息服务;MQClient为我们提供了一个MQ应用程序的开发和运行环境,它是MQAPI的Client实现。在客户端环境下,没有队列管理器、队列等对象,它通过MQI通道与服务器之间建立通讯,并将消息从客户端发往服务器端的队列,或从Server端的队列中取得消息,它比较适合于网络条件较好或实时通讯的情况。同时要指出的是:采用MQClient并不会导致数据的丢失或不完整性。MQClient提供下列好处:适合同步处理的工作模式;减少系统负担;减少系统管理开销;减少磁盘空间要求等。

 

MQ的通讯模式

1) 点对点通讯:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。

2) 多点广播:MQ适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点(DestinationList)。可以使用一条MQ指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ将消息的一个复制版本和该系统上接收者的名单发送到目标MQ系统。目标MQ系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。

3) 发布/订阅(Publish/Subscribe)模式:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。在MQ家族产品中,MQEvent Broker是专门用于使用发布/订阅技术进行数据通讯的产品,它支持基于队列和直接基于TCP/IP两种方式的发布和订阅。

4) 群集(Cluster):为了简化点对点通讯模式中的系统配置,MQ提供Cluster(群集)的解决方案。群集类似于一个域(Domain),群集内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用群集(Cluster)通道与其它成员通讯,从而大大简化了系统配置。此外,群集中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 

 

MQ的基本配置举例

点对点的通讯方式,我们至少要建立如下MQ的对象:

在发送方A:

1) 建立队列管理器QMA:crtmqm -q QMA

2) 定义本地传输队列:define qlocal (QMB) usage (xmitq) defpsist(yes)

3) 创建远程队列:define qremote (QR.TOB) rname (LQB) rqmname (QMB) xmitq (QMB)

4) 定义发送通道:define channel (A.TO.B) chltype (sdr) conname ('IP of B') xmitq (QMB) + trptype(tcp)

在接收方B:

1) 建立队列管理器QMB:crtmqm -q QMB

2) 定义本地队列QLB:define qlocal (LQB)

3) 创建接收通道:define channel (A.TO.B) chltype (rcvr) trptype (tcp)

 

java访问MQ

1、MQQueueManager―――队列管理器访问类

常用方法:

public MQQueueManager(String queueManagerName)―――建立一个管理器实例

 

注:如果使用绑定的方式则可以直接创建一个新的队列管理器实例。但是在某些平台下这样直接创建会出错,必须采用MQClient的方式进行连接。此时需要先定义服务通道,端口,服务名等环境变量,再创建一个队列管理器实例。如:

MQEnvironment.hostname = “127.0.0.1”;

MQEnvironment.port = 50000;

MQEnvironment.channel=”SRVCHL”;  //服务器连接的通道 reciver

MQQueueManager manager = new MQQueueManager(“BOSS_ZH”);

其中hostname表示队列管理器所在的机器地址(一般在本地则填localhost或127.0.0.1)。port就是队列管理器的侦听端口。Channel定义访问的服务器通道名(需要自己在队列管理器中先定义,其方法类似与一般通道的定义,不过类型是服务器通道)

 

推荐使用MQQueueManager(StringqmName, Hashtable hashtable)构造函数,如下:

private static Hashtable properties = new Hashtable();

properties.put("hostname", " 主机名");

properties.put("port", new Integer(1414));

MQQueueManager qMgr = new MQQueueManager(qmName,properties);

可以使应用程序访问MQ服务器的特定端口。

 

public synchornized MQQueue accessQueue(StringQueueName,int openOptions)―――返回一个连接队列的实例(类名为:MQQueue)

openOptions的常用值有:

MQC.MQOO_FAIL_IF_QUIESCING―――如果队列管理器停止则返回失败

MQC.MQOO_OUTPUT――――以写方式打开队列

MQC.MQOO_INPUT_AS_Q_DEF―――以队列默认读取方式打开队列

使用的时候可以采用与操作来实现多种打开队列方式,如:

int openOptions = MQC.MQOO_FAIL_IF_QUIESCING |MQC.MQOO_OUTPUT | MQC.MQOO_INPUT_AS_Q_DEF;

表示以读、写方式打开队列

public bool isConnected()―――返回队列管理器是否在连接状态

public synchronized void disconnect()―――断开队列管理器的连接

 

2、MQQueue―――队列访问类

常用方法:

通常MQQueue实例的生成通过调用MQQueueManager的accessQueue方法类实例化。

public synchronized void get(MQMessagemessage,MQGetMessageOptions gmo)―――从队列管理器读取一条消息

通过message实例返回。MQGetMessageOptions的用法下面再详述。

public synchronized void put(MQMessage message,MQPutMessageOptionspmo)―――往队列管理器放入一条消息

public synchronized void close()―――关闭队列的连接

 

3、MQMessage―――消息操作类

常用方法:

public MQMessage()―――默认构造函数

public int getDataLength()―――返回可读取的消息的长度(以byte作为单位)

public void readFully(byte b[])―――读取消息到数组b中,长度以b的数组长度为准

一般读取消息的操作为:

MQMessage message = new MQMessage();

int length = message.getDataLength();

byte buffer[] = new byte[length];

message.readFully(buffer);

public void write(byte b[])―――把指定的字节数组写入消息

一般写消息的操作为:

byte[] buffer = “asdasdad”.getBytes();

MQMessage message = new MQMessage();

message.write(buffer);

queue.put(message….

 

4、MQGetMessageOptions―――取消息操作选项

常用方法:

public MQGetMessageOptions()―――默认构造函数

public int options―――操作选项(位操作)

public int matchOptions―――条件选项(按照某种条件获取消息)

public int waitInterval―――等待时长(单位:毫秒)仅当options选项有MQC.MQGMO_WAIT才有效

 

5、MQPutMessageOptions―――方消息操作选项

常用方法:

public MQPutMessageOptions()―――默认构造函数

public int options―――操作选项(位操作)

 

public   class   MQSample  {

public  static   void   main(java.lang.String[]   args)

{

     java.util.Hashtable   properties;

      try

      {

         MQQueueManager   qMgr=   new MQQueueManager("QM_ZHIDUI");

          int   openOptions= MQC.MQOO_INPUT_AS_Q_DEF |MQC.MQOO_OUTPUT;

          MQQueuesystem_default_local_queue= qMgr.accessQueue( "REMOTE ",  openOptions,  null,   null,   null); 

         MQMessage   hello_world=   new  MQMessage();

         hello_world.writeString( "Hello  World! ");

         MQPutMessageOptions   pmo=   new  MQPutMessageOptions(); 

         system_default_local_queue.put(hello_world,   pmo); 

         system_default_local_queue.close();

         qMgr.disconnect();

      }catch   (MQException   ex){

     }catch(Exception   e){

      }

}

}

public void open(){

                MQQueueManagerqueueManager;

                MQQueuelocalQueue;

                if (this.clientServerConf){

                    Hashtable mqProperties = new Hashtable();

                    mqProperties.put(MQC.HOST_NAME_PROPERTY,"主机名");

                    mqProperties.put(MQC.PORT_PROPERTY, newInteger(1414));

                    mqProperties.put(MQC.CHANNEL_PROPERTY,"channel");

                    mqProperties.put(MQC.TRANSPORT_PROPERTY,MQC.TRANSPORT_MQSERIES);

                    queueManager = newMQQueueManager("", mqProperties);

                }else{

                    queueManager = newMQQueueManager(this.manager);

                }

                intopenOptions = MQC.MQOO_INPUT_AS_Q_DEF | MQC.MQOO_INQUIRE;

                localQueue= this.queueManager.accessQueue("REMOTE ", openOptions);

               

                while(true){       readline();           }

}

 

public String readLine() {

                                StringretrieveMessageString = "";

    MQMessageretrievedMessage = new MQMessage();

   MQGetMessageOptions getMessageOptions = new MQGetMessageOptions(); //accept the defaults

   getMessageOptions.options = MQC.MQGMO_NO_WAIT | MQC.MQGMO_NO_SYNCPOINT |MQC.MQGMO_ACCEPT_TRUNCATED_MSG;

   retrievedMessage.format = MQC.MQFMT_STRING;

   retrievedMessage.characterSet = MQC.MQCCSI_Q_MGR;

    try {

        if(localQueue.getCurrentDepth() <= 0) {

            returnnull;

        }

       localQueue.get(retrievedMessage, getMessageOptions, maxMsgSize);

        if(retrievedMessage.characterSet == MQ_UTF8_CHARSET) {

                retrieveMessageString =retrievedMessage.readUTF();

        }

        else {

            byteretrievedMessageBytes[] = new byte[retrievedMessage.getMessageLength()];

           retrievedMessage.readFully(retrievedMessageBytes);

                retrieveMessageString = new String(retrievedMessageBytes,javaCharsetName);

        }

   }catch(Exceptione){}

}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值