5.ActiveMQ消息发送【源码分析】

如需了解消息发送策略【同步发送 & 异步发送】,请移步:ActiveMQ消息发送策略


ActiveMQ消息发送源码分析

  我们可以从消息发送端的producer.send()出发,开始分析源码:

TextMessage message = session.createTextMessage("Hello ActiveMQ:" + i);
producer.send(message);

 1.调用ActiveMQMessageProducerSupport类中的send方法

public void send(Message message) throws JMSException {
    this.send(this.getDestination(), message, this.defaultDeliveryMode, this.defaultPriority, this.defaultTimeToLive);
}

   1.1 调用ActiveMQMessageProducer类中的send方法

public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException {
    this.send(destination, message, deliveryMode, priority, timeToLive, (AsyncCallback)null);
}

   1.2 在调用该类(ActiveMQMessageProducer类)下的具体send实现方法

public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, AsyncCallback onComplete) throws JMSException {
    //检查当前的session会话状态。如果session已经关闭,则抛出异常
    this.checkClosed();
    if (destination == null) {
        if (this.info.getDestination() == null) {
            throw new UnsupportedOperationException("A destination must be specified.");
        } else {
            throw new InvalidDestinationException("Don't understand null destinations");
        }
    } else {
        ActiveMQDestination dest;
        //检查destination的类型,如果符合要求,就转变为ActiveMQDestination
        if (destination.equals(this.info.getDestination())) {
            dest = (ActiveMQDestination)destination;
        } else {
            if (this.info.getDestination() != null) {
                throw new UnsupportedOperationException("This producer can only send messages to: " + this.info.getDestination().getPhysicalName());
            }

            dest = ActiveMQDestination.transform(destination);
        }

        if (dest == null) {
            throw new JMSException("No destination specified");
        } else {
            if (this.transformer != null) {
                Message transformedMessage = this.transformer.producerTransform(this.session, this, message);
                if (transformedMessage != null) {
                    message = transformedMessage;
                }
            }
            //如果发送窗口大小不为空,则判断发送窗口的大小决定是否阻塞
            if (this.producerWindow != null) {
                try {
                    //根据当前窗口大小,来决定是否去阻塞
                    this.producerWindow.waitForSpace();
                } catch (InterruptedException var10) {
                    throw new JMSException("Send aborted due to thread interrupt.");
                }
            }
            //【跳转至步骤2】通过session.send(),发送消息到broker的topic
            this.session.send(this, dest, message, deliveryMode, priority, timeToLive, this.producerWindow, this.sendTimeout, onComplete);
            this.stats.onMessage();
        }
    }
}

 2.调用ActiveMQSession类中的send方法,发送消息至Broker

protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, int deliveryMode, int priority, long timeToLive, MemoryUsage producerWindow, int sendTimeout, AsyncCallback onComplete) throws JMSException {
    //检查当前的session会话状态
    this.checkClosed();
    if (destination.isTemporary() && this.connection.isDeleted(destination)) {
        throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
    } else {
        //互斥锁,如果一个session的多个producer发送消息到这里,会保证消息发送的有序性
        synchronized(this.sendMutex) {
            this.doStartTransaction();//告诉broker,开始一个新事务,只有事务性会话中才会开启
            TransactionId txid = this.transactionContext.getTransactionId();
            long sequenceNumber = producer.getMessageSequence();
            message.setJMSDeliveryMode(deliveryMode);//在JMS协议头中,设置是否持久化标识
            long expiration = 0L;//计算消息过期时间
            if (!producer.getDisableMessageTimestamp()) {
                long timeStamp = System.currentTimeMillis();
                message.setJMSTimestamp(timeStamp);
                if (timeToLive > 0L) {
                    expiration = timeToLive + timeStamp;
                }
            }

            message.setJMSExpiration(expiration);//设置消息过期时间
            message.setJMSPriority(priority);//设置消息的优先级
            message.setJMSRedelivered(false);//设置消息为非重发
            //将不同的消息格式,统一转化为ActiveMQMessage
            ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, this.connection);
            msg.setDestination(destination);//设置消息的目的地
            //生成并设置消息id
            msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));
            if (msg != message) {//如果消息式经过转化的,则更新原来的消息id和目的地
                message.setJMSMessageID(msg.getMessageId().toString());
                message.setJMSDestination(destination);
            }

            msg.setBrokerPath((BrokerId[])null);
            msg.setTransactionId(txid);
            if (this.connection.isCopyMessageOnSend()) {
                msg = (ActiveMQMessage)msg.copy();
            }

            msg.setConnection(this.connection);
            msg.onSend();//把消息属性和消息体都设置为只读,防止被修改
            msg.setProducerId(msg.getMessageId().getProducerId());
            if (LOG.isTraceEnabled()) {
                LOG.trace(this.getSessionId() + " sending message: " + msg);
            }
            //如果onComplete有设置,或者发送超时时间小于0,或者消息需要反馈,或者连接器是同步发送模式,或者消息是持久化且连接器是异步发送模式
            //或者不存在事务id的情况下,走异步发送,否则走同步发送
            if (onComplete != null || sendTimeout > 0 || msg.isResponseRequired() || this.connection.isAlwaysSyncSend() || msg.isPersistent() && !this.connection.isUseAsyncSend() && txid == null) {
                //走同步发送
                if (sendTimeout > 0 && onComplete == null) {
                    this.connection.syncSendPacket(msg, sendTimeout);
                } else {
                    this.connection.syncSendPacket(msg, onComplete);
                }
            } else {
                //【跳转至步骤3】走异步发送
                this.connection.asyncSendPacket(msg);
                //判断producerWindow窗口是否为空
                if (producerWindow != null) {
                    //获取producer发送消息的大小
                    int size = msg.getSize();
                    //增加producerWindow大小
                    producerWindow.increaseUsage((long)size);
                }
            }
        }
    }
}

 3.分析异步发送过程(调用ActiveMQConnection类下的asyncSendPacket方法)

public void asyncSendPacket(Command command) throws JMSException {
    if (this.isClosed()) {
        throw new ConnectionClosedException();
    } else {
        //【跳转至步骤3.1】如果连接没有关闭,执行异步发送过程(command为当前的消息,经过组装后的结果)
        this.doAsyncSendPacket(command);
    }
}

   3.1 执行同类(ActiveMQConnection类)下的doAsyncSendPacket方法

private void doAsyncSendPacket(Command command) throws JMSException {
    try {
        //【跳转至步骤3.2】此处即为传输层transport
        this.transport.oneway(command);
        //此处this.transport,在很多的中间件中都会看到,它并不是一个单纯的transport。我们在brokerURL中可以配置,我们
        //如果配置"tcp://192.168.204.201:61616",此处传递一个tcp的话,意味着我们是使用TcpTransport.所以此处
        //transport一定是一个多态实现的方式。因为transport是一个接口,所以我们在oneway中能够看到针对transport的很多
        //实现这个时候它会调用哪个实现呢?我们发现在此处走不下去之后,我们可以去返回看一下transport是如何实例化的
    } catch (IOException var3) {
        throw JMSExceptionSupport.create(var3);
    }
}

   3.2 transport是如何实例化的

        transport在创建connection的时候会去初始化 。因为当前transport是绑定在当前的activeMQConnection,按我们的猜想,这个transport一定是在构建这个连接的时候去传递的。

     3.2.1 创建Connection连接

Connection connection = connectionFactory.createConnection();

     3.2.2 进入ActiveMQConnectionFactory类下的createConnection方法

public Connection createConnection() throws JMSException {
    return this.createActiveMQConnection();
}
//执行该类下的createActiveMQConnection()方法
protected ActiveMQConnection createActiveMQConnection() throws JMSException {
    return this.createActiveMQConnection(this.userName, this.password);
}

     3.2.3 进入ActiveMQConnectionFactory类下的createActiveMQConnection()方法的构造方法,开始创建连接

protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
    if (this.brokerURL == null) {
        throw new ConfigurationException("brokerURL not set.");
    } else {
        ActiveMQConnection connection = null;

        try {
            //【跳转至步骤3.2.4】创建一个transport传输协议
            Transport transport = this.createTransport();
            connection = this.createActiveMQConnection(transport, this.factoryStats);
            connection.setUserName(userName);
            connection.setPassword(password);
            this.configureConnection(connection);
            //启动
            transport.start();
            if (this.clientID != null) {
                connection.setDefaultClientID(this.clientID);
            }

            return connection;
        } catch (JMSException var8) {
            try {
                connection.close();
            } catch (Throwable var7) {
            }

            throw var8;
        } catch (Exception var9) {
            try {
                connection.close();
            } catch (Throwable var6) {
            }

            throw JMSExceptionSupport.create("Could not connect to broker URL: " + this.brokerURL + ". Reason: " + var9, var9);
        }
    }
}

     3.2.4 如何构建transport(进入本类(ActiveMQConnectionFactory)下的createTransport()方法)

          调用ActiveMQConnectionFactory.createTransport方法,去创建一个transport对象。
              1.构建一个URI
              2.根据URL去创建一个连接TransportFactory.connect
          Ø 默认使用的是tcp的协议

protected Transport createTransport() throws JMSException {
    try {
        URI connectBrokerUL = this.brokerURL;
        //获取brokerUrl的scheme
        String scheme = this.brokerURL.getScheme();
        //根据Scheme来判断
        if (scheme == null) {
            throw new IOException("Transport not scheme specified: [" + this.brokerURL + "]");
        } else {
            if (scheme.equals("auto")) {//Scheme默认为auto
                connectBrokerUL = new URI(this.brokerURL.toString().replace("auto", "tcp"));//将auto替换为tcp
            } else if (scheme.equals("auto+ssl")) {
                connectBrokerUL = new URI(this.brokerURL.toString().replace("auto+ssl", "ssl"));
            } else if (scheme.equals("auto+nio")) {
                connectBrokerUL = new URI(this.brokerURL.toString().replace("auto+nio", "nio"));
            } else if (scheme.equals("auto+nio+ssl")) {
                connectBrokerUL = new URI(this.brokerURL.toString().replace("auto+nio+ssl", "nio+ssl"));
            }
            //【跳转至步骤3.2.5】将brokerUrl设置到TransportFactory中(这里是一个工厂设计模式)
            return TransportFactory.connect(connectBrokerUL);
        }
    } catch (Exception var3) {
        throw JMSExceptionSupport.create("Could not create Transport. Reason: " + var3, var3);
    }
}

     3.2.5 使用工厂模式来创建连接(调用TransportFactory类中的connect方法)

public static Transport connect(URI location) throws Exception {
    //【跳转至步骤3.2.6】通过URI,来获取具体使用的Transport实例(通过步骤3.2.8可以知道该方法默认返回TcpTransportFactory类)
    TransportFactory tf = findTransportFactory(location);
    //【跳转至步骤3.2.9】
    return tf.doConnect(location);
}

     3.2.6 调用TransportFactory类中的findTransportFactory方法

        具体步骤:
            1.从TRANSPORT_FACTORYS这个Map集合中,根据Scheme去获得一个TransportFactory指定的实例对象;
            2.如果Map集合中不存在,则通过TRANSPORT_FACTORY_FINDER去找一个并且构建实例
        Ø 这个地方又有点类似于Java的SPI的思想。它会从META-INF/services/org/apache/activemq/transport/ 这个路径下,根据URI组装的scheme去找到匹配的class对象并且实例化,所以根据tcp为key去对应的路径下可以找到TcpTransportFactory

public static TransportFactory findTransportFactory(URI location) throws IOException {
    //获取Scheme
    String scheme = location.getScheme();
    if (scheme == null) {
        throw new IOException("Transport not scheme specified: [" + location + "]");
    } else {
        //此处类似于SPI思想
        //通过Scheme,从ConcurrentHashMap中拿取一个TransportFactory实例
        TransportFactory tf = (TransportFactory)TRANSPORT_FACTORYS.get(scheme);
        //如果实例为空
        if (tf == null) {
            try {
                //private static final FactoryFinder TRANSPORT_FACTORY_FINDER = new FactoryFinder("META-INF/services/org/apache/activemq/transport/");
                //从TRANSPORT_FACTORY_FINDER去拿(此处又类似于SPI机制,但是和Java SPI机制又有点区别)
                //【跳转至步骤3.2.7】
                tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
                TRANSPORT_FACTORYS.put(scheme, tf);
            } catch (Throwable var4) {
                throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", var4);
            }
        }
        return tf;
    }
}

     3.2.7 使用类似于Java SPI机制来newInstance新建实例

        我们进入META-INF/services/org/apache/activemq/transport/目录下,会看到该目录下的文件,是以文件的名字作为key。默认的话,会读取到名字为tcp的文件,然后拿到文件定义的class=org.apache.activemq.transport.tcp.TcpTransportFactory的Factroy,然后再去newInstance来创建一个实例(相当于改造版的Java SPI机制)

     3.2.8 步骤3.2.6中findTransportFactory方法返回的是个什么对象

        在步骤3.2.6中,经过分析,我们知道该方法返回的是一个TransportFactory,默认的话返回的是一个TcpTransportFactory。

     3.2.9 tf.doConnect(location);根据findTransportFactory返回的类,创建Transport的连接

        在步骤3.2.5中,我们知道tf默认返回的是一个TcpTransportFactory。但是我们在doConnect()方法实现中,并没有找到TcpTransportFactory的实现类。我们通过搜索TcpTransportFactory类,会发现  public class TcpTransportFactory extends TransportFactory,该类继承了TransportFactory父类tory类,所以tf.doConnect(location)方法,实际执行的是父类TransportFactory类中的doConnect()方法,代码如下:

public Transport doConnect(URI location) throws Exception {
    try {
        Map<String, String> options = new HashMap(URISupport.parseParameters(location));
        if (!options.containsKey("wireFormat.host")) {
            options.put("wireFormat.host", location.getHost());
        }
        //此处为具体的TCP协议层实现  start
        WireFormat wf = this.createWireFormat(options);
        //此处为创建一个Transport(重要),此处是一个模版方法.默认情况下会进入TcpTransportFactory类下的createTransport()方法
        //创建一个Transport,创建一个socket连接---->终于找到真相了
        //【跳转至步骤3.2.10】
        Transport transport = this.createTransport(location, wf);
        //configure 【跳转至步骤3.2.12】 配置configure,这个里面是对Transport做的链路包装
        Transport rc = this.configure(transport, wf, options);
		
        //end
		
        IntrospectionSupport.extractProperties(options, "auto.");
        if (!options.isEmpty()) {
            throw new IllegalArgumentException("Invalid connect parameters: " + options);
        } else {
            return rc;
        }
    } catch (URISyntaxException var6) {
        throw IOExceptionSupport.create(var6);
    }
}

     3.2.10 开始TcpTransport创建过程

protected Transport createTransport(URI location, WireFormat wf) throws UnknownHostException, IOException {
    URI localLocation = null;
    String path = location.getPath();
    if (path != null && path.length() > 0) {
        int localPortIndex = path.indexOf(58);

        try {
            Integer.parseInt(path.substring(localPortIndex + 1, path.length()));
            String localString = location.getScheme() + ":/" + path;
            localLocation = new URI(localString);
        } catch (Exception var7) {
            LOG.warn("path isn't a valid local location for TcpTransport to use", var7.getMessage());
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failure detail", var7);
            }
        }
    }
    //Tcp通信是基于Socket通信(NIO通信是基于原生NIO支持或者基于Netty的支持)(此处是重点)
    SocketFactory socketFactory = this.createSocketFactory();
    //【跳转至步骤3.2.11】创建TcpTransport
    return this.createTcpTransport(wf, socketFactory, location, localLocation);
}

     3.2.11 创建TcpTransport

public TcpTransport(WireFormat wireFormat, SocketFactory socketFactory, URI remoteLocation, URI localLocation) throws UnknownHostException, IOException {
    this.connectionTimeout = 30000;
    this.socketBufferSize = 65536;
    this.ioBufferSize = 8192;
    this.closeAsync = true;
    this.buffOut = null;
    this.trafficClass = 0;
    this.trafficClassSet = false;
    this.diffServChosen = false;
    this.typeOfServiceChosen = false;
    this.trace = false;
    this.logWriterName = TransportLoggerSupport.defaultLogWriterName;
    this.dynamicManagement = false;
    this.startLogging = true;
    this.jmxPort = 1099;
    this.useLocalHost = false;
    this.stoppedLatch = new AtomicReference();
    this.soLinger = -2147483648;
    this.wireFormat = wireFormat;
    this.socketFactory = socketFactory;

    try {
        //这个地方,创建一个Socket(TcpTransport其实就是创建了一个连接)
        //【跳转步骤3.2.9】,你就知道了Transport transport = this.createTransport(location, wf);返回的其实是一个连接
        //【步骤3.2.9】中return 的是一个rc,并不是此处的transport,所以我们回到【跳转回步骤3.2.9】分析生成rc的configure方法
        this.socket = socketFactory.createSocket();
    } catch (SocketException var6) {
        this.socket = null;
    }

    this.remoteLocation = remoteLocation;
    this.localLocation = localLocation;
    this.initBuffer = null;
    this.setDaemon(false);
}

     3.2.12  对createTransport()方法返回的连接进行链式包装(共做了3层包装)

public Transport configure(Transport transport, WireFormat wf, Map options) throws Exception {
    //组装一个符合的transport,这里会包装两层,一个是InactivityMonitor,另一个是WireFormatNegotiator
    transport = this.compositeConfigure(transport, wf, options);
    //再做一层包装MutexTransport
    Transport transport = new MutexTransport(transport);
    //包装ResponseCorrelator
    Transport transport = new ResponseCorrelator(transport);
    return transport;
}

    最终包装完毕后,这个transport实际上就是一个调用链了,它的链式结构如下:

        ResponseCorrelator(MutexTransport(WireFormatNegotiator(InactivityMonitor(TcpTransport()))))

    每一层包装表示的意思:

       ResponseCorrelator:用于实现异步请求

       MutexTransport:实现写锁,表示同一时间只允许发送一个请求

       WireFormatNegotiator:实现客户端连接broker的时候先发送数据解析相关的协议信息,比如解析版本号,是否使用缓存等

       InactivityMonitor:用于实现连接成功后的心跳检查机制,客户端每10s发送一次心跳信息。服务端每30s读取一次心跳信息。(心跳机制,用于维持当前连接的一个方式)

接下分析同步/异步发送具体操作

4. 异步发送分析

       此时便可以【退回至步骤2】中,执行具体的同步/异步操作

    4.1 异步发送源码分析

private void doAsyncSendPacket(Command command) throws JMSException {
    try {
        //此处即为传输层transport
        this.transport.oneway(command);
        //此处transport首先会去调用链路包装最外层的 【步骤4.2】中的 ResponseCorrelator类中的oneway方法
    } catch (IOException var3) {
        throw JMSExceptionSupport.create(var3);
    }
}

   4.2 组装ResponseCorrelator类(实际为组装Command信息,设置commondId 和 responseRequired)

public void oneway(Object o) throws IOException {
    //强转
    Command command = (Command)o;
    //设置commondId
    command.setCommandId(this.sequenceGenerator.getNextSequenceId());
    //设置responseRequired
    command.setResponseRequired(false);
    //5.3  此处进行链路组装下一层(即:MutexTransport类)
    this.next.oneway(command);
}

   4.3 组装MutexTransport类,来完成加锁操作

public void oneway(Object command) throws IOException {
    this.writeLock.lock();
    try {
        //【跳转至步骤4.4】此处进行链路组装下一层(即:WireFormatNegotiator类)
        this.next.oneway(command);
    } finally {
        this.writeLock.unlock();
    }
}

   4.4 通过WireFormatNegotiator类,对内容进行解析(该类继承自TransportFilter父类)

public void oneway(Object command) throws IOException {
    ......//省略部分代码
    //【跳转至步骤4.5】调用super.oneway,即 TransportFilter类中的oneway方法
    super.oneway(command);
}

   4.5 调用父类的oneway,发现父类中没做任何操作

public void oneway(Object command) throws IOException {
    //【跳转至步骤4.6】此处进行链路组装下一层(即:InactivityMonitor类)
    this.next.oneway(command);
}

   4.6 发现next.oneway()的实现类没有InactivityMonitor类。

       我们查找InactivityMonitor类可以发现他继承自AbstractInactivityMonitor类,所以【步骤4.5】中进入的链路组装下一层是AbstractInactivityMonitor父类

    4.6.1 AbstractInactivityMonitor父类中的oneway方法

public void oneway(Object o) throws IOException {
    this.sendLock.readLock().lock();
    this.inSend.set(true);

    try {
        //【跳转至步骤4.6.2】调用doOnewaySend方法
        this.doOnewaySend(o);
    } finally {
        this.commandSent.set(true);
        this.inSend.set(false);
        this.sendLock.readLock().unlock();
    }
}

    4.6.2 AbstractInactivityMonitor父类中的doOnewaySend方法

private void doOnewaySend(Object command) throws IOException {
    if (this.failed.get()) {
        throw new InactivityIOException("Cannot send, channel has already failed: " + this.next.getRemoteAddress());
    } else {
        //执行判断
        if (command.getClass() == WireFormatInfo.class) {
            synchronized(this) {
                this.processOutboundWireFormatInfo((WireFormatInfo)command);
            }
        }
        //【跳转至步骤4.6.3】再次执行oneway()方法,最终调用至TcpTransport
        this.next.oneway(command);
    }
}

    4.6.3 进入包装最内层---TcpTransport类

public void oneway(Object command) throws IOException {
    this.checkStarted();
    //Tcp,通过格式化等一系列操作,然后将数据发送至Broker
    this.wireFormat.marshal(command, this.dataOut);
    this.dataOut.flush();
}

5. 同步发送分析

    【回退至步骤2】

    5.1 调用步骤2中的  ActiveMQConnection类下的syncSendPacket方法  执行同步发送操作

public Response syncSendPacket(Command command, int timeout) throws JMSException {
    if (this.isClosed()) {
        throw new ConnectionClosedException();
    } else {
        try {
            //【跳转至步骤5.2】调用this.transport.request()来获取返回response
            Response response = (Response)((Response)(timeout > 0 ? this.transport.request(command, timeout) : this.transport.request(command)));
            ......省略部分代码
        } catch (IOException var9) {
            throw JMSExceptionSupport.create(var9);
        }
    }
}

    5.2 调用包装类  ResponseCorrelator类中的request方法

public Object request(Object command, int timeout) throws IOException {
    //你在这里会发现,可能还是一个异步操作,为什么呢?
    //(会在response.getResult()方法中拿到异步请求的一个结果,只是这个阶段是阻塞的)
    FutureResponse response = this.asyncRequest(command, (ResponseCallback)null);
    //【跳转至步骤5.3】此处有response.getResult
    return response.getResult(timeout);
}

    5.3 调用response.getResult()方法

public Response getResult(int timeout) throws IOException {
    boolean wasInterrupted = Thread.interrupted();

    Response var4;
    try {
        //有一个responseSlot.poll()操作
        Response result = (Response)this.responseSlot.poll((long)timeout, TimeUnit.MILLISECONDS);
		
        ......省略部分代码
}

    5.4 调用responseSlot.poll()操作

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {
            if (nanos <= 0)
                return null;
            //此处有一个等待操作()
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}

至此,你应该知道了同步和异步的差别在哪里了吧?

       一句话总结:同步就是不阻塞发送,阻塞获取结果。

       同步的发送,说到底还是异步发送,只是说从异步请求中拿到一个返回的结果,但是这个过程是阻塞的而已.所以它就实现了一个阻塞的同步发送。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

扛麻袋的少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值