3. Netty系列——Netty

2,136 阅读13分钟

Netty系列


上一篇文章我们讲了NIO,并了解了NIO的使用以及一些缺点,而Netty正是来解决这些问题的,本章我们就会了解Netty是什么,为什么要用Netty以及该怎么用Netty

Netty概念

引用官方的一段话:Netty是一个高性能、异步事件驱动的网络应用框架。 基于Netty,可以快速的开发和部署高性能、 高可用的网络服务端和客户端应用

Netty的优点

Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特点有:

  • 设计
    • 针对多种传输类型的统一接口 - 阻塞和非阻塞
    • 简单但更强大的线程模型
    • 真正的无连接的数据报套接字支持
    • 链接逻辑支持复用
  • 易用性
    • 大量的 Javadoc 和 代码实例
    • 除了在 JDK 1.6 + 额外的限制。(一些特征是只支持在Java 1.7 +。可选的功能可能有额外的限制。)
  • 性能
    • 比核心 Java API 更好的吞吐量,较低的延时
    • 资源消耗更少,这个得益于共享池和重用
    • 减少内存拷贝
  • 健壮性
    • 消除由于慢,快,或重载连接产生的 OutOfMemoryError
    • 消除经常发现在 NIO 在高速网络中的应用中的不公平的读/写比
  • 安全
    • 完整的 SSL / TLS 和 StartTLS 的支持
    • 运行在受限的环境例如 Applet 或 OSGI
  • 社区
    • 发布的更早和更频繁
    • 社区驱动

Netty常见使用场景

  • 互联网行业在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
  • 游戏行业无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信
  • 大数据领域经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现

为什么用Netty

  • 快,Netty正是基于NIO实现了这种Reactor模型,Boss线程用来专门处理连接的建立,SubReactor专门用来处理IO的读写以及任务的处理。这种线程模型在充分利用CPU性能的情况下支撑大量的并发连接
  • 内存使用少,网络数据传输面临着大量的对象创建和销毁,Netty主要从两个方面缓解JVM的压力
    • ByteBufAllocator对象池。池化ByteBuf实例以提高性能并最小化内存碎片,后者每次调用时都返回一个新的实例
    • 零拷贝。支持DirectBuffer的使用,通过JVM的本地调用分配内存,这可避免每次调用本地I / O操作之前(或之后)将缓冲区的内容复制到(或从)中间缓冲区
  • API简单,网络编程一般都比较复杂,更面临着IO读写以及线程安全问题问题要处理,Netty针对这些问题做了大量封装,使API更简单易用。基于事件模式,对网络事件进行串行化处理,在保证高效的同时,又降低了编程的复杂度
  • Netty非常稳定,一般我们遇到的NIO的select空转,TCP断线重连,keep-alive检测等问题,Neety都已解决

Netty线程模型

Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:

MainReactor负责客户端的连接请求,并将请求转交给SubReactor SubReactor负责相应通道的IO读写请求 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

引自Scalable IO in Java

Netty由哪几部分构成

Channel 数据传输流,与channel相关的概念有以下四个

  • Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。
  • ChannelHandler,核心处理业务就在这里,用于处理业务请求。
  • ChannelHandlerContext,用于传输业务数据。
  • ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext

ByteBuf

ByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示

他有三种使用模式:

  • Heap Buffer 堆缓冲区 堆缓冲区是ByteBuf最常用的模式,他将数据存储在堆空间。
  • Direct Buffer 直接缓冲区 直接缓冲区是ByteBuf的另外一种常用模式,他的内存分配都不发生在堆,jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处 -通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
  • Composite Buffer 复合缓冲区 复合缓冲区相当于多个不同ByteBuf的视图,这是netty提供的,jdk不提供这样的功能。

除此之外,他还提供一大堆api方便你使用,在这里我就不一一列出了,具体参见ByteBuf字节缓存。

Codec

Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,从而达到自定义协议的目的。

在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。

Callback (回调)

callback (回调)是一个简单的方法,提供给另一种方法作为引用,这样后者就可以在某个合适的时间调用前者。这种技术被广泛使用在各种编程的情况下,最常见的方法之一通知给其他人操作已完成。

Netty 内部使用回调处理事件时。一旦这样的回调被触发,事件可以由接口 ChannelHandler 的实现来处理。如下面的代码,一旦一个新的连接建立了,调用 channelActive(),并将打印一条消息

Future

Future 提供了另外一种通知应用操作已经完成的方式。这个对象作为一个异步操作结果的占位符,它将在将来的某个时候完成并提供结果

每个 Netty 的 outbound I/O 操作都会返回一个ChannelFuture;这样就不会阻塞。这就是 Netty所谓的“自底向上的异步和事件驱动” ...

Event 和 Handler

Netty 使用不同的事件来通知我们更改的状态或操作的状态。这使我们能够根据发生的事件触发适当的行为。

这些行为可能包括:

  • 日志
  • 数据转换
  • 流控制
  • 应用程序逻辑

由于 Netty 是一个网络框架,事件很清晰的跟入站或出站数据流相关。因为一些事件可能触发传入的数据或状态的变化包括:

  • 活动或非活动连接
  • 数据的读取
  • 用户事件
  • 错误

出站事件是由于在未来操作将触发一个动作。这些包括:

  • 打开或关闭一个连接到远程
  • 写或冲刷数据到 socket

总结

FUTURE, CALLBACK 和 HANDLER

Netty 的异步编程模型是建立在 future 和 callback 的概念上的。所有这些元素的协同为自己的设计提供了强大的力量。

拦截操作和转换入站或出站数据只需要您提供回调或利用 future 操作返回的。这使得链操作简单、高效,促进编写可重用的、通用的代码。一个 Netty 的设计的主要目标是促进“关注点分离”:你的业务逻辑从网络基础设施应用程序中分离。

SELECTOR, EVENT 和 EVENT LOOP

Netty 通过触发事件从应用程序中抽象出 Selector,从而避免手写调度代码。EventLoop 分配给每个 Channel 来处理所有的事件,包括

  • 注册感兴趣的事件
  • 调度事件到 ChannelHandler
  • 安排进一步行动

该 EventLoop 本身是由只有一个线程驱动,它给一个 Channel 处理所有的 I/O 事件,并且在 EventLoop 的生命周期内不会改变。这个简单而强大的线程模型消除你可能对你的 ChannelHandler 同步的任何关注,这样你就可以专注于提供正确的回调逻辑来执行。该 API 是简单和紧凑

实战

服务端

Netty实现服务端需要的组件及其作用

  • 一个服务器handler,这个组件实现了服务器的业务逻辑,决定了连接创建后和接收到信息后该如何处理
    • ChannelHandler 是给不同类型的事件调用
    • 应用程序实现或扩展 ChannelHandler 挂接到事件生命周期和 提供自定义应用逻辑
  • Bootstrapping: 这个是配置服务器的启动代码。最少需要设置服务器绑定的端口,用来监听连接请求
    • 监听和接收进来的连接请求
    • 配置 Channel 来通知一个关于入站消息的 EchoServerHandler 实例

依赖

我们通过maven引入pom

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

Handler示例代码

/**
 * 自定义事件处理方法
 * @ChannelHandler.Sharable  表示该类的实例可以在channel中共享
 */
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuffer byteBuffer = (ByteBuffer) msg;
        // 将接受到的消息打印到控制台
        System.out.printf("server reviceived: %s%n", byteBuffer.toString());
        // 将所接受到的消息返回给发送者,这里还没有冲刷数据
        ctx.write(byteBuffer);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 冲刷所有待审消息到远程节点,关闭通道后,操作完成
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 每个 Channel 都有一个关联的 ChannelPipeline,它代表了 ChannelHandler 实例的链。
     * 适配器处理的实现只是将一个处理方法调用转发到链中的下一个处理器。
     * 因此,如果一个 Netty 应用程序不覆盖exceptionCaught ,
     * 那么这些错误将最终到达 ChannelPipeline,并且结束警告将被记录
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 打印异常堆栈信息
        cause.printStackTrace();
        // 关闭通道
        ctx.close();
    }
}

服务端的开发步骤:

  • 创建 ServerBootstrap 实例来引导服务器并随后绑定
  • 创建并分配一个 NioEventLoopGroup
  • 实例来处理事件的处理,如接受新的连接和读/写数据。
  • 指定本地 InetSocketAddress 给服务器绑定
  • 通过 EchoServerHandler 实例给每一个新的 Channel 初始化
  • 最后调用 ServerBootstrap.bind() 绑定服务器
/**
 * 服务端
 */
public class EchoServer {
    public int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println(
                    "Server Class name is: " + EchoServer.class.getSimpleName() + "<port>"
            );
            return;
        }
        // 通过终端参数设置端口值
        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }

private void start() {
        // 创建 EventLoopGroup
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
        try {
            // 创建 ServerBootstrap
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 指定使用 NIO 的传输 Channel
            serverBootstrap.group(nioEventLoopGroup)
                    .channel(NioServerSocketChannel.class) // 指定使用 NIO 的传输 Channel
                    .localAddress(new InetSocketAddress(port)) // 设置 socket 地址使用所选的端口
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 添加 EchoServerHandler 到 Channel 的 ChannelPipeline
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            // 通过socketChannel获得对应的管道
                            ChannelPipeline channelPipeline = socketChannel.pipeline();

                            // 通过管道,添加handler
                            // HttpServerCodec时由netty自己提供的助手类,可以理解为拦截器
                            // 当请求到服务端,我们需要做解码,响应到客户端做编码
                            // WebSocket基于http协议,所以要有http编解码器
                            channelPipeline.addLast("HttpServerCodec", new HttpServerCodec());
                            // 对写大数据流的支持
                            channelPipeline.addLast(new ChunkedWriteHandler());
                            // 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse
                            // 几乎在netty中的变成,都会用到此handler
                            channelPipeline.addLast(new HttpObjectAggregator(1024 * 64));
                            // ========================= 以上是用于http协议 =========================

                            // webSocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws
                            // 本handler会帮你处理一些繁重的复杂的事
                            // 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
                            // 对于webSocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
                            channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

                            // 添加自定义助手类
                            channelPipeline.addLast("EchoServerHandler", new EchoServerHandler());
                        }
                    });

            // 绑定的服务器; 同步等待服务器关闭
            ChannelFuture channelFuture = serverBootstrap.bind().sync();
            System.out.println(EchoServer.class.getName() + " started and listen on " + channelFuture.channel().localAddress());
            // 关闭 channel 和 块,直到它被关闭
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭EventLoopGroup, 释放所有资源
                nioEventLoopGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

先来看下客户的工作内容

  • 连接服务器
  • 发送信息
  • 等待和接收从服务器返回的信息
  • 关闭连接

客户端与服务端一样都需要一个handler来处理数据

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuffer> {
    /**
     * 数据从服务器接收到调用
     * 由服务器所发送的消息可以以块的形式被接收。
     * 即,当服务器发送 5 个字节是不是保证所有的 5 个字节会立刻收到 - 即使是只有 5 个字节,
     * channelRead0() 方法可被调用两次,第一次用一个ByteBuf(Netty的字节容器)装载3个字节
     * 和第二次一个 ByteBuf 装载 2 个字节。唯一要保证的是,该字节将按照它们发送的顺序分别被接收
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuffer msg) throws Exception {
        System.out.println("Client received msg is: " + msg.toString());
    }

    /**
     * 服务器的连接被建立后调用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("已连接到服务器,等待消息...", CharsetUtil.UTF_8));
    }

    /**
     * 捕获异常时调用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Cient

public class EchoClient {

    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println(
                    "Server Class name is: " + EchoClient.class.getSimpleName() + " <host>:<port>"
            );
            return;
        }
        // 通过终端参数设置端口值
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }

    private void start() {
        EventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            // 创建 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            // 指定 EventLoopGroup 来处理客户端事件。
            // 由于我们使用 NIO 传输,所以用到了 NioEventLoopGroup 的实现
            bootstrap.group(eventExecutors)
                    // 使用NIO类型的 channel
                    .channel(NioSocketChannel.class)
                    // 设置服务器的 InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    // 当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例 到 channel pipeline
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });

            // 连接到远程;等待连接完成
            ChannelFuture channelFuture = bootstrap.connect().sync();
            // .阻塞直到 Channel 关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                eventExecutors.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}