Java进阶:Netty实现RPC的代码

一、RPC是什么

RPC,全称为Remote Procedure Call,即远程过程调用。它允许像调用本地服务一样调用远程服务。

 

个人感觉,与http类似,都需要本地给远程服务器发报文,获取返回信息,因此记录下两者的区别。

 

RPC与http区别:

http://www.ccutu.com/244407.html

RPC可以基于TCP协议,也可以基于HTTP协议;

RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。

 

个人理解,RPC在公司内部的分布式系统中比直接用http方式具有优势,网络传输效率高,具有额外的适合分布式的一些功能(如包含负载均衡策略等),所以分布式系统内部会使用RPC。

 

二、Netty是什么

Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

Netty用到了NIO(同步非阻塞)。

 

Netty、Servlet都属于Web框架:https://blog.csdn.net/dengkane/article/details/84720822

因此记录下Netty与Tomcat(Servlet)的区别:https://www.cnblogs.com/pangguoming/p/9353536.html

Servlet是基于Http协议的;Netty可以通过编程自定义各种协议,因为netty能够通过编程自己来编码/解码字节流。

 

个人感觉,Netty既用到了异步非阻塞,也用到了同步非阻塞。为了避免搞混,在下方简要记录一下。

 

NIO基本概念:

1、缓冲区Buffer

Buffer是一个对象,它包含了一些要写入的或者要读出的数据。

缓冲区Buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

数据是从通道读入缓冲区,从缓冲区写入到通道中的。

 

2、通道Channel

Channel是一个通道,可以通过它进行数据的读取和写入。

数据在channel中可以进行双向的流通,通道可以用于读、写、或同时进行读写。

 

3、多路复用器 Selector

selector会不断轮询注册在其上的Channel,如果某个Channel上有新的Tcp接入,或者有发生读写事件,这个Channel就会处于就绪状态,可以被Selector轮询出来,然后通过selectedKey获取就绪的Channel集合,以便进行后续的IO操作。

一个多路复用器可以同时轮询多个Channel。

 

Netty用到了NIO,个人理解:

首先,NIO模型中,每个客户端Socket请求服务器时,不是直接与服务器建立一个链接,而是与Selector建立一个链接,这样就减小了服务器的压力。(Netty中实现了Selector)

然后,Selector会轮询与客户端的链接,如果有请求,就转发给服务器,并将服务器返回的结果转发给客户端。

Netty可以有多个Selector,每个Selector与服务器建立一个链接即可,减小了服务器的压力。

所以Netty用到了NIO。

 

Netty的所有IO操作都是异步非阻塞的,个人理解:

首先,IO操作异步是说,客户端或Netty会对将数据写入Channel,或从Channel读取数据,这是异步的,进行IO后不会立刻得到结果,而是实际进行IO的部件在完成后,通过状态、通知和回调来通知调用者。

异步的好处是不会造成阻塞,在高并发情形下会更稳定和更高的吞吐量。

 

 

三、Netty实现RPC的代码

通过上方的分析发现,RPC协议适合用于分布式系统;Netty是NIO网络应用程序框架,可以开发客户端程序与服务器程序。

所以这两个组合起来,就能开发分布式系统的客户端程序与服务器程序了。

 

代码样例分3部分:

*使用下方的代码时,import对应的目录结构需要自己调整下。

 

rpc-common:其中定义了一些客户端与服务器端都会用到的JavaBean类与接口类。

要实现RPC,最重要的就是要定义一个客户端与服务器公用的接口类;

客户端调用这个接口类中的方法,看起来是直接获得了处理结果,实际上是给服务器发送请求、服务器处理后返回的结果;

服务器则需要实现这个接口类,编写实际执行的方法,等待客户端请求。

 

rpc-consumer:这个是rpc客户端的代码。

 

rpc-provider:这个是rpc服务器的代码。

 

1.rpc-common部分

(1)IUserService.java,这个是客户端与服务器都会用到的接口类。

public interface IUserService {

    public String sayHello(String smg);

}

(2)RpcRequest.java,这个是自定义了一个Request对象,这个对象中包含客户端要调用的方法的名称、参数等信息,客户端发送这个对象给服务器,服务器解析这个对象。

public class RpcRequest{

    //本次请求的ID,可以自定义一个
    private String requestId;

    //客户端准备调用服务端的类名
    private String className;

    //对应类中的方法名
    private String methodName;

    //调用这个方法时,要传递的参数类型的数组,按顺序
    private Class<?>[] parameterTypes;

    //调用这个方法时,要传递的参数数组,按顺序
    private Object[] parameters;


    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Class<?>[] getParameterTypes() {
        return parameterTypes;
    }

    public void setParameterTypes(Class<?>[] parameterTypes) {
        this.parameterTypes = parameterTypes;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

(3)Serializer.java,这个类是自定义的序列化/反序列化类,用来将java对象(RpcRequest对象)序列化和反序列化,然后就能在网络上传输了(也就是转为字节数组,用对象流传输)。

import java.io.IOException;

public interface Serializer {

    //将java对象转换为二进制,接口方法
    //传入对象,传出字节数组
    byte[] serialize(Object object) throws IOException;



    //将二进制转换成java对象,接口方法
    //传入字节数组与类的类型,返回该类型的对象
    <T> T deserialize(Class<T> clazz, byte[] bytes) throws IOException;

}

(4)JSONSerializer.java,这个类是Serializer.java的实现类,采用了json方式进行序列化/反序列化。

import com.alibaba.fastjson.JSON;

public class JSONSerializer implements Serializer{

    public byte[] serialize(Object object) {

        return JSON.toJSONBytes(object);

    }

    public <T> T deserialize(Class<T> clazz, byte[] bytes) {

        return JSON.parseObject(bytes, clazz);

    }
}

(5)pom.xml,这个是rpc-common用到的依赖。同时要注意下groupId与artifactId,这个在后续的rpc-consumer与rpc-provider中会用到。


    <groupId>com.test</groupId>
    <artifactId>rpc-common</artifactId>
    <version>1.0-SNAPSHOT</version>
   <dependencies>
       <dependency>
           <groupId>io.netty</groupId>
           <artifactId>netty-all</artifactId>
           <version>4.1.16.Final</version>
       </dependency>

       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>fastjson</artifactId>
           <version>1.2.41</version>
       </dependency>
   </dependencies>

 

2.rpc-consumer部分(rpc客户端)

(1)pom.xml,用到的依赖,其中用到了rpc-common的jar包。

<groupId>org.example</groupId>
    <artifactId>rpc-consumer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.test</groupId>
            <artifactId>rpc-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.41</version>
        </dependency>
    </dependencies>

(2)ConsumerBoot.java,客户端的启动类。

import com.test.client.RPCConsumer;
import com.test.service.IUserService;

public class ConsumerBoot {

    //这个字段的意思是,要调用UserService类中的sayHello方法
    private static final String PROVIDER_NAME = "UserService#sayHello#";

    public static void main(String[] args) throws InterruptedException {


        //1.创建代理对象
        IUserService service = (IUserService) RPCConsumer.createProxy(IUserService.class, PROVIDER_NAME);

        //2.循环给服务器写数据
        while (true){
            //调用这个方法后,实际上就给服务器发请求并获取返回结果了,不过看起来就像调用本地方法一样。这就是rpc。
            String result = service.sayHello("are you ok !!");
            System.out.println(result);
            Thread.sleep(2000);
        }

    }
}

(3)RPCConsumer.java,客户端消费者类,有创建代理对象等一系列功能。

import com.test.bean.RpcRequest;
import com.test.encoder.RpcEncoder;
import com.test.handler.UserClientHandler;
import com.test.serializer.JSONSerializer;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 客户端消费者类
 */
public class RPCConsumer {
    //自定义的请求id,从0开始自增
    private static int requestId = 0;

    //1.创建一个线程池对象  -- 它要处理我们自定义事件
    private static ExecutorService executorService =
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    //2.声明一个自定义事件处理器  UserClientHandler
    private static UserClientHandler userClientHandler;


    //3.编写方法,初始化客户端  ( 创建连接池  创建bootStrap  设置bootstrap  连接服务器)
    public static void initClient() throws InterruptedException {
        //1) 初始化UserClientHandler
        userClientHandler  = new UserClientHandler();
        //2)创建连接池对象
        EventLoopGroup group = new NioEventLoopGroup();
        //3)创建客户端的引导对象
        Bootstrap bootstrap =  new Bootstrap();
        //4)配置启动引导对象
        bootstrap.group(group)
                //设置通道为NIO
                .channel(NioSocketChannel.class)
                //设置请求协议为TCP
                .option(ChannelOption.TCP_NODELAY,true)
                //监听channel 并初始化
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //获取ChannelPipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //设置如何编码,使用自定义json序列化类编码
                        pipeline.addLast(new RpcEncoder(RpcRequest.class, new JSONSerializer()));
                        //设置如何解码
                        pipeline.addLast(new StringDecoder());
                        //添加自定义事件处理器
                        pipeline.addLast(userClientHandler);
                    }
                });

        //5)连接服务端
        bootstrap.connect("127.0.0.1",8999).sync();
    }

    //4.编写一个方法,使用JDK的动态代理创建对象
    // serviceClass 接口类型,根据哪个接口生成子类代理对象;   providerParam :  "UserService#sayHello#"
    public static Object createProxy(Class<?> serviceClass, final String providerParam){
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{serviceClass}, new InvocationHandler() {
                    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

                        //封装好rpcRequest
                        RpcRequest rpcRequest = getRpcRequest(providerParam, objects[0]);


                        //1)初始化客户端client
                        if(userClientHandler == null){
                            initClient();
                        }

                        //2)给UserClientHandler 设置param参数
                        userClientHandler.setParam(rpcRequest);

                        //3).使用线程池,开启一个线程处理处理call() 写操作,并返回结果
                        Object result = executorService.submit(userClientHandler).get();

                        //4)return 结果
                        return result;
                    }
                });
    }

    //封装rpcRequest对象的方法
    public static RpcRequest getRpcRequest(String providerParam, Object object){
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setRequestId(String.valueOf(requestId));
        requestId++;
        rpcRequest.setClassName(providerParam.split("#")[0]);
        rpcRequest.setMethodName(providerParam.split("#")[1]);
        rpcRequest.setParameterTypes(new Class[]{String.class});
        //are you ok !!
        rpcRequest.setParameters(new Object[]{object});
        return rpcRequest;
    }

}

(4)RpcEncoder.java,上方设置pipeline如何编码时用到了。

import com.test.serializer.Serializer;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

//注意继承的是MessageToByteEncoder类
public class RpcEncoder extends MessageToByteEncoder {

    private Class<?> clazz;

    private Serializer serializer;

    public RpcEncoder(Class<?> clazz, Serializer serializer) {

        this.clazz = clazz;

        this.serializer = serializer;
    }



    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf byteBuf) throws Exception {

        if (clazz != null && clazz.isInstance(msg)) {

            byte[] bytes = serializer.serialize(msg);

            byteBuf.writeBytes(bytes);
        }
    }
}

(5)UserClientHandler.java,自定义事件处理器。

import com.test.bean.RpcRequest;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.concurrent.Callable;

/**
 *  自定义事件处理器
 */
public class UserClientHandler extends ChannelInboundHandlerAdapter implements Callable {

    //1.定义成员变量
    private ChannelHandlerContext context; //事件处理器上下文对象 (存储handler信息,写操作)
    private String result; // 记录服务器返回的数据
    private RpcRequest rpcRequest; //将要发送给服务器的数据(自定义的类对象)

    //2.实现channelActive  客户端和服务器连接时,该方法就自动执行
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //初始化ChannelHandlerContext
        this.context = ctx;
    }


    //3.实现channelRead 当我们读到服务器数据,该方法自动执行
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //将读到的服务器的数据msg ,设置为成员变量的值
        result = msg.toString();
        //唤醒线程,线程才会继续执行下方的return result;
        notify();
    }

    //4.将客户端的数写到服务器
    public synchronized Object call() throws Exception {
        //context给服务器写数据
        context.writeAndFlush(rpcRequest);
        //使用wait阻塞自己,等待服务器返回数据
        wait();
        //在上方notify()后,才继续执行,return服务器返回的信息
        return result;
    }

    //5.设置参数的方法
    public void setParam(RpcRequest param){
        this.rpcRequest = param;
    }
}

 

3.rpc-provider部分(rpc服务器)

(1)pom.xml,依赖信息,其中用到了rpc-common的jar包;服务器写成了springboot项目,因此还有springboot相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.test</groupId>
	<artifactId>rpc-provider</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>myspringboot</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>8</java.version>
		<mybatis-springboot-starter.version>1.3.0</mybatis-springboot-starter.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>com.test</groupId>
			<artifactId>rpc-common</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.41</version>
		</dependency>
    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

(2)ServerBootstrap.java,rpc服务端的启动类。

import com.test.service.IUserService;
import com.test.service.UserServiceImpl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerBootstrap {

	@Autowired
    @Qualifier("UserService")
	IUserService userServiceImpl;

	public static void main(String[] args) throws InterruptedException {
		//启动spring容器
		SpringApplication.run(ServerBootstrap.class, args);
		//启动服务器
		UserServiceImpl.startServer("127.0.0.1",8999);

	}

}

(3)UserServiceImpl.java,实现类,客户端相当于实际调用了这个类中的方法。(客户端给服务器发请求,服务器调用方法,然后将结果返回给客户端。)

import com.test.bean.RpcRequest;
import com.test.decoder.RpcDecoder;
import com.test.handler.UserServiceHandler;
import com.test.serializer.JSONSerializer;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

@Service("UserService")
public class UserServiceImpl implements IUserService,ApplicationContextAware {

    //spring容器对象,通过实现ApplicationContextAware获取到
    private static ApplicationContext applicationContext;

    //将来客户端要远程调用的方法
    public String sayHello(String msg) {
        System.out.println("客户端发来数据:"+msg);
        return "服务器返回数据 : "+msg+"[success]";
    }


    //创建一个方法启动服务器
    public static void startServer(String ip , int port) throws InterruptedException {
        //1.创建两个线程池对象
        NioEventLoopGroup  bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup  workGroup = new NioEventLoopGroup();

        //2.创建服务端的启动引导对象
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        //3.配置启动引导对象
        serverBootstrap.group(bossGroup,workGroup)
                //设置通道为NIO
                .channel(NioServerSocketChannel.class)
                //创建监听channel
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        //获取管道对象
                        ChannelPipeline pipeline = nioSocketChannel.pipeline();
                        //给管道对象pipeLine 设置编码方式
                        pipeline.addLast(new StringEncoder());
                        //给管道对象pipeLine 设置解码方式,使用自定义json方式解码
                        pipeline.addLast(new RpcDecoder(RpcRequest.class, new JSONSerializer()));
                        //把自定义的一个ChannelHander添加到通道中
                        pipeline.addLast(new UserServiceHandler(applicationContext));
                    }
                });

        //4.绑定端口
        serverBootstrap.bind(8999).sync();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

(4)UserServiceHandler.java,自定义业务处理器,包含处理从客户端收到的信息的方法,以及返回给客户端信息的方法。

import com.test.bean.RpcRequest;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.springframework.context.ApplicationContext;

import java.lang.reflect.Method;

/**
 * 自定义的业务处理器
 */
public class UserServiceHandler extends ChannelInboundHandlerAdapter {
    //spring容器对象
    private ApplicationContext applicationContext;
    //构造方法,给spring容器对象赋值
    public UserServiceHandler(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    //当客户端发来数据时,该方法会被调用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //将客户端发来的对象强转为RpcRequest。(约定好的,本来就是RpcRequest)
        RpcRequest rpcRequest = (RpcRequest)msg;
        //从spring容器中获得要调用的类,名字是UserService,实际的类型是UserServiceImpl
        Object bean = applicationContext.getBean(rpcRequest.getClassName());
        //用反射获得UserServiceImpl对象
        //com.test.UserServiceImpl
        //要注意这个类的路径
        Class c = Class.forName(bean.getClass().getPackage().getName()+ "." + rpcRequest.getClassName() +"Impl");
        //然后用反射获得这个类的方法
        //sayHello
        Method method = c.getDeclaredMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());

        System.out.println("当前请求号:"+rpcRequest.getRequestId());
        //注意method中传入的是spring容器中取出的bean,而不是c.newInstance()
        //are you ok!!
        Object invoke = method.invoke(bean, rpcRequest.getParameters());
        //把调用实现类的方法获得的结果写到客户端
        ctx.writeAndFlush(invoke);

    }
}

(5)RpcDecoder.java,自定义json格式解码用到的类。

import com.test.serializer.Serializer;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

//注意解码继承的是ByteToMessageDecoder
public class RpcDecoder extends ByteToMessageDecoder {

    private Class<?> clazz;

    private Serializer serializer;

    public RpcDecoder(Class<?> clazz, Serializer serializer) {

        this.clazz = clazz;

        this.serializer = serializer;

    }



    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if (clazz != null && serializer != null) {
            byte[] bytes = new byte[byteBuf.readableBytes()];
            byteBuf.readBytes(bytes);
            //如果不想改变byteBuf的读写位置,可以用getBytes
            //int readerIndex = byteBuf.readerIndex();
            //byteBuf.getBytes(readerIndex, bytes);
            //这个就是RpcRequest对象
            Object obj = serializer.deserialize(clazz, bytes);

            //注意这里只添加了一个RpcRequest对象
            //如果list中添加了多个元素,则handler中的channelRead(ChannelHandlerContext ctx, Object msg)方法会被多次调用,按顺序一次给msg传入一个元素
            list.add(obj);
        }
    }
}

以上,Netty实现RPC的代码样例就完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

追逐梦想永不停

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

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

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

打赏作者

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

抵扣说明:

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

余额充值