理解面向消息的中间件和JMS

第二章:理解面向消息的中间件和jms

2.1企业级消息简介

企业级消息是在分布式系统之间进行传递的数据。当前有很多方法可以完成这一目的:

1.远程过程调用,例如COM、CORBA、DCE和EJB

2.事件通知、进程间通信和消息队列等,依赖于具体操作系统的数据交换方式,例如先进先出(FIFO)缓冲、消息队列、管道(pipes)、信号(signals)、套接字(sockets)等。

3.一类消息中间件,能够提供异步的、可靠的消息队列,例如:MQSeries、SonicMQ、TIBCO和Apache ActiveMQ等用于企业级应用程序集成(EAI Enterprise ApplicationInteration)的中间件

很多种消息传输的方式都能够完成应用程序间的通信,基于中间件的消息通信是本文的关注重点。基于中间件的通信方式具有很好的数据交互和传输能力,并且熟练的管理分布式系统中的数据格式化以屏蔽不同的操作系统、协议和编程语言带来的通信问题。此外,成熟的消息传输和路由机制也正不断的结合进来,这种系统被人们称为面向消息的中间件。

2.2面向消息的中间件

面向消息的中间件(MOM)可以描述为一类能够为分布式应用程序或者异构的操作系统提供松散耦合的、可靠的、可扩展的、安全的消息通信软件。MOMs在分布式计算中有着重要的地位。使用MOMs利用不同厂商提供的APIs完成应用程序和应用程序之间的通信,处理企业级领域的通信问题。

MOM在消息的发送者和消息的接收者之间提供一个消息中介的作用,这种中介提供了高层次松散耦合的企业级消息应用。MOM不仅可以用于应用程序和应用程序之间的通信,也可以应用于应用程序(终端)和主框架之间的通信。如图所示:


在更高的层次上来理解,消息就是一组由一个应用程序通过MOM发向另一个应用程序的业务信息。通过MOM发送和接收消息的应用程序被称为目的地(destinations),消息将会投递给链接或者订阅它的接收者。消息的发送者和接收者在消息发送期间无需跟MOM保持链接状态,这是松散耦合机制的根本。发送者无需对接收者做任何假设和了解,接收者也无需知道发送者的相关信息。这就是一种异步消息通信。

MOMs提供了很多企业级消息没有的特性,包括消息持久化、高延时和不可靠链接的消息加强发送、复杂消息的路由、消息事务等。消息持久化减轻了不可靠链接的处理代价,当消息的接收者出现简单错误时不会影响到消息发送者的状态。复杂消息的路由可以将一条消息发送给多个接收者,消息路由基于消息的属性和消息的内容。消息事务确保两个应用不会重复处理同一个消息。

此外,很多的市场上的MOMs支持多种链接协议。很多厂商都支持HTTP/S,multicast、SSL、TCP/IP、UDP等,很多厂商还提供对多语言的支持。

2.3JMS

JMS屏蔽了不同厂商提供的MOM APIs的异构性,为企业级消息通信提供了标准的API。JMS指在为使用Java语言进行编程并且使用面向消息中间件的程序提供一个标准的API。JMS自身不是一个MOM(面向消息的中间件),它是一个在客户端和MOMs之间提供的抽象接口,就像JDBC为数据库和客户端提供的编程接口一样。如下图所示:

 

JMS第一个版本是1998年制定的,最后一个版本是在2002年制定的JMS1.1。在标准API中JMS定义了很多的概念:

 JMS Client –一个完全使用JMS编写的发送和接收消息的客户端(很显然,这个客户端必然用Java实现)。

Non-JMSClient—使用JMS提供商提供的本地客户端API编写收发消息,而不是JMS提供的API。

JMSProducer—一个创建和发送JMS消息的客户端应用。

JMS Consumer –一个接收并处理JMS消息的客户端应用。

JMS Provider–完全使用Java对JMS接口的一个实现。

 JMS Message – JMS基础概念,JMS客户端发送和接收的消息。

 JMS Domains – 两种类型的消息,分别为点对点(PTP,point-to-point)和发布/订阅(publish/subscribe)。

 Administered Object –对JMS对象进行配置。

ConnectionFactory—客户端使用Connection Factory 创建与JMSProvider的连接。

Destination –用于收发消息的客户端

2.3.1 MessagingClients

正如前面提到的,有两种JMS客户端,分别为clients-JMS和non-JMS Client。

2.3.1.1 JMS Clients

JMSClients使用JMS API连接到消息服务器。许多JMS服务提供者会提供比JMS规范更多的特性,如果您使用了这些特性,当您更换JMS服务提供者时,将不得不重构程序代码,以适应新的JMS服务提供者。

JMS客户端工具包提供了MessageProducer和MessageConsumer这两个接口,所有的JMS提供商都必须提供对这些接口的实现。发送消息的JMS客户端称为消息发送者(Producer),接收消息的客户端称为消费者(consumer)。在很多情况下,一个JMS客户端即要充当发送者,也要充当接收者。

2.3.1.1.1 JMS Producer

JMS客户端使用MessageProducer来发送消息到目的地。在使用session.createProducer()创建Producer的时候可以设置默认的目标(destination)。但是可以通过使用MessageProducer.send()方法进行重写。MessageProducer接口描述如下:



MessageProducer不仅提供了发送消息的方法,同时也提供了设置消息头部的方法(JMSDeliveryMode、JMSPriority、JMSExpiration(get/setTimeToLive())).

2.3.1.1.2 JMS Consumer

JMS客户端使用JMS MessageConsumer类来接收和处理消息。可以使用receive方法处理同步消息,也可以采用MessageListener的实现来处理异步消息。下面是MessageConsumer的接口:


MessageConsumer没有设置目标(destination)的方法,在消费者被创建的时候(session.createConsumer())需要提供默认的目标。

2.3.1.2 Non-JMS Clients

Non-JMS就是不使用JMS接口的客户端。

2.3.2 The JMS Provider

JMSProvider 就是实现了JMS接口的MOM提供商的实现。

2.3.3 JMS 消息的分析

JMS允许消息承载多种形式的信息,可以是文本形式,也可以是二进制的数据或者是在头部提供特殊的属性设置。如下图:


JMS消息主要分为三个部分,分别为头部(headers),属性对(properties)和实际负载(payLoad)。头部提供消息的元数据(主要是对消息的一些控制信息),可以被客户端和JMS提供者来使用。Properties是可选字段,为消息的消费者提供附加的信息。实际负载是消息的主体,可以是文本数据也可以是二进制数据等。

2.3.3.1 JMS Message Headers

可以被客户端的send方法自动设置的headers属性

JMSDestination—消息投递的目标。

JMSDeliveryMode—JMS提供了两种类型的发送模式:持久化(persistent)和非持久化。默认情况下发送模式为持久化。他们分别有不同的开销和可靠性。

        Persistent –建议JMS提供者持久化消息,当提供者出现错误时,消息不会丢失。JMS提供者必须保证消息的once-and-only-once语义。消息持久化会导致额外的储存开销但是具有较高的可靠性。

        Non-Persistent—建议JMS提供者不要持久化消息。保证了at-most-once语义。如果JMS提供者出现错误,消息很可能会丢失。这种方式具有开销小可靠性低的特点。

发送模式是由消息提供者设置的,并且应用于该Producer发送的所有消息。但是这种发送模式可以通过重写个别的发送方法,对个别消息进行修改。

JMSExpiration –消息过期的时间。可以阻止过期的消息被再次投递。可以使用MessageProducer.setTimeToLive()设置默认的过期时间,也可以使用MessageProducer.send()方法设置个别消息的过期时间。默认情况下,time-to-live设置是0,表示消息永不过期。

这个头部属性可以应用于时间敏感的消息。要注意的是,JMS Providers不会发送过期的消息,所以JMS客户端应该不会处理已经过期的消息。

JMSMessageID – 一个能够唯一标识一条消息的字符串,这个字符串被JMS提供者签发,并且必须以“ID:”开始。这个MessageID可以用于消息处理或者消息处理机制中的历史目的。由于消息ID可能导致一些额外的开销,所以消息提供者可以通过使用MessageProducer.setDisableMessageID()建议JMS Provider,JMS应用无需依赖这样一个消息ID。如果JMS 提供者接受这个建议,那么消息ID选项必然被设置为null。但是,需要注意的是JMS 提供者很可能忽略您的设置,无论如何都会继续签发messageID。

JMSPriority –用来为消息签发优先级。这个消息头属性仍然是由消息提供者设置。一旦消息优先级被设置,消息发送者将会对该消息提供者发出的所有消息使用这一优先级。这个优先级同样也可以被个别消息重写。JMS定义了10个等级的优先级策略,0表示最低优先级,9表示最高优先级。等级策略如下:

Priorities0-4 –适用于日常普通消息。

Priorities 5-9 –适用于紧急消息。

JMS提供者没有强制性的要求必须实现消息的次序,尽管应该如此。它们只是尽量的将优先级高的消息先投递。

JMSTimestamp – 这个头选项标志了消息提供者向JMS提供者发送消息的时间。时间是以标准Java的毫秒值表示。可以使用MessageProducer.setDisableMessageTimestamp()取消JMS提供者为JMSTimestamp赋值。如果JMS提供者接受了这项设置,那么JMSTimestamp选项将被赋值为0。

客户端可选的头选项:

JMSCorrelationID –用于当前的消息与之前提交的消息建立关联。这个头选项往往用于将一个请求消息(Request)关联一个响应消息(Response)。JMSCorrelationID可以是下面值中的一种:

        与一个提供者(Provider-specific)相关的消息ID

        与一个应用程序相关(application-specific)的字符串

        一个本地提供者(Provider-native)的byte[]值。

Provider-specific类型的MessageID将会以“ID:”为前缀,而application-specific必须不能以“ID:”为前缀。如果JMS Provider支持接受本地相关的ID,那么JMS 客户端可能需要签发一个被non-JMS客户端匹配的specific JMSCorrelationID 值,但这不是必须的。

JMSReplyTo –用来指定一个反馈消息投递的目的地。这个头部属性经常用于request/reply类型的消息。使用这个头部属性的消息希望接收者能够给出一个反馈信息,但是这是可选的。客户端决定是否发送反馈消息。

JMSType –在语义上定义消息类型,这个头部属性只有很少的厂商提供支持,并且与实际负载的Java类型没有关系。

JMS 提供者设定的头部可选字段:

JMSRedelivered –用来指明已经发送的消息但是没有获取确认消息。当消费者反馈确认消息失败时会出现这种情况。

2.3.3.2 JMS Message Properties

Properties是附加的头部信息,可以更加详细的定制消息。JMS为客户属性设置提供了多种方法。JMS提供使用Java原始模型设置属性的方法包括Boolean、byte、short、int、long、float、double和String Object等。Message interface描述如下:






需要注意的是getPropertyNames方法和propertyExists方法。可以使用getPropertyNames方法返回一个给定消息的具有全部属性的Enumeration,然后简单遍历这些属性。propertyExists可以判定给定的属性字段是否存在。

有三种类型的属性:任意的或称客户属性(arbitry or custom Properties),JMS定义的属性和提供商属性(Provider-specific)。

2.3.3.2.1 客户属性

这些属性是任意的,并且是由JMS应用定义的。开发者可以根据需要任意的定义消息的相关属性。

2.3.3.2.2 JMS-Defined 属性

以JMSX为前缀的JMS定义的属性。对这些属性的支持是可选的。

JMSXAppID:标识发送消息的应用程序。

JMSXConsumerTXID:标识处理这条消息的事物(transaction)。

JMSXDeliveryCount:消息被尝试发送的次数。

JMSXGroupID:标识组消息。

JMSXGroupSeq:本条消息在消息组中的次序。

JMSXProducerTXID:产生消息的事物(transaction)

JMSXRvcTimestamp:JMS提供者向消费者发送消息的时间。

JMSXState:定义Provider-specific的状态

JMSXUserID:标识发送消息的用户。

仅有JMSGroupID和JMSXGroupSeq属性被提倡使用。这两个属性很可能在发送组消息时使用。

2.3.3.2.3 Provider-specific Properties

以JMS_<vendor-name>为前缀的属性为Provider-specific属性。不同的厂商根据需要提供各自的属性。这些属性在non-JMS客户端使用,您不应该在JMS-to-JMS消息通信中使用这些属性。

 

头部和头部属性对于客户端过滤消息是很重要的。

 

 

2.3.4消息选择器(Selectors

很多情况下,JMS客户端订阅了一个目标(Destination)的消息,但是客户端可能仅想消费其中一部分,此时需要对消息进行过滤。可以使用消息的头部或者属性字段对消息进行过滤。JMS客户端工具包提供的选择器(selectors),会通知JMS 提供商(Provider),消费者只想接收包含了特定属性字段的消息。

消息选择器通过对消息头中的属性进行过滤,允许JMS客户端指定哪些消息是其关注的消息。选择器使用SQL92表达式的子集定义。消息选择器使用布尔逻辑,通过对消息的头部属性和属性字段为评判目标进行简单布尔计算。不满足表达式的消息将不被投递到客户端。消息选择器不能对消息的实际负载进行过滤,只能对消息头部的属性和属性字段进行过滤。

过滤器通过字符类型的条件表达式,使用javax.jms.Session进行创建。语法、语义与操作符符合SQL92标准,如下表所示:


实例代码:



消息发送方:

 

1 发送方为消息提供了一个String类型的客户属性

2 发送方为消息提供了一个double类型的客户属性

 

接收方进行过滤:



 

1 选择包含了SYMBOL属性名,并且属性值为AAPL的消息。



 

1 选择有SYMBOL属性名和PRICE属性名,并且SYMBOL的值为AAPL,PRICE的值大于当前价钱的消息。



 

1 接收包含SYMBOL属性名,并且值为AAPL或CSCO,并且包含PRICE属性名,属性值大于当前值,并且没有过期的消息。

2.3.4.1 消息体

JMS定义了六种类型的消息体(或称为实际负载payload)。

Message – 最基础的消息类型。用来发送没有实际负载的消息,这种消息仅有函数头和属性字段。一般用于一般性的通知。

TextMessage – 实际负载为String类型的消息。多用于传输简单的文本类型或者xml类型的数据。

BytesMessage – 用于发送一个未解析的字节数组。

2.3.5 JMS

JMS有两种类型的消息,分别是发布/订阅式和点对点式。

2.3.5.1 点对点(PTP Point-to-Point

点对点式的消息传输中目标(Destination)将被认为是一个队列,消息可以同步的或者异步的被发送和接收。队列中的每条消息将仅会投递给一个消费者,并且仅投递一次(once-and-only-once语义)。这很像发送email一样。消费者可以使用MessageConsumer.receive()同步接收队列中的消息,也可以实现MessageListener的MessageConsumer.setMessageListener()方法异步接收消息。队列将会存储所有投递来的消息,直到消息被投递出去或者消息已经过期。


与PTP消息相似,订阅消息可以通过MessageConsumer.receive()同步的从主题(topic)处接收消息,也可以通过实现MessageListener的setMessageListener方法异步接收订阅主题。多个消费者可以注册同一个消息队列接收消息,但是一条PTP消息仅会发给一个消费者。上图中需要注意,一个PTP消息的发送者发送的消息仅会被一个消费者接收,并不是所有注册到这个队列的消费者都会接收到同一消息。JMS提供者保证消息有且仅有一次被投递到注册的消费者。JMS提供商会在众多消费者之间做到一种负载平衡。

2.3.5.2 发布/订阅域(Publish/Subscribe Domain

发布/订阅使用远程的主题(topic)目的地。发布者将消息发布给远程的主题,订阅者注册接收相应主题的消息。所有的发向主题的消息都会被以一种推的方式(push model)投递给所有的接收者。


StreamMessage – 用来发送一串原始的Java类型的数据,这些数据将会按照一定的次序填充和读取。

ObjectMessage – 用于串行化的Java对象作为负载。用于复杂的Java对象,同时也支持Java集合类型。

与PTP消息相似,订阅消息可以通过MessageConsumer.receive()同步的从主题(topic)处接收消息,也可以通过实现MessageListener的setMessageListener方法异步接收订阅主题。如果不显示指明保持(durability)消息,主题消息将不会被保持。可以使用具有生命期(durable)的订阅。使用具有生命期的订阅时,如果有订阅者在消息发布时没有与JMS提供者连接,JMS将会为这种订阅者存储消息,当订阅者再次连接到JMS提供者时,这个订阅者将会收到失去连接期间的所有没过期的消息。生命期订阅允许发送消息时,接收者的非连接状态。

2.3.5.2.1 区分生命期消息(Message Durability)和消息持久化(MessagePersistence

生命期消息仅适用于发布/订阅消息域,当客户连接到主题时,他们可以决定是使用有生命期的(durable)还是无生命期的(non-durable)订阅。

Durable Subscription – 当接收有生命期主题订阅的消费者在消息发布时处于不可用状态,JMS提供者将会对订阅的主题进行存储,当消费者状态可用时,会接收到之前订阅的消息。

Non-Durable Subscription - 在消费者不可用期间发布的所有消息,都将丢失。

消息持久化(persistence)是一个独立任何域的主题。主要用于JMS提供者崩溃时消息的恢复。可以通过JMSDeliveryMode属性设置消息的持久化和非持久化属性。

JMS应用中使用Request/Reply消息

尽管JMS规范没有将Request/Reply消息作为一种消息域进行规范,但是却提供了支持这种域的方式,提供了一些消息头部属性和一组方便实用的类,处理基础的Request-reply消息。Request-reply消息是一种同步的后端和前端模式。即不属于PTP域,也不属于pub/sub域。需要结合使用了头部的JMSReplyTo和JMSCorrelationID属性和临时目的地。JMSReplyTo明确了反馈消息需要投递的目的地,JMSCorrelationID指定了应答的消息是哪一条。这些头部属性用于将反馈消息与已发送消息相关联。临时目的地是用于在连接期间使用,并且只能被连接创建者使用的。

为Request/reply提供使用的类包括QueueRequestor和TopicRequestor。这些类提供了request()方法发送请求消息,并且为每一次请求等待反馈消息。


这种类型的通信通常使用JMSReplyTo头部属性,和一个临时队列,接收者将反馈消息发送给这个临时队列,并被发送者接收消费。QueueRequestor和TopicRequestor只能解决基础的Request/reply,不能处理复杂的情况。就是说只能处理一条Request对应一条reply,不能处理一条Request对应多个接收者的情况。

2.3.6 管理员对象(Administered Objects

管理员对象包含了配置JMS相关信息,一般由JMS管理员进行创建。管理员对象被客户端使用,用来为客户端隐藏JMS提供商的细节,并且将JMS提供者任务进行抽象化。对于JMS提供者嵌入于JavaEE容器的情况更为常见。JMS规范定义了两种管理员对象:ConnectionFactory和Destination.

2.3.6.1 连接工厂(ConnectionFactory

JMS客户端使用ConnectionFactory对象创建于JMS提供者之间的连接。JMS提供者与客户端之间的连接需要使用TCP socket,连接的花销是比较大的,提倡使用连接池技术减少这种开销。JMS客户端可以使用JMS连接创建javax.jms.Session对象,并且使用这个对象与JMS Provider进行交换。

2.3.6.2 Destination

Destination对象封装了提供者相关的消息发送和接收的地址。尽管Destination是Session创建的,但是它们的生命期与创建Session的连接相匹配(就是说Session终结了,连接没有终结,Destination仍然有效)。

一个连接仅有一个临时目的地。临时目的地的生命期将会与创建它们的连接一样长,并且仅有创建它们的连接才能为他们创建新的消费者。临时目的地用于Request/reply类型消息的发送和接收。

2.3.7 使用JMSAPI创建JMS应用

创建JMS应用程序的步骤:

1.      获得JMS连接工厂(connectionFactory)

2.      使用JMS工厂创建JMS连接

3.      启动JMS连接

4.      使用JMS连接创建JMS 会话(Session)

5.      获得一个JMS的目的地

6.      创建JMS 发送者(Producer)

A.创建JMS发送者

B.创建JMS消息,并指明消息发送的目的地

7.      创建JMS消费者(Consumer)

A.创建一个消费者

B.注册一个消息监听器

8.      发送接收JMS消息

9.      关闭JMS资源(connection、Session、producer、Consumer)

使用JMSAPI发送一条消息:







1 使用JNDI初始化运行环境。(更多情况下,我们使用spring等环境)

2 在初始化的上下文环境中通过唯一的工厂名字,寻找JMS连接工厂(一般称这个过程为注入,多用于EJB和Spring等开发环境)。

3 使用连接工厂,创建JMS连接

4 调用start()方法开启连接

5 使用连接创建一次会话,这里使用了自动消息确认

6 使用会话创建JMS队列

7 使用会话和目的(指6中创建的队列)创建一个发送者(Producer)

8 显示的设置了持久化,默认的JMS发送使用的就是持久化方式。

9 创建了一个简单的文本负载消息。

10 使用发送者发送消息到目的地,并且关闭响应的对象连接。

 

需要注意的是发送方并不关注JMS消费者是否在另一端等待接收消息。

接收者代码:


解释略。

 

注意JMS应用程序中使用多线程

JMS规范定义了多种可以并行访问的API,但是只用很少一部分对象支持并行访问。ConnectionFactory,Connection和Destination对象需要支持并行访问,Session、MessageProducer和MessageConsumer等对象不支持并行访问。这就意味着,像Session、MessageProducer和MessageConsumer这些对象不应该在多线程中共享。

2.3.8 消息驱动的实体BeanMessage-DrivenBeansMDBs

MDBs出现在EJB2.0规范中,目的是将JMS集成到EJBs中,使得异步消息通信就像使用标准JMS APIs一样方便。通过使用JMSMessageListener,EJB以一种推(push,就是接收端在监听,JMS提供者有消息直接投递给监听进程,另外一种方式是poll,轮询方式,就是接收方不断的向JMS提供者查询,是否有其想要的消息已经到达,这对应了JMS的两种消息交换方式,即同步方式和异步方式)的形式从JMS 提供者接收消息。下面是一段示例代码:


需要注意的是上述的MyMessageProcessor类实现了MessageDrivenBean和MessageLstener两个接口。MessageDrivenBean接口需要对setMessageDrivenContext()方法和ejbRemove()方法提供实现。这两个方法都是由EJB容器调用,用来创建和销毁MDB。MessageListener接口仅包含了一个称为onMessage()的方法。当新的消息到达了MDB注册的目的地后,JMS提供者会自动的调用onMessage()方法接收消息。

此外EJB容器可以管理所有的必要资源,包括JavaEE的资源(例如:JDBC、JMS和JCA的连接等)、安全、事务甚至是JMS消息的确认消息。使用MDBs的最大优势在于可以并行的对消息进行处理。典型的JMS客户端需要手动的管理其拥有的资源和环境,并且往往以一种串行的方式来处理消息,即一次处理一个消息(除非在设计之初就考虑将并行处理消息作为重点来处理)。EJB则不同,MDBs可以同时处理尽可能多的消息,这是因为EJB容器可以创建EJB配置文件描述的最大数目的MDB,每个MDB可以处理一个单独的消息。这种配置与具体的JavaEE容器相关。如果您使用Java EE 容器,可以参考帮助文档获取对EJB的配置方法。

MDBs的一个缺陷在于需要一个完整的JavaEE容器。到目前为止支持MDBs的EJB容器必须使用完整的Java EE容器。当使用完整的Java EE容器后,MDBs可以被使用,但是,还有可选的方案不必使用完整的JavaEE容器。使用Spring框架JMS相关的APIs使得开发消息驱动的POJOs(MDPs)更加容易。就是说,Plain Old JavaObjects(POJOs)以一种消息驱动的方式被驱动。实际上,这种方式的配置在Java开发领域已经很流行了,因为其避免了完整JavaEE容器所带来的额外代价。

不是所有的EJB容器都需要完整的JavaEE容器支持---OpenEJB

目前为止,基本上所有的EJB容器都是用了完整的JavaEE容器来支持MDBs。只有Apache的OpenEJB不同。OpenEJB通过EJB1.1规范和EJB2规范以及EJB3规范。不论是嵌入式还是独立式的方式支持MDBs。OpenEJB可以嵌入到Apache Geronimo,Jetty,Apache Tomcat或者是您自己的Java应用程序中,并且支持MDBs。

2.4 总结

JMS规范在Java世界中有着巨大的影响,使得面向消息的通信在Java世界中成为可能。这是Java能够在商业领

域中满足重要应用集成的关键一步。因为JMS允许以一种标准的形式处理消息。

现在您应该基本了解JMS和其提供的功能,下一步可以学习JMS提供者的实现细节。下一章提供了对Apache ActiveMQ的一些信息。


 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值