Mina源码阅读笔记(四)—Mina的连接IoConnector

上一篇写的是IoAcceptor是服务器端的接收代码,今天要写的是IoConnector,是客户端的连接器。在昨天,我们还留下一些问题没有解决,这些问题今天同样会产生,但是都要等到讲到session的时候才能逐步揭开。先回顾一下问题:

l  我们已经在AbstractPollingIoAcceptor中看到了,mina是将连接(命令)和业务(读写)分不同线程处理的,但是我们还没有看到mina是如何实现对这些线程的管理的。

l  在昨天的最后,我们看到在NioSocketAcceptor中的具体NIO实现,但是我们还没有看到mina是在哪里调用了这些具体操作的。当然这也是mina对连接线程管理的一部分。

这些问题今天也会出现,因为从名字上就能看出,IoConnector和IoAcceptor的构造相差不大,所以在写connector的分析时,主要会从结构和差异上入手,最后再给出昨天没写完的删减版的Acceptor。先回顾一下客户端连接的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建一个非阻塞的客户端程序
         IoConnector connector = new NioSocketConnector();
         // 设置链接超时时间
         connector.setConnectTimeout( 30000 );
         // 添加过滤器
         connector.getFilterChain().addLast(
                 "codec" ,
                 new ProtocolCodecFilter( new MessageCodecFactory(
                         new InfoMessageDecoder(Charset.forName( "utf-8" )),
                         new InfoMessageEncoder(Charset.forName( "utf-8" )))));
         // 添加业务逻辑处理器类
         connector.setHandler( new ClientHandler());
         IoSession session = null ;
         try {
             ConnectFuture future = connector.connect( new InetSocketAddress(
                     HOST, PORT)); // 创建连接
             future.awaitUninterruptibly(); // 等待连接创建完成
             session = future.getSession(); // 获得session

还是先看IoConnector的结构图,图来自mina官网,用XMind绘制的。在写构成之前,我们还是先看一下mina官网对这些connector的介绍:

As we have to use an IoAcceptor for servers, you have to implement the IoConnector. Again, we have many implementation classes :

  • NioSocketConnector : the non-blocking Socket transport Connector
  • NioDatagramConnector : the non-blocking UDP transport * Connector*
  • AprSocketConnector : the blocking Socket transport * Connector*, based on APR
  • ProxyConnector : a Connector providing proxy support
  • SerialConnector : a Connector for a serial transport
  • VmPipeConnector : the in-VM * Connector*

其中,NioSocketConnector是我们最常用到的,proxy方式虽然在mina的源码中也花了大篇幅去撰写,但可惜的是很少有相关的文档,所以学习的成本还挺高的。今天我们主要还是按照上图画的两条路来看NioSocketConnector

和昨天一样,我们还是从左边的路走起,看interface IoConnector,这个接口主要定义了连接的方法以及socket连接时用到的参数。在mina中通过IoFuture来描述、侦听在IoSession上实现的异步IO操作,所以这IoConnector中的connect方法都返回了一个ConnectFuture实例。

而在SocketConnector接口中的定义中就显得更简单了,它和IoConnector之间也是接口的继承关系,在SocketConnector中就定义了两类方法,一个对远程地址的getset,一个拿到session的配置。这些都容易理解。

再来看右边,AbstractIoConnector,这个抽象类主要作用就是实现IoConnector里定义的操作,至于他又继承了AbstractIoService,一是为了用到父类(AbstractIoService)的方法,二是为了将那些父类没有实现的方法继续传递下去,让它(AbstractIoConnector)的子类去实现。所以看多了,好多结构也能看明白了,这里我觉得主要要学习的还是接口、抽象类之间的引用关系。

继续看AbstractIoConnector,这个类主要是实现了connect的逻辑操作(封装了连接前后的一些必要执行步骤和check一些状态),具体的连接操作还是让子类去实现,这个和上篇写的AbstractIoAcceptor一模一样,在AbstractIoAcceptor中,主要也是封装了bind的逻辑操作,真正的bind过程是让子类去实现的简单看下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public final ConnectFuture connect(SocketAddress remoteAddress, SocketAddress localAddress,
             IoSessionInitializer<? extends ConnectFuture> sessionInitializer) {
         if (isDisposing()) {
             throw new IllegalStateException( "The connector has been disposed." );
         }
 
         if (remoteAddress == null ) {
             throw new IllegalArgumentException( "remoteAddress" );
         }
 
         if (!getTransportMetadata().getAddressType().isAssignableFrom(remoteAddress.getClass())) {
             throw new IllegalArgumentException( "remoteAddress type: " + remoteAddress.getClass() + " (expected: "
                     + getTransportMetadata().getAddressType() + ")" );
         }
 
         if (localAddress != null && !getTransportMetadata().getAddressType().isAssignableFrom(localAddress.getClass())) {
             throw new IllegalArgumentException( "localAddress type: " + localAddress.getClass() + " (expected: "
                     + getTransportMetadata().getAddressType() + ")" );
         }
 
         if (getHandler() == null ) {
             if (getSessionConfig().isUseReadOperation()) {
                 setHandler( new IoHandler() {
                     public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
                         // Empty handler
                     }
 
                     public void messageReceived(IoSession session, Object message) throws Exception {
                         // Empty handler
                     }
 
                     public void messageSent(IoSession session, Object message) throws Exception {
                         // Empty handler
                     }
 
                     public void sessionClosed(IoSession session) throws Exception {
                         // Empty handler
                     }
 
                     public void sessionCreated(IoSession session) throws Exception {
                         // Empty handler
                     }
 
                     public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
                         // Empty handler
                     }
 
                     public void sessionOpened(IoSession session) throws Exception {
                         // Empty handler
                     }
                 });
             } else {
                 throw new IllegalStateException( "handler is not set." );
             }
         }
 
         return connect0(remoteAddress, localAddress, sessionInitializer);
}
 
 
    protected abstract ConnectFuture connect0(SocketAddress remoteAddress, SocketAddress localAddress,
             IoSessionInitializer<? extends ConnectFuture> sessionInitializer);

Connect0才是最后具体的操作,而这一步操作在这个类中被没有给出实现。具体实现放在了AbstractPollingIoConnector上。和昨天一样,这些设计都是对称的,我们还是看三点:

l  implementing client transport using a polling strategy

l  A Executor will be used for running client connection, and an AbstractPollingIoProcessor will be used for processing connected client I/O operations like reading, writing and closing.

l  All the low level methods for binding, connecting, closing need to be provided by the subclassing implementation

至于内部的具体实现,那跟acceptor中没什么区别,连使用的工具类都差别不大,这部分就很容易读懂了,只不过一个是bind一个是connect

最后我们看NioSocketConnector,具体连接的实现类,只有一个成员变量和NioSocketAcceptor一样:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private volatile Selector selector;
 
    @Override
     protected SocketChannel newHandle(SocketAddress localAddress) throws Exception {
         SocketChannel ch = SocketChannel.open();
 
         int receiveBufferSize = (getSessionConfig()).getReceiveBufferSize();
         if (receiveBufferSize > 65535 ) {
             ch.socket().setReceiveBufferSize(receiveBufferSize);
         }
 
         if (localAddress != null ) {
             ch.socket().bind(localAddress);
         }
         ch.configureBlocking( false );
         return ch;
     }

只是需要注意,这里面专门有个内部类来处理selectionkey,将遍历的过程都抽离出来了,这个和我们用NIO的一般写法稍有不同,这样做的好处也是为了复用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static class SocketChannelIterator implements Iterator<SocketChannel> {
 
         private final Iterator<SelectionKey> i;
 
         private SocketChannelIterator(Collection<SelectionKey> selectedKeys) {
             this .i = selectedKeys.iterator();
         }
 
         /**
          * {@inheritDoc}
          */
         public boolean hasNext() {
             return i.hasNext();
         }
 
         /**
          * {@inheritDoc}
          */
         public SocketChannel next() {
             SelectionKey key = i.next();
             return (SocketChannel) key.channel();
         }
 
         /**
          * {@inheritDoc}
          */
         public void remove() {
             i.remove();
         }
     }
---------------------------------------------------------

补一个上篇就应该发的acceptor的阉割版,写这样的东西主要还是为了理清楚结构。我主要是把内容简化了,但是结构都没有变,核心的成员变量也没有少:

起点IoService:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.apache.mina.core.rewrite.service;
 
/**
  * IO Service --handler/processor/acceptor/connector
  *
  * @author ChenHui
  *
  */
public interface IoService {
     /** 添加listener */
     void addListener(IoServiceListener listener);
 
     /** 销毁 */
     void dispose( boolean awaitTermination);
 
     /** 设置handler */
     IoHandler getHandler();
 
     void setHandler(IoHandler handler);
 
     /** 管理session */
     int getManagedSessionCount();
     
     boolean isActive();
}
左边的路
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.apache.mina.core.rewrite.service;
 
import java.io.IOException;
import java.net.SocketAddress;
import java.util.Set;
 
/**
  * 注意接口的继承,这里的方法都是新定义的
  *  
  * Acceptor 主要用于:Accepts incoming connection, communicates with clients, and
  * fires events to IoHandler
  *
  * @author ChenHui
  */
public interface IoAcceptor extends IoService {
     
     SocketAddress getLocalAddress();
     
     Set<SocketAddress> getLocalAddresses();
     
     void bind(SocketAddress localAddress) throws IOException;
     
     void bind(Iterable<? extends SocketAddress> localAddresses) throws IOException;
     
     void unbind(SocketAddress localAddress);
     
     
     /**没有写到IoSession 所以暂时不用*/
     //IoSession newSession(SocketAddress remoteAddress,SocketAddress localAddress);
}
SocketAcceptor:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.apache.mina.rewrite.transport.socket;
 
import java.net.InetSocketAddress;
 
import org.apache.mina.core.rewrite.service.IoAcceptor;
 
public interface SocketAcceptor extends IoAcceptor {
 
     InetSocketAddress getLocalAddress();
 
     void setDefaultLocalAddress(InetSocketAddress localAddress);
 
     public boolean isReuseAddress();
     
     // ...
 
     // SocketSessionConfig getSessionConfig();
}
再看右边的
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package org.apache.mina.core.rewrite.service;
 
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
 
public abstract class AbstractIoService implements IoService {
 
     private static final AtomicInteger id = new AtomicInteger();
 
     private final String threadName;
 
     private final Executor executor;
 
     private final boolean createdExecutor;
 
     private IoHandler handler;
 
     // 用于安全的关闭
     protected final Object disposalLock = new Object();
 
     private volatile boolean disposing;
 
     private volatile boolean disposed;
 
     /**
      *
      * @param param
      *            sessionConfig IoSessionConfig
      * @param executor
      *            used for handling execution of IO event. can be null
      */
     protected AbstractIoService(Object param, Executor executor) {
 
         // TODO listener & session config
 
         if (executor == null ) {
             this .executor = Executors.newCachedThreadPool();
             createdExecutor = true ;
         } else {
             this .executor = executor;
             createdExecutor = false ;
         }
 
         threadName = getClass().getSimpleName() + "-" + id.incrementAndGet();
     }
     
     @Override
     public void addListener(IoServiceListener listener) {
         // TODO add listener
     }
 
     /**注意这个不是override来的*/
     protected final void ececuteWorker(Runnable worker, String suffix){
         
         String actualThreadName=threadName;
         if (suffix!= null ){
             actualThreadName=actualThreadName+ "-" +suffix;
         }
         executor.execute(worker);
     }
     
     @Override
     public void dispose( boolean awaitTermination) {
         if (disposed) {
             return ;
         }
 
         synchronized (disposalLock) {
             if (!disposing) {
                 disposing = true ;
                 try {
                     /** 真正的关闭方法TODO */
                     dispose0();
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
             }
         }
 
         if (createdExecutor) {
             ExecutorService e = (ExecutorService) executor;
             e.shutdown();
 
             if (awaitTermination) {
                 try {
 
                     e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
 
                 } catch (InterruptedException e1) {
                     // 注意异常时的中断处理
                     Thread.currentThread().interrupt();
                 }
             }
         }
         disposed = true ;
     }
 
     protected abstract void dispose0() throws Exception;
 
     @Override
     public IoHandler getHandler() {
         return this .handler;
     }
 
     @Override
     public void setHandler(IoHandler handler) {
         if (handler == null ) {
             throw new IllegalArgumentException( "handler cannot be null" );
         }
         // TODO isActive: when service is active, cannot be set handler
         if (isActive()){
             throw new IllegalStateException( "when service is active, cannot be set handler" );
         }
         
         this .handler = handler;
     }
 
}
AbstractIoAcceptor:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package org.apache.mina.core.rewrite.service;
 
import java.io.IOException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
 
public abstract class AbstractIoAcceptor extends AbstractIoService implements
         IoAcceptor {
 
     private final List<SocketAddress> defaultLocalAddresses = new ArrayList<SocketAddress>();
 
     private final List<SocketAddress> unmodifiableDeffaultLocalAddresses = Collections
             .unmodifiableList(defaultLocalAddresses);
 
     private final Set<SocketAddress> boundAddresses = new HashSet<SocketAddress>();
 
     private boolean disconnectOnUnbind = true ;
 
     /** 这里不是很明白,为什么要用protected 而 不是private */
     protected final Object bindLock = new Object();
 
     /**
      * 注意这个构造方法是一定要写的,否则编译不通过:抽象类继承时候,构造方法都要写,而且必须包含super
      *
      * @param param
      *            sessionConfig
      * @param executor
      */
     protected AbstractIoAcceptor(Object param, Executor executor) {
         super (param, executor);
         defaultLocalAddresses.add( null );
     }
 
     @Override
     public SocketAddress getLocalAddress() {
 
         Set<SocketAddress> localAddresses = getLocalAddresses();
         if (localAddresses.isEmpty()) {
             return null ;
         }
         return localAddresses.iterator().next();
     }
 
     @Override
     public final Set<SocketAddress> getLocalAddresses() {
         Set<SocketAddress> localAddresses = new HashSet<SocketAddress>();
         synchronized (boundAddresses) {
             localAddresses.addAll(boundAddresses);
         }
         return localAddresses;
     }
 
     @Override
     public void bind(SocketAddress localAddress) throws IOException {
         // TODO Auto-generated method stub
 
     }
 
     @Override
     public void bind(Iterable<? extends SocketAddress> localAddresses)
             throws IOException {
         // TODO isDisposing()
 
         if (localAddresses == null ) {
             throw new IllegalArgumentException( "localAddresses" );
         }
 
         List<SocketAddress> localAddressesCopy = new ArrayList<SocketAddress>();
 
         for (SocketAddress a : localAddresses) {
             // TODO check address type
             localAddressesCopy.add(a);
         }
 
         if (localAddressesCopy.isEmpty()) {
             throw new IllegalArgumentException( "localAddresses is empty" );
         }
 
         boolean active = false ;
 
         synchronized (bindLock) {
             synchronized (boundAddresses) {
                 if (boundAddresses.isEmpty()) {
                     active = true ;
                 }
             }
         }
         /** implement in abstractIoService */
         if (getHandler() == null ) {
             throw new IllegalArgumentException( "handler is not set" );
         }
 
         try {
             Set<SocketAddress> addresses = bindInternal(localAddressesCopy);
 
             synchronized (boundAddresses) {
                 boundAddresses.addAll(addresses);
             }
         } catch (IOException e) {
             throw e;
         } catch (RuntimeException e) {
             throw e;
         } catch (Throwable e) {
             throw new RuntimeException( "Filed ti bind" );
         }
         
         if (active){
             //do sth
         }
     }
 
     protected abstract Set<SocketAddress> bindInternal(
             List<? extends SocketAddress> localAddress) throws Exception;
 
     @Override
     public void unbind(SocketAddress localAddress) {
         // TODO Auto-generated method stub
         
     }
}
polling:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package org.apache.mina.core.rewrite.polling;
 
import java.net.SocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
 
import org.apache.mina.core.rewrite.service.AbstractIoAcceptor;
 
public abstract class AbstractPollingIoAcceptor extends AbstractIoAcceptor {
 
     private final Semaphore lock = new Semaphore( 1 );
 
     private volatile boolean selectable;
 
     private AtomicReference<Acceptor> acceptorRef = new AtomicReference<Acceptor>();
 
     /**
      * define the num of sockets that can wait to be accepted.
      */
     protected int backlog = 50 ;
 
     /**
      * 一样的,这个构造方法也要写
      *
      * @param param
      * @param executor
      */
     protected AbstractPollingIoAcceptor(Object param, Executor executor) {
         super (param, executor);
         // TODO Auto-generated constructor stub
     }
 
     /**
      * init the polling system. will be called at construction time
      *
      * @throws Exception
      */
     protected abstract void init() throws Exception;
 
     protected abstract void destory() throws Exception;
 
     protected abstract int select() throws Exception;
     /**这里有点儿变动*/
     protected abstract ServerSocketChannel open(SocketAddress localAddress) throws Exception;
 
     @Override
     protected Set<SocketAddress> bindInternal(
             List<? extends SocketAddress> localAddress) throws Exception {
         // ...
         try {
             lock.acquire();
             Thread.sleep( 10 );
         } finally {
             lock.release();
         }
         // ...
         return null ;
     }
 
     /**
      * this class is called by startupAcceptor() method it's a thread accepting
      * incoming connections from client
      *
      * @author ChenHui
      *
      */
     private class Acceptor implements Runnable {
         @Override
         public void run() {
             assert (acceptorRef.get() == this );
 
             int nHandles = 0 ;
 
             lock.release();
 
             while (selectable) {
                 try {
                     int selected = select();
 
                     // nHandles+=registerHandles();
 
                     if (nHandles == 0 ) {
                         acceptorRef.set( null );
                         // ...
                     }
                 } catch (Exception e) {
 
                 }
             }
         }
     }
}
好了最后看NioSoeketAcceptor:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package org.apache.mina.rewrite.transport.socket.nio;
 
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.concurrent.Executor;
 
import org.apache.mina.core.rewrite.polling.AbstractPollingIoAcceptor;
import org.apache.mina.rewrite.transport.socket.SocketAcceptor;
 
public final class NioSocketAcceptor extends AbstractPollingIoAcceptor
         implements SocketAcceptor {
 
     private volatile Selector selector;
 
     protected NioSocketAcceptor(Object param, Executor executor) {
         super (param, executor);
         // TODO Auto-generated constructor stub
     }
 
     @Override
     public int getManagedSessionCount() {
         // TODO Auto-generated method stub
         return 0 ;
     }
 
     /**
      * 这个方法继承自AbstractIoAcceptor
      *
      * The type NioSocketAcceptor must implement the inherited abstract method
      * SocketAcceptor.getLocalAddress() to override
      * AbstractIoAcceptor.getLocalAddress()
      */
     @Override
     public InetSocketAddress getLocalAddress() {
         // TODO Auto-generated method stub
         return null ;
     }
 
     @Override
     public void setDefaultLocalAddress(InetSocketAddress localAddress) {
         // TODO Auto-generated method stub
 
     }
 
     @Override
     public boolean isReuseAddress() {
         // TODO Auto-generated method stub
         return false ;
     }
 
     @Override
     protected void init() throws Exception {
         selector = Selector.open();
     }
 
     @Override
     protected void destory() throws Exception {
         if (selector != null ) {
             selector.close();
         }
     }
 
     @Override
     protected int select() throws Exception {
         return selector.select();
     }
 
     @Override
     protected void dispose0() throws Exception {
         // TODO Auto-generated method stub
 
     }
 
     protected ServerSocketChannel open(SocketAddress localAddress)
             throws Exception {
         ServerSocketChannel channel =ServerSocketChannel.open();
         
         boolean success= false ;
         
         try {
             channel.configureBlocking( false );
             
             ServerSocket socket=channel.socket();
             
             socket.setReuseAddress(isReuseAddress());
             
             socket.bind(localAddress);
             
             channel.register(selector, SelectionKey.OP_ACCEPT);
             
             success= true ;
         } finally {
             if (!success){
                 //close(channel);
             }
         }
         return channel;
     }
 
     @Override
     public boolean isActive() {
         // TODO Auto-generated method stub
         return false ;
     }
 
}
------------------------------------------------------

到此为止将连接部分都写完了,在连接部分还有些零碎的东西,比如handlerpolling,这些都只是稍稍提了一下,具体后面会在介绍其他部分是肯定还会碰上,我还是想把重心放在最主要的部分去写,下一篇应该要写到session了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值