Netty 实现 HTTP 协议教程

Netty 实现 HTTP 协议教程

 

一、引言

Netty 是一个异步的、事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。在本教程中,我们将学习如何使用 Netty 实现 HTTP 协议。我们将从基础知识开始,逐步深入到自定义协议的实现。

二、Netty 简介

 

Netty 是一个广泛使用的 Java 网络编程框架,它提供了一组丰富的 API,用于构建高性能、可扩展的网络应用程序。Netty 基于非阻塞 I/O 模型,支持多种协议,如 TCP、UDP、HTTP 等。它的设计目标是提供一种简单而强大的方式来处理网络通信,同时确保高并发和低延迟。

 

三、准备工作

 

在开始之前,我们需要确保已经安装了 JDK 1.8 或更高版本。接下来,我们需要添加 Netty 的依赖到我们的项目中。可以通过 Maven 或 Gradle 来管理项目依赖。以下是 Maven 项目的依赖配置:

<dependencies>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.72.Final</version>
    </dependency>
</dependencies>

 

四、HTTP 协议基础

 

HTTP(HyperText Transfer Protocol)是一种用于在 Web 上传输数据的协议。它是基于请求-响应模型的,客户端向服务器发送请求,服务器返回响应。HTTP 请求和响应都由起始行、头部字段和主体组成。

 

HTTP 请求的起始行包含请求方法、请求 URI 和 HTTP 版本。例如,GET /index.html HTTP/1.1 表示一个 GET 请求,请求的 URI 为 /index.html,使用的 HTTP 版本为 1.1。

 

HTTP 响应的起始行包含 HTTP 版本、状态码和状态消息。例如,HTTP/1.1 200 OK 表示一个成功的响应,使用的 HTTP 版本为 1.1,状态码为 200,状态消息为 OK。

 

头部字段用于传递关于请求或响应的附加信息,如 Content-Type、Content-Length、User-Agent 等。主体则用于携带请求或响应的数据,如 HTML 页面、JSON 数据等。

 

五、使用 Netty 实现 HTTP 服务器

 

接下来,我们将使用 Netty 实现一个简单的 HTTP 服务器,该服务器将监听端口 8080,并处理客户端的 GET 请求,返回一个简单的 HTML 页面

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpServerCodec;

public class HttpServer {

    public static void main(String[] args) throws InterruptedException {
        // 创建 boss 和 worker 线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建服务器引导程序
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                   .channel(NioServerSocketChannel.class)
                   .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 添加 HTTP 编解码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            // 添加 HTTP 请求消息聚合器
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            // 添加自定义的 HTTP 处理逻辑
                            ch.pipeline().addLast(new HttpHandler());
                        }
                    })
                   .option(ChannelOption.SO_BACKLOG, 128)
                   .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 绑定端口并启动服务器
            ChannelFuture future = bootstrap.bind(8080).sync();

            // 等待服务器关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭线程组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

 

在上述代码中,我们首先创建了两个 NioEventLoopGroup 对象,一个用于接收连接(bossGroup),一个用于处理连接的 I/O 操作(workerGroup)。然后,我们创建了一个 ServerBootstrap 对象,用于配置和启动服务器。

 

在 ServerBootstrap 的配置中,我们指定了使用 NioServerSocketChannel 作为服务器的通道类型,并在 childHandler 中添加了 HTTP 编解码器 HttpServerCodec、HTTP 请求消息聚合器 HttpObjectAggregator 和自定义的 HttpHandler

 

HttpServerCodec 用于将 HTTP 请求和响应编码和解码为 HttpRequest 和 HttpResponse 对象。HttpObjectAggregator 用于将多个 HTTP 消息片段聚合为一个完整的 HttpContent 对象。

 

最后,我们使用 bind 方法绑定端口 8080 并启动服务器,然后使用 closeFuture 方法等待服务器关闭。

 

接下来,我们来实现自定义的 HttpHandler

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;

public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        // 处理 GET 请求
        if (request.method().equals(HttpMethod.GET)) {
            // 创建响应
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
                    Unpooled.wrappedBuffer("<h1>Hello, Netty HTTP Server!</h1>".getBytes()));

            // 设置响应头部
            response.headers().set("Content-Type", "text/html; charset=UTF-8");
            response.headers().set("Content-Length", response.content().readableBytes());

            // 发送响应
            ctx.writeAndFlush(response);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

 

在 HttpHandler 中,我们继承了 SimpleChannelInboundHandler<FullHttpRequest>,并重写了 channelRead0 方法来处理接收到的 FullHttpRequest 对象。在这个方法中,我们首先检查请求方法是否为 GET,如果是,则创建一个包含 HTML 页面内容的 DefaultFullHttpResponse 对象,并设置响应头部的 Content-Type 和 Content-Length。最后,使用 ctx.writeAndFlush 方法将响应发送给客户端。

 

如果在处理请求过程中发生异常,我们将在 exceptionCaught 方法中打印异常信息并关闭连接。

 

六、使用 Netty 实现 HTTP 客户端

 

接下来,我们将使用 Netty 实现一个简单的 HTTP 客户端,该客户端将向我们之前实现的 HTTP 服务器发送一个 GET 请求,并打印服务器返回的响应内容。

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;

public class HttpClient {

    public static void main(String[] args) throws InterruptedException {
        // 创建事件循环组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // 创建客户端引导程序
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                   .channel(NioSocketChannel.class)
                   .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 添加 HTTP 编解码器
                            ch.pipeline().addLast(new HttpClientCodec());
                            // 添加 HTTP 请求消息聚合器
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            // 添加自定义的 HTTP 处理逻辑
                            ch.pipeline().addLast(new HttpClientHandler());
                        }
                    })
                   .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                   .option(ChannelOption.SO_KEEPALIVE, true);

            // 连接到服务器
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync();

            // 等待连接关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭事件循环组
            group.shutdownGracefully();
        }
    }
}

 

在上述代码中,我们首先创建了一个 NioEventLoopGroup 对象,然后创建了一个 Bootstrap 对象,用于配置和启动客户端。

 

在 Bootstrap 的配置中,我们指定了使用 NioSocketChannel 作为客户端的通道类型,并在 handler 中添加了 HTTP 编解码器 HttpClientCodec、HTTP 请求消息聚合器 HttpObjectAggregator 和自定义的 HttpClientHandler

 

然后,我们使用 connect 方法连接到服务器的地址 127.0.0.1 和端口 8080,并使用 sync 方法等待连接完成。

 

接下来,我们来实现自定义的 HttpClientHandler

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;

public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 创建 GET 请求
        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");

        // 发送请求
        ctx.writeAndFlush(request);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception {
        // 打印响应内容
        System.out.println(response.content().toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

在 HttpClientHandler 中,我们继承了 SimpleChannelInboundHandler<FullHttpResponse>,并重写了 channelActive 方法来在连接建立后发送一个 GET 请求,以及 channelRead0 方法来处理服务器返回的响应。在 channelRead0 方法中,我们将响应内容打印到控制台。

 

如果在处理请求或响应过程中发生异常,我们将在 exceptionCaught 方法中打印异常信息并关闭连接。

 

七、自定义 HTTP 协议

 

在实际应用中,我们可能需要自定义 HTTP 协议来满足特定的需求。例如,我们可能需要添加自定义的头部字段、修改请求或响应的格式等。下面我们将介绍如何使用 Netty 实现自定义 HTTP 协议。

 

1. 添加自定义头部字段

 

要添加自定义头部字段,我们可以在 HttpRequest 或 HttpResponse 对象中设置头部字段的值。例如,我们可以在服务器端的 HttpHandler 中添加一个自定义头部字段 X-Custom-Header

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;

public class CustomHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        // 处理 GET 请求
        if (request.method().equals(HttpMethod.GET)) {
            // 创建响应
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
                    Unpooled.wrappedBuffer("<h1>Hello, Custom HTTP Protocol!</h1>".getBytes()));

            // 设置响应头部
            response.headers().set("Content-Type", "text/html; charset=UTF-8");
            response.headers().set("Content-Length", response.content().readableBytes());
            response.headers().set("X-Custom-Header", "Custom Value");

            // 发送响应
            ctx.writeAndFlush(response);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

 

在上述代码中,我们在创建 DefaultFullHttpResponse 对象后,设置了一个自定义头部字段 X-Custom-Header,并将其值设置为 Custom Value

在客户端,我们可以通过 FullHttpResponse 对象获取自定义头部字段的值:

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;

public class CustomHttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 创建 GET 请求
        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");

        // 发送请求
        ctx.writeAndFlush(request);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception {
        // 打印自定义头部字段的值
        String customHeaderValue = response.headers().get("X-Custom-Header");
        System.out.println("X-Custom-Header: " + customHeaderValue);

        // 打印响应内容
        System.out.println(response.content().toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

 

在上述代码中,我们在 channelRead0 方法中通过 response.headers().get("X-Custom-Header") 来获取自定义头部字段 X-Custom-Header 的值,并将其打印到控制台。

 

2. 修改请求或响应的格式

 

要修改请求或响应的格式,我们可以自定义 HttpRequestDecoder 和 HttpResponseEncoder 来实现。例如,我们可以将请求和响应的内容编码为 JSON 格式。

 

首先,我们需要创建一个自定义的 HttpRequestDecoder

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;

public class CustomHttpRequestDecoder extends HttpRequestDecoder {

    @Override
    protected HttpRequest createRequest(String methodName, String uri, HttpVersion version) {
        HttpMethod method = HttpMethod.valueOf(methodName);
        return new DefaultHttpRequest(version, method, uri);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
        // 解析请求内容为 JSON 格式
        String requestContent = buffer.toString(CharsetUtil.UTF_8);
        JSONObject jsonObject = new JSONObject(requestContent);

        // 创建 HttpRequest 对象
        HttpRequest request = createRequest(jsonObject.getString("method"), jsonObject.getString("uri"), HttpVersion.valueOf(jsonObject.getString("version")));

        // 设置请求头部
        for (String key : jsonObject.keySet()) {
            if (!key.equals("method") &&!key.equals("uri") &&!key.equals("version")) {
                request.headers().set(key, jsonObject.getString(key));
            }
        }

        // 将 HttpRequest 对象添加到到输出列表中
        out.add(request);
    }
}


在上述代码中,我们继承了`HttpRequestDecoder`,并重写了`createRequest`和`decode`方法。在`decode`方法中,我们将请求内容解析为JSON格式,并从中提取出请求方法、URI、HTTP版本和头部字段等信息,然后创建一个`HttpRequest`对象并将其添加到输出列表中。

接下来,我们创建一个自定义的`HttpResponseEncoder`:



```java
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpVersion;
import org.json.JSONObject;

public class CustomHttpResponseEncoder extends HttpResponseEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx, HttpResponse response, ByteBuf out) throws Exception {
        // 将响应内容编码为 JSON 格式
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("statusCode", response.getStatus().code());
        jsonObject.put("reasonPhrase", response.getStatus().reasonPhrase());

        // 设置响应头部
        for (String key : response.headers().names()) {
            jsonObject.put(key, response.headers().get(key));
        }

        // 设置响应主体
        if (response instanceof DefaultFullHttpResponse) {
            DefaultFullHttpResponse fullResponse = (DefaultFullHttpResponse) response;
            jsonObject.put("content", fullResponse.content().toString(io.netty.util.CharsetUtil.UTF_8));
        }

        // 将 JSON 字符串写入 ByteBuf
        out.writeBytes(jsonObject.toString().getBytes(io.netty.util.CharsetUtil.UTF_8));
    }
}

 

在上述代码中,我们继承了HttpResponseEncoder,并重写了encode方法。在encode方法中,我们将响应内容编码为JSON格式,并将其写入ByteBuf中。

 

然后,我们在服务器和客户端的代码中使用我们自定义的编解码器:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.CustomHttpRequestDecoder;
import io.netty.handler.codec.http.CustomHttpResponseEncoder;

public class CustomHttpServer {

    public static void main(String[] args) throws InterruptedException {
        // 创建 boss 和 worker 线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建服务器引导程序
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                  .channel(NioServerSocketChannel.class)
                  .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 添加自定义的 HTTP 请求解码器
                            ch.pipeline().addLast(new CustomHttpRequestDecoder());
                            // 添加 HTTP 请求消息聚合器
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            // 添加自定义的 HTTP 响应编码器
                            ch.pipeline().addLast(new CustomHttpResponseEncoder());
                            // 添加自定义的 HTTP 处理逻辑
                            ch.pipeline().addLast(new CustomHttpHandler());
                        }
                    })
                  .option(ChannelOption.SO_BACKLOG, 128)
                  .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 绑定端口并启动服务器
            ChannelFuture future = bootstrap.bind(8080).sync();

            // 等待服务器关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭线程组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

 

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.http.CustomHttpRequestDecoder;
import io.netty.handler.codec.http.CustomHttpResponseEncoder;

public class CustomHttpClient {

    public static void main(String[] args) throws InterruptedException {
        // 创建事件循环组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // 创建客户端引导程序
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                  .channel(NioSocketChannel.class)
                  .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 添加自定义的 HTTP 请求解码器
                            ch.pipeline().addLast(new CustomHttpRequestDecoder());
                            // 添加 HTTP 请求消息聚合器
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            // 添加自定义的 HTTP 响应编码器
                            ch.pipeline().addLast(new CustomHttpResponseEncoder());
                            // 添加自定义的 HTTP 处理逻辑
                            ch.pipeline().addLast(new CustomHttpClientHandler());
                        }
                    })
                  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                  .option(ChannelOption.SO_KEEPALIVE, true);

            // 连接到服务器
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync();

            // 等待连接关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭事件循环组
            group.shutdownGracefully();
        }
    }
}

在上述代码中,我们在服务器和客户端的ChannelPipeline中分别添加了自定义的HttpRequestDecoderHttpResponseEncoder

 

八、总结

在本教程中,我们学习了如何使用Netty实现HTTP协议。我们首先介绍了Netty的基础知识和HTTP协议的基本概念,然后使用Netty实现了一个简单的HTTP服务器和客户端。接着,我们介绍了如何自定义HTTP协议,包括添加自定义头部字段和修改请求或响应的格式。通过本教程的学习,相信你已经对Netty实现HTTP协议有了深入的了解,并能够根据实际需求进行自定义开发。

 

 

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

马丁的代码日记

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

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

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

打赏作者

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

抵扣说明:

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

余额充值