Netty源码(二)EventLoop的创建

EventLoop

因为最常用的就是NioEventLoop,所以主要看下NioEventLoop的实现

类继承结构

在这里插入图片描述

EventExecutor

EventExecutor的作用主要分为两个部分:

  1. 一部分来自AbstractExecutorService,因此具有提交任务,运行任务的能力
  2. 另一部分的能力是自己定义的,用来判断一个线程是否在EventLoop中
    下面看下EventExecutor自己定义了哪些方法
public interface EventExecutor extends EventExecutorGroup {

    /**
     * Returns a reference to itself.
     */
     // 一直返回自己
    @Override
    EventExecutor next();

    /**
     * Return the {@link EventExecutorGroup} which is the parent of this {@link EventExecutor},
     */
     // 返回当前EventExecutor属于的EventExecutorGroup
    EventExecutorGroup parent();

    /**
     * Calls {@link #inEventLoop(Thread)} with {@link Thread#currentThread()} as argument
     */
     // 判断当前线程是否在eventLoop中
    boolean inEventLoop();

    /**
     * Return {@code true} if the given {@link Thread} is executed in the event loop,
     * {@code false} otherwise.
     */
    // 判断指定线程是否在eventLoop中
    boolean inEventLoop(Thread thread);

    /**
     * Return a new {@link Promise}.
     */
    <V> Promise<V> newPromise();

    /**
     * Create a new {@link ProgressivePromise}.
     */
    <V> ProgressivePromise<V> newProgressivePromise();

    /**
     * Create a new {@link Future} which is marked as successes already. So {@link Future#isSuccess()}
     * will return {@code true}. All {@link FutureListener} added to it will be notified directly. Also
     * every call of blocking methods will just return without blocking.
     */
    <V> Future<V> newSucceededFuture(V result);

    /**
     * Create a new {@link Future} which is marked as fakued already. So {@link Future#isSuccess()}
     * will return {@code false}. All {@link FutureListener} added to it will be notified directly. Also
     * every call of blocking methods will just return without blocking.
     */
    <V> Future<V> newFailedFuture(Throwable cause);
}

OrderedEventExecutor

EventExecutor的标记接口,会按照一定的顺序来执行提交的任务

public interface OrderedEventExecutor extends EventExecutor {
}

AbstractEventExecutor

AbstractEventExecutor是EventExecutor的一个抽象类,实现了部分方法

重要属性

// 所属的ExecutorGroup
private final EventExecutorGroup parent;
// eventExecutor集合,只有自己一个
private final Collection<EventExecutor> selfCollection = Collections.<EventExecutor>singleton(this);

next

当调用next方法来获取一个EventExecutor时,会一直返回自身

@Override
public EventExecutor next() {
    return this;
}

inEventLoop

当调用inEventLoop判断当前线程是否在eventLoop中时,会调用子类的实现

@Override
public boolean inEventLoop() {
    return inEventLoop(Thread.currentThread());
}

submit

会调用父类AbstractEventExecutor的submit

@Override
public Future<?> submit(Runnable task) {
    return (Future<?>) super.submit(task);
}

schedule

不支持schedule

@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
    throw new UnsupportedOperationException();
}

iterator

返回用来遍历eventExecutor的迭代器

@Override
public Iterator<EventExecutor> iterator() {
   return selfCollection.iterator();
}

newXXXPromise

Promise是特殊的Future,主要是支持手动设置成功结果和失败原因
可以看到下面创建的Promise都会将当前的eventExecutor实例传入

@Override
public <V> Promise<V> newPromise() {
    return new DefaultPromise<V>(this);
}

@Override
public <V> ProgressivePromise<V> newProgressivePromise() {
    return new DefaultProgressivePromise<V>(this);
}

newXXXFuture

可以看到下面创建的Future都会将当前eventExecutor传入

@Override
public <V> Future<V> newSucceededFuture(V result) {
    return new SucceededFuture<V>(this, result);
}

@Override
public <V> Future<V> newFailedFuture(Throwable cause) {
    return new FailedFuture<V>(this, cause);
}

newTaskFor

在创建PromiseTask的时候,会将当前的executor作为参数传入

@Override
protected final <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new PromiseTask<T>(this, runnable, value);
}

@Override
protected final <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new PromiseTask<T>(this, callable);
}

SingleThreadEventExecutor

AbstractEventExecutor的一个子类,其子类的实现类在一个线程中来执行所有提交的任务

重要属性

// 任务队列中最多pending任务个数
static final int DEFAULT_MAX_PENDING_EXECUTOR_TASKS = Math.max(16,
            SystemPropertyUtil.getInt("io.netty.eventexecutor.maxPendingTasks", Integer.MAX_VALUE));
// 当前状态
// 未启动
private static final int ST_NOT_STARTED = 1;
// 已启动
private static final int ST_STARTED = 2;
// 正在关闭
private static final int ST_SHUTTING_DOWN = 3;
// 已关闭
private static final int ST_SHUTDOWN = 4;
// 已终结
private static final int ST_TERMINATED = 5;

// 任务队列
private final Queue<Runnable> taskQueue;
// 执行任务的线程
private volatile Thread thread;
// 执行器
private final Executor executor;
// 最多pending任务个数
private final int maxPendingTasks;

构造方法

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, int maxPendingTasks,
                                        RejectedExecutionHandler rejectedHandler) {
    super(parent);
    // 是否只有在调用addTask来向任务队列中添加任务时才唤醒executor线程
    this.addTaskWakesUp = addTaskWakesUp;
    // 最大pedning任务个数
    this.maxPendingTasks = Math.max(16, maxPendingTasks);
    this.executor = ObjectUtil.checkNotNull(executor, "executor");
    // 任务队列
    taskQueue = newTaskQueue(this.maxPendingTasks);
    rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}

inEventLoop

判断指定的线程是否在当前eventLoop中

@Override
public boolean inEventLoop(Thread thread) {
    return thread == this.thread;
}

offerTask

final boolean offerTask(Runnable task) {
	// 判断当前状态是否已经关闭或者终结
    if (isShutdown()) {
    	// 抛出拒绝异常
        reject();
    }
    // 向任务队列中添加任务
    return taskQueue.offer(task);
}

addTask

protected void addTask(Runnable task) {
	// 判断待添加的任务是否是空,如果为空抛出异常
    if (task == null) {
        throw new NullPointerException("task");
    }
    // 向任务队列添加任务,如果失败调用reject
    if (!offerTask(task)) {
        reject(task);
    }
}

peekTask

查看当前任务队列中队头的任务,要求当前线程在eventLoop中

protected Runnable peekTask() {
    assert inEventLoop();
    return taskQueue.peek();
}

removeTask

从队列中移除任务

protected boolean removeTask(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }
    return taskQueue.remove(task);
}

reject

当添加任务失败的时候,会调用reject方法

protected final void reject(Runnable task) {
   rejectedExecutionHandler.rejected(task, this);
}

RejectedExecutionHandlers这个类中提供了RejectedExecutorHandler的一些实现类
下面看下源码

public final class RejectedExecutionHandlers {
    private static final RejectedExecutionHandler REJECT = new RejectedExecutionHandler() {
        @Override
        public void rejected(Runnable task, SingleThreadEventExecutor executor) {
            throw new RejectedExecutionException();
        }
    };

    private RejectedExecutionHandlers() { }

    /**
     * Returns a {@link RejectedExecutionHandler} that will always just throw a {@link RejectedExecutionException}.
     */
    // 第一种实现,直接抛出异常
    public static RejectedExecutionHandler reject() {
        return REJECT;
    }

    /**
     * Tries to backoff when the task can not be added due restrictions for an configured amount of time. This
     * is only done if the task was added from outside of the event loop which means
     * {@link EventExecutor#inEventLoop()} returns {@code false}.
     */
     // 第二种实现,会尝试将任务添加到队列中,失败重试
    public static RejectedExecutionHandler backoff(final int retries, long backoffAmount, TimeUnit unit) {
        ObjectUtil.checkPositive(retries, "retries");
        final long backOffNanos = unit.toNanos(backoffAmount);
        return new RejectedExecutionHandler() {
            @Override
            public void rejected(Runnable task, SingleThreadEventExecutor executor) {
                if (!executor.inEventLoop()) {
                	// 循环尝试将任务添加到队列中
                    for (int i = 0; i < retries; i++) {
                    	// 尝试唤醒执行器,这样任务队列中的任务就会减少
                        // Try to wakup the executor so it will empty its task queue.
                        executor.wakeup(false);
						
						// 阻塞等待指定的时间
                        LockSupport.parkNanos(backOffNanos);
                        // 尝试将任务添加到队列中
                        if (executor.offerTask(task)) {
                            return;
                        }
                    }
                }
                // Either we tried to add the task from within the EventLoop or we was not able to add it even with
                // backoff.
                throw new RejectedExecutionException();
            }
        };
    }
}

execute

下面看下比较重要的方法execute,看是如何执行任务的

public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }

	// 判断当前线程是否是eventLoop绑定的线程
    boolean inEventLoop = inEventLoop();
    // 将任务添加到任务队列中
    addTask(task);
    if (!inEventLoop) {
    	// 不在eventLoop绑定的线程,启动线程
        startThread();
        // 如果eventLoop已经关闭,并且移除任务失败,那么调用reject方法
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

	// 如果addTaskWakesUp设置为false,那么会唤醒
	// addTaskWakesUp代表当向任务队列中添加任务时,是否会自动唤醒
	// 如果不会自动唤醒,则需要调用wakeup进行主动唤醒
	// wakesupForTask方法默认返回true,子类可以有自己的实现
    if (!addTaskWakesUp && wakesUpForTask(task)) {
    	// 唤醒
        wakeup(inEventLoop);
    }
}

startThread

private void startThread() {
	// 判断当前的状态是否是ST_NOT_STARTED
    if (state == ST_NOT_STARTED) {
    	// 将当前状态设置为ST_STARTED
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            try {
            	// 真正启动线程的逻辑
                doStartThread();
            } catch (Throwable cause) {
            	// 如果启动失败,将状态设置为ST_NOT_STARTED
                STATE_UPDATER.set(this, ST_NOT_STARTED);
                PlatformDependent.throwException(cause);
            }
        }
    }
}
private void doStartThread() {
	// 判断线程是否是空
    assert thread == null;
    // 将任务提交给executor进行执行
    executor.execute(new Runnable() {
        @Override
        public void run() {
        	// 将thread设置为执行当前任务的线程,这个就是每个SingleThreadEventExecutor独占线程的实现方式
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            // 更新最近执行时间
            updateLastExecutionTime();
            try {
            	// 执行任务
                SingleThreadEventExecutor.this.run();
                success = true;
            } catch (Throwable t) {
                logger.warn("Unexpected exception from an event executor: ", t);
            } finally {
            	// 优雅关闭,将状态设置为ST_SHUTTING_DOWN
                for (;;) {
                    int oldState = state;
                    if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                            SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                        break;
                    }
                }

                // Check if confirmShutdown() was called at the end of the loop.
                if (success && gracefulShutdownStartTime == 0) {
                    if (logger.isErrorEnabled()) {
                        logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
                                SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must " +
                                "be called before run() implementation terminates.");
                    }
                }

                try {
                	// 运行剩余的任务和关闭钩子
                    // Run all remaining tasks and shutdown hooks.
                    for (;;) {
                        if (confirmShutdown()) {
                            break;
                        }
                    }
                } finally {
                    try {
                    	// 清理资源
                        cleanup();
                    } finally {
                    	// 将状态设置为ST_TERMINATED
                        STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                        threadLock.release();
                        if (!taskQueue.isEmpty()) {
                            if (logger.isWarnEnabled()) {
                                logger.warn("An event executor terminated with " +
                                        "non-empty task queue (" + taskQueue.size() + ')');
                            }
                        }

                        terminationFuture.setSuccess(null);
                    }
                }
            }
        }
    });
}

run

SingleThreadEventExecutor中的run方法是一个抽象方法,由子类来实现

protected abstract void run();

invokeAll

invokeAll执行给定的多个任务,等待所有任务执行完毕

public <T> List<java.util.concurrent.Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
            throws InterruptedException {
    // 判断当前线程是否在EventLoop中
    throwIfInEventLoop("invokeAll");
    // 调用父类的invokeAll方法
    return super.invokeAll(tasks);
}
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    if (tasks == null)
        throw new NullPointerException();
    ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
    boolean done = false;
    try {
    	// 为每个任务创建一个RunableFuture,并且添加到futures列表中
    	// 然后执行该任务
        for (Callable<T> t : tasks) {
            RunnableFuture<T> f = newTaskFor(t);
            futures.add(f);
            execute(f);
        }
        // 阻塞等待所有任务执行完毕
        for (int i = 0, size = futures.size(); i < size; i++) {
            Future<T> f = futures.get(i);
            if (!f.isDone()) {
                try {
                    f.get();
                } catch (CancellationException ignore) {
                } catch (ExecutionException ignore) {
                }
            }
        }
        done = true;
        return futures;
    } finally {
        if (!done)
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
    }
}

invokeAny

给定多个任务,只要其中一个执行完毕就返回

@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
	// 判断当前线程是否在eventloop中
    throwIfInEventLoop("invokeAny");
    // 调用父类的invokeAny方法
    return super.invokeAny(tasks);
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
    try {
        return doInvokeAny(tasks, false, 0);
    } catch (TimeoutException cannotHappen) {
        assert false;
        return null;
    }
}
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                              boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
    if (tasks == null)
        throw new NullPointerException();
    int ntasks = tasks.size();
    if (ntasks == 0)
        throw new IllegalArgumentException();
    ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
    // 当向ExecutorCompletionService提交多个任务后
    // 调用take方法会返回已经完成的任务的future,如果当前没有已经完成的任务,那么会阻塞等待
    // 调用poll方法和take相似,只是如果当前没有已经完成的任务,会返回Null
    ExecutorCompletionService<T> ecs =
        new ExecutorCompletionService<T>(this);

    // For efficiency, especially in executors with limited
    // parallelism, check to see if previously submitted tasks are
    // done before submitting more of them. This interleaving
    // plus the exception mechanics account for messiness of main
    // loop.

    try {
        // Record exceptions so that if we fail to obtain any
        // result, we can throw the last exception we got.
        ExecutionException ee = null;
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        Iterator<? extends Callable<T>> it = tasks.iterator();

        // Start one task for sure; the rest incrementally
        // 渐进式地提交任务,并不是一次将所有的任务都提交
        futures.add(ecs.submit(it.next()));
        // 减少待提交任务数量
        --ntasks;
        // 增加当前正在运行的任务数量
        int active = 1;

        for (;;) {
        	// 取出当前已经运行成功的任务的future
            Future<T> f = ecs.poll();
           	// 为null代表当前没有完成的任务
            if (f == null) {
            	// 如果仍然有没有提交的任务,那么提交一个
                if (ntasks > 0) {
                	// 减少待提交任务数量
                    --ntasks;
                    // 提交任务
                    futures.add(ecs.submit(it.next()));
                    // 增加当前正在运行的任务数量
                    ++active;
                }
                // 提交的任务全部执行完成,退出循环
                else if (active == 0)
                    break;
                else if (timed) {
                    f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                    if (f == null)
                        throw new TimeoutException();
                    nanos = deadline - System.nanoTime();
                }
                else
                    f = ecs.take();
            }
            // 有执行成功的任务
            if (f != null) {
                --active;
                try {
                	// 返回执行结果
                    return f.get();
                } catch (ExecutionException eex) {
                    ee = eex;
                } catch (RuntimeException rex) {
                    ee = new ExecutionException(rex);
                }
            }
        }

        if (ee == null)
            ee = new ExecutionException();
        throw ee;

    } finally {
        for (int i = 0, size = futures.size(); i < size; i++)
            futures.get(i).cancel(true);
    }
}

wakeup

protected void wakeup(boolean inEventLoop) {
 // 如果在eventloop中,代表eventloop正在运行
 if (!inEventLoop || state == ST_SHUTTING_DOWN) {
        // Use offer as we actually only need this to unblock the thread and if offer fails we do not care as there
        // is already something in the queue.
        // 向任务队列中添加唤醒任务
        taskQueue.offer(WAKEUP_TASK);
    }
}

SingleThreadEventLoop

重要属性

// 默认最大pending任务个数
protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16,
            SystemPropertyUtil.getInt("io.netty.eventLoop.maxPendingTasks", Integer.MAX_VALUE));
// 尾部任务队列,执行在taskQueue后面
private final Queue<Runnable> tailTasks;

register

@Override
public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    // 将channel注册到eventloop上
    promise.channel().unsafe().register(this, promise);
    return promise;
}

hasTasks

判断是否有没有执行的任务
结合判断taskQueue和tailTask

protected boolean hasTasks() {
    return super.hasTasks() || !tailTasks.isEmpty();
}

pendingTasks

获取pending任务的个数

@Override
public int pendingTasks() {
    return super.pendingTasks() + tailTasks.size();
}

executeAfterEventLoopIteration

public final void executeAfterEventLoopIteration(Runnable task) {
    ObjectUtil.checkNotNull(task, "task");
    // 如果已经关闭,那么拒绝,并且执行拒绝回调
    if (isShutdown()) {
        reject();
    }

	// 向tailTask队列中添加任务
    if (!tailTasks.offer(task)) {
    	// 添加失败,拒绝任务
        reject(task);
    }
	
	// 唤醒
    if (wakesUpForTask(task)) {
        wakeup(inEventLoop());
    }
}

removeAfterEventLoopIterationTask

移除指定的任务

final boolean removeAfterEventLoopIterationTask(Runnable task) {
    return tailTasks.remove(ObjectUtil.checkNotNull(task, "task"));
}

afterRunningAllTasks

运行tailTask中的所有任务

protected void afterRunningAllTasks() {
    runAllTasksFrom(tailTasks);
}

NioEventLoop

重要属性

private static final int CLEANUP_INTERVAL = 256; // XXX Hard-coded value, but won't need customization.

// 是否禁用keyset优化
private static final boolean DISABLE_KEYSET_OPTIMIZATION =
        SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false);
// 空轮询创建新的selector最少的轮询次数
private static final int MIN_PREMATURE_SELECTOR_RETURNS = 3;
// 空轮询指定次数后,创建新的selector对象
private static final int SELECTOR_AUTO_REBUILD_THRESHOLD;

// Workaround for JDK NIO bug.
//
// See:
// - http://bugs.sun.com/view_bug.do?bug_id=6427854
// - https://github.com/netty/netty/issues/203
static {
    final String key = "sun.nio.ch.bugLevel";
    final String buglevel = SystemPropertyUtil.get(key);
    if (buglevel == null) {
        try {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    System.setProperty(key, "");
                    return null;
                }
            });
        } catch (final SecurityException e) {
            logger.debug("Unable to get/set System Property: " + key, e);
        }
    }

    int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
    if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
        selectorAutoRebuildThreshold = 0;
    }

    SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

    if (logger.isDebugEnabled()) {
        logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEYSET_OPTIMIZATION);
        logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD);
    }
}

/**
 * The NIO {@link Selector}.
 */
// 包装过的selector
private Selector selector;
// 未包装过的selector
private Selector unwrappedSelector;
// 注册的事件
private SelectedSelectionKeySet selectedKeys;
// 用于创建selector
private final SelectorProvider provider;

/**
 * Boolean that controls determines if a blocked Selector.select should
 * break out of its selection process. In our case we use a timeout for
 * the select method and the select method will block for that time unless
 * waken up.
 */
// 唤醒标记,用来决定selector.select是否需要停止
private final AtomicBoolean wakenUp = new AtomicBoolean();
// select策略
private final SelectStrategy selectStrategy;
// 处理io事件占比
private volatile int ioRatio = 50;
// 取消的selectionKey的数量
private int cancelledKeys;
// 是否需要再次select
private boolean needsToSelectAgain;

构造方法

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

newTaskQueue

覆写父类版本
返回的是一个MpscQueue,是一个适用于多个生产线程和一个消费线程的队列

protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
        // This event loop never calls takeTask()
    return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
                                                : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}
```
### pendingTasks
```java
public int pendingTasks() {
    // As we use a MpscQueue we need to ensure pendingTasks() is only executed from within the EventLoop as
    // otherwise we may see unexpected behavior (as size() is only allowed to be called by a single consumer).
    // See https://github.com/netty/netty/issues/5297
    // 因为使用的线程是多个生成线程,单个消费线程的队列,因此查看队列长度只允许在消费线程中进行
    if (inEventLoop()) {
        return super.pendingTasks();
    } else {
    	// 不在eventLoop中,即不在消费线程中,需要通过提交任务的方式来实现
    	// 提交的任务会在消费线程中执行
        return submit(pendingTasksCallable).syncUninterruptibly().getNow();
    }
}
```
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值