为了账号安全,请及时绑定邮箱和手机立即绑定

Netty网络通讯入门:新手必读教程

标签:
杂七杂八
概述

本文提供了Netty网络通讯入门的全面指南,涵盖了Netty框架的基础知识、环境搭建、核心组件介绍以及简单的服务器和客户端实现。文章还详细讲解了Netty中的数据传输机制、异常处理以及性能优化策略,帮助读者快速掌握Netty网络编程的核心技巧。

Netty简介与环境搭建

1.1 Netty是什么

Netty 是一个高性能、异步事件驱动的网络应用程序框架,由 JBoss 社区开发,广泛用于开发高性能的网络客户端和服务器应用。它简化了网络编程的复杂度,使得开发人员能够更加专注于应用逻辑的实现,而不是底层网络通信的细节。

1.2 Netty的优势

  1. 高性能与可扩展性:Netty 使用了高效灵活的内存池和零拷贝技术,使得网络通信效率得到了极大提高。
  2. 多协议支持:支持多种网络协议,包括但不限于 HTTP、WebSocket、SSL/TLS、FTP、SMTP 等。
  3. 异步非阻塞:基于 NIO(New IO)技术,实现了异步非阻塞的网络通信模型,大大提高了程序的并发性。
  4. 灵活的事件模型:Netty 提供了强大的事件驱动引擎,使得事件处理更加灵活和高效。
  5. 便捷的协议编码与解码:内置了多种协议编码与解码机制,极大地简化了开发工作量。

1.3 开发环境搭建

  1. 安装JDK:Netty 是基于 Java 的,因此首先需要在本地安装 JDK。可以通过官方网站下载对应版本的 JDK 安装包。
  2. 配置环境变量:将 JDK 的 bin 目录添加到系统的 PATH 环境变量中。
  3. 创建 Maven 项目:使用 IDE(如 IntelliJ IDEA 或 Eclipse)创建一个 Maven 项目。在项目的 pom.xml 文件中添加 Netty 的依赖。

下面是在 pom.xml 文件中添加 Netty 依赖的示例代码:

<dependencies>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.69.Final</version>
    </dependency>
</dependencies>
Netty的核心组件

2.1 Bootstrap和ServerBootstrap

  1. Bootstrap:用于创建客户端的启动配置。它是一个帮助类,用于简化配置客户端的步骤。
  2. ServerBootstrap:用于创建服务端的启动配置。它也是一个帮助类,用于简化配置服务端的步骤。

下面是一个使用 ServerBootstrap 创建服务端的简单示例代码:

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;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        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
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    })
                    .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();
        }
    }
}

下面是一个使用 Bootstrap 创建客户端的简单示例代码:

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

2.2 Channel与ChannelHandler

  1. Channel:代表一个网络连接,包括了输入和输出流。
  2. ChannelHandler:处理每个 Channel 上发送和接收的数据。ChannelHandler 可以处理读写事件、异常事件等,使得网络通信更加灵活和高效。

下面是一个简单的 ChannelHandler 示例代码:

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

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.write("TCP Server 应答");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("Hello, Server!");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String response = (String) msg;
        System.out.println("收到服务器应答:" + response);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

2.3 EventLoop与EventLoopGroup

  1. EventLoop:负责执行异步任务的调度器。通常,每个 Channel 会关联一个 EventLoop,用于处理该 Channel 的所有 I/O 事件。
  2. EventLoopGroup:一个 EventLoop 的集合,可以包含一个或多个 EventLoop。它为一组 Channel 提供 EventLoop 的管理。

下面是一个使用 EventLoopGroup 的示例代码:

import io.netty.channel.EventLoopGroup;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        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
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    })
                    .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();
        }
    }
}
创建一个简单的Netty服务器

3.1 服务端的基本实现

下面是一个简单的服务端实现,它会接收客户端的连接,读取客户端消息并发送应答。

首先,创建 Netty 服务器的启动类 NettyServer

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;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        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
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    })
                    .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();
        }
    }
}

然后,创建服务端处理逻辑的 NettyServerHandler 类,用于读取客户端的消息并发送应答:

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

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.write("TCP Server 应答");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

3.2 客户端的基本实现

接下来,创建客户端的基本实现,用于连接到服务器并发送消息。

首先,创建 Netty 客户端的启动类 NettyClient

import io.netty.bootstrap.Bootstrap;
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.string.StringEncoder;

public class NettyClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringEncoder(), new NettyClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

然后,创建客户端处理逻辑的 NettyClientHandler 类,用于发送消息到服务器并读取应答:

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

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("Hello, Server!");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String response = (String) msg;
        System.out.println("收到服务器应答:" + response);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

3.3 客户端与服务端的连接测试

启动服务端和客户端,测试客户端与服务端的连接。

首先,启动服务端:

java -cp target/classes:lib/* NettyServer

接着,启动客户端:

java -cp target/classes:lib/* NettyClient

客户端输出:

收到服务器应答:TCP Server 应答

服务端输出:

收到客户端消息:Hello, Server!

可以观察到客户端成功发送了消息,并收到了服务端的应答。

Netty中的数据传输

4.1 编码与解码机制

Netty 提供了多种编码与解码机制,使得开发者可以方便地处理各种协议。

4.1.1 编码器与解码器

编码器(Encoder)用于将数据从应用层转换为传输层格式,解码器(Decoder)用于将数据从传输层格式转换为应用层格式。

下面是一个简单的字符串编码器与解码器的示例代码:

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        public void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new NettyServerHandler());
                        }
                    })
                    .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();
        }
    }
}

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.write("TCP Server 应答");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

4.2 长连接和短连接

4.2.1 长连接

长连接指的是客户端与服务器之间保持一个持久连接,客户端在需要时通过该连接发送请求和接收响应,连接不会轻易断开。

4.2.2 短连接

短连接指的是每次客户端发送请求前需要先建立连接,请求处理后关闭连接。

下面是一个长连接的示例代码:

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.write("TCP Server 应答");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("Hello, Server!");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String response = (String) msg;
        System.out.println("收到服务器应答:" + response);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

4.3 数据序列化与反序列化

数据序列化(Serialization)和反序列化(Deserialization)是将对象转换为字节流和将字节流转换为对象的过程。在 Netty 中,可以使用 Java 自带的序列化机制或第三方库(如 Kryo、FST 等)进行序列化与反序列化。

下面是一个使用 Kryo 序列化与反序列化的示例代码:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    private Kryo kryo;

    public NettyServerHandler() {
        kryo = new Kryo();
        kryo.register(MyMessage.class);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        byte[] bytes = (byte[]) msg;
        Input input = new Input(bytes);
        MyMessage message = kryo.readObject(input, MyMessage.class);
        System.out.println("收到客户端消息:" + message.content);
        ctx.write(kryo.writeClassAndObject(new Output(1024), "TCP Server 应答"));
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

public class MyMessage {
    public String content;
}

4.4 示例:使用 Protobuf 序列化与反序列化

Protobuf(Protocol Buffers)是由 Google 开发的一种高效的序列化协议。下面是一个使用 Protobuf 进行序列化与反序列化的示例代码:

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        MyMessageProto.MyMessage message = (MyMessageProto.MyMessage) msg;
        System.out.println("收到客户端消息:" + message.getContent());
        MyMessageProto.MyMessage response = MyMessageProto.MyMessage.newBuilder().setContent("TCP Server 应答").build();
        ctx.write(response);
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}

public class MyMessageProto {
    public static class MyMessage {
        public String content;
    }
}

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        MyMessageProto.MyMessage message = MyMessageProto.MyMessage.newBuilder().setContent("Hello, Server!").build();
        ctx.writeAndFlush(message);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        MyMessageProto.MyMessage response = (MyMessageProto.MyMessage) msg;
        System.out.println("收到服务器应答:" + response.getContent());
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}
Netty中的异常处理

5.1 异常处理的基本思路

  1. 捕获异常:在 ChannelHandler 中捕获并处理异常,如连接超时、数据解析错误等。
  2. 关闭连接:在异常处理完成后,关闭对应的 Channel 连接。
  3. 日志记录:记录异常信息,便于后续的调试和维护。

5.2 实例:如何处理连接超时

下面是一个简单的示例代码,处理连接超时的异常:

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

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.write("TCP Server 应答");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (cause instanceof TimeoutException) {
            System.err.println("连接超时:" + cause.getMessage());
        } else {
            System.err.println("异常信息:" + cause.getMessage());
        }
        ctx.close();
    }
}
Netty的性能优化

6.1 常见优化策略

  1. 减少对象创建:减少不必要的对象创建,尤其是在高并发环境下。
  2. 复用缓冲区:使用 Netty 内置的缓冲区复用机制,减少垃圾回收的开销。
  3. 线程模型优化:合理设置 EventLoopGroup 的线程数,根据实际应用场景进行调整。
  4. 零拷贝技术:利用操作系统提供的零拷贝技术,减少数据拷贝次数,提高传输效率。
  5. 使用高效的数据结构:选择合适的数据结构,如使用队列而非直接 List 存储数据。
  6. 异步处理:充分利用异步非阻塞的特性,使得 I/O 操作不会阻塞整个应用。

6.2 实战:如何提高通信效率

下面是一个优化后的示例代码,通过减少对象创建来提高通信效率:


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.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        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
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new EfficientNettyServerHandler());
                        }
                    })
                    .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();
        }
    }
}

public class EfficientNettyServerHandler extends ChannelInboundHandlerAdapter {
    private static final byte[] RESPONSE = "TCP Server 应答".getBytes();

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String request = (String) msg;
        System.out.println("收到客户端消息:" + request);
        ctx.writeAndFlush(Unpooled.copiedBuffer(RESPONSE));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("异常信息:" + cause.getMessage());
        ctx.close();
    }
}
``

上面的代码中,使用了 `Unpooled.copiedBuffer` 方法来减少字符串对象的创建,提高通信效率。

通过减少对象创建、复用缓冲区、优化线程模型等策略,可以显著提升 Netty 服务器的性能和效率。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消