一文快速上手高性能网络框架netty

1、netty是什么?

在使用netty之前,我们首先要知道netty的优势在哪,它能解决什么问题。

我们不妨直接引用netty官网上最顶部的内容:

image.png

翻译一下就是:netty是一款异步事件驱动的网络程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。

注意上方加粗的关键词:

  • 支持异步是netty高效的重要支撑,在传统java API中,我们使用NIO需要较为繁琐的编码,而netty为我们封装了易于使用的API,我们甚至能够通过一行代码将整个服务由OIO转为NIO!
  • 事件驱动指的是netty处理网络消息的方式,框架内部为我们提供了完善的接口/抽象类,其中的各个方法代表了在网络传输过程中发生的各种事件,在我们编码过程中只需要完善各个方法,就可以处理各种网络通信!
  • 快速开发的快速不仅仅在于netty提供了恰当的处理网络通信的框架逻辑,更在于其内部提供了丰富的成熟的编解码器,我们只需要将它们加入我们的pipline,便可以轻易的处理如HTTP/HTTPS/Websocket/IMAP/ProtoBuf等等协议
  • 高性能于netty有着许多原因,包括但不限于支持NIO、零拷贝、多线程优化等。而我们只需要使用netty框架,便能轻易的享受前人给予我们的强大性能。

我们将从以下几方面来介绍netty:

  • netty的工作流程:大致了解netty是如何处理网络信息的
  • ChannelHandler:netty中重要的网络信息处理组件
  • 编解码器:作为特殊的ChannelHandler为使用者提供了方便的协议解析
  • EventLoop:netty内部基于Reactor模型对事件进行分发
  • ByteBuf:netty提供的高效且易用的字节容器
  • ChannelFuture:netty自身定义的future模型,在执行异步操作时使用
  • 引导客户端与服务器启动:最后一步,如何启动我们的网络服务

2、netty的工作流程

对于入门使用netty而言,一图便可以了解netty的工作流程:

channel.png

接下来我们对这张图进行解释:

  • channel:就如同JavaNIO中所做的那样,netty将对网络IO的操作抽象为channel,因此我们不再操作基础的socket,而是封装过后的channel。
  • pipline:由管道流向netty的事件,需要经过重重的ChannelHandler加以处理,ChannelHandler便是netty提供给我们的、编码者大部分精力需要花费的框架结构。pipline则是将一个个ChannelHandler顺序连接起来的抽象。
  • ChannelHandler:如上文所说,ChannelHandler是我们处理网络信息的主体,我们编码的ChannelHandler其本身继承于netty所定义ChannelHandler,而其各个方法本身就是为了响应一个个不同的事件,这也是netty事件驱动的主要体现。

3、ChannelHandler

概念

ChannelHandler从大类上分为两类:ChannelInboundHandler与ChannelOutboundHandler。分别对应处理了上图中的入站事件和出站事件,这也是很多作者会像下方这样描述pipline与ChannelHandler的关系的原因(图源网络):

image.png

而事实上这样的图很容易给读者造成误导,认为ChannelInboundHandler与ChannelOutboundHandler是在两条不同的线上。

而实际它们都串联在pipline上,只是在流经时会对是处理入站事件的handler还是出站事件的handler加以判别。例如入站事件遇到了出站事件的ChannelOutboundHandler会直接跳过。

实践

通常我们继承的ChannelHandler来自下图:

image.png

  • ChannelHandler是公共的抽象父类,我们通常不会使用。
  • ChannelInboundHandler与ChannelOutboundHandler接口中定义了netty中各种事件,是事件驱动的规范,通过继承它们我们可以实现自己的ChannelHandler。
  • ChannelHandlerAdapter为我们封装了接口的简单实现,因此继承它们我们不用为每个方法编写自己的实现,只需要将注意力集中在我们所需要的事件驱动方法上。

一个简单的继承于SimpleChannelInboundHandler(其是ChannelInboundHandlerAdapter的子类,帮助我们在处理完消息后释放消息的内存引用,这是ChannelInboundHandlerAdapter不会做到的,因此我们也通常继承它)的Handler如下所示:

public class EchoClientHandler extends  SimpleChannelInboundHandler<ByteBuf>{
    
    //当channel连接事件发生时的响应
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rock!",CharsetUtil.UTF_8));
    }
    
    //当channel读入数据事件发生时的响应
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf in) throws Exception {
        System.out.println("Client received:"+in.toString(CharsetUtil.UTF_8));
    }
}
复制代码

4、编解码器

概念

编解码器其实就是一类特殊的ChannelHandler,以MessageToMessageDecoder为例,其继承关系如下:

image.png

而往往为一类事物中的某种专门抽象出子类,代表着其必然有着特殊的意义。

在网络通信中,我们要传递信息往往需要协议的封装,而协议通常是规范的、不变的,如果每次都要为了处理协议而编写大量代码无疑是冗余的。而在其他如文件IO方面,也需要特定的编码器支持。

因此编解码器出现了,netty提供了可拔插的编解码器构件,能够方便地帮助我们处理各个协议。我们可以这样区分各种编码器:

  • Encoder

    • MessageToByteEncoder //将消息编码为字节
    • MessageToMessageEncoder //将消息编码为消息
  • Decoder

    • ByteToMessageDecoder //将字节解码为消息
    • MessageToMessageDecoder //将消息解码为消息
  • Codec //相当于Encoder+Decoder

加入编解码器后的pipline如下所示

无标题.png

实践

一个简单的Decoder如下所示(DatagramPacket是netty定义的一种消息容器,作为泛型成为传入消息的载体):

public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> list) throws Exception {
        ByteBuf buf=datagramPacket.content();//获取数据,ByteBuf见第六小节
        //处理数据
        int index=buf.indexOf(0, buf.readableBytes(), LogEvent.SEPARATOR);
        index=buf.indexOf(index+1, buf.readableBytes(), LogEvent.SEPARATOR);
        String filename=buf.slice(0, index).toString(CharsetUtil.UTF_8);
        String logMsg=buf.slice(index+1, buf.readableBytes()-index-1).toString(CharsetUtil.UTF_8);
        LogEvent logEvent=new LogEvent(datagramPacket.sender(),filename, logMsg,System.currentTimeMillis());
        //将解码后消息传递给下一个handler
        list.add(logEvent);
    }
}
复制代码

而在netty的官方文档中,我们可以看到大量的支持各种协议的编解码器具体实现,这为我们高效开发提供了有力支持。下面仅展示部分:

image.png

当然,你也可以编写自己的编解码器来解析自定义协议。

5、EventLoop

EventLoop 是 Netty Reactor 线程模型的核心处理引擎,其工作流程可以如下所示:

image.png

  • 创建EventLoop,其可以归属于一个Event Loop组(可以理解为池化)
  • channel在创建时将其绑定在一个EventLoop上
  • 当有事件发生时,会将其放入Event Queue中
  • EventLoop通过轮询从Event Queue中取出事件并且方法到对应的,并且将其分发给对应的pipline/future回调函数(执行异步IP时会返回future,可以通过给其添加Listner的方式来进行回调,详见第七节)

对于客户端-服务器模型,EventLoop如下工作

image.png

  • 当事件出发时,会先传入第一个EventLoopGroup,其之上绑定了一个或多个channel,这个EventLoopGroup会将事件分发给对应的channel的EventLoop(右边的WorkerEventLoop内)
  • 右边的EventLoop完成IO后,会将事件沿着pipline进行传播
  • 当future执行完成时,右边的EventLoop同样会为其执行它的回调函数

6、ByteBuf

ByteBuf是netty内置的帮助开发者处理字节的API,与琐碎的Java NIO提供的ByteBuffer相比,拥有更方便的使用体验以及更好的性能,其优点包括:

  • 易于为用户拓展
  • 通过内置的复合缓冲区类型实现零拷贝
  • 类似StringBuffer可以按需增长
  • 读写模式间不需要切换,读写使用不同索引
  • 支持链式调用
  • 支持引用计数
  • 支持池化

在这里我并不想对各个API进行解释,这也是没有必要的,详情可以参考官方文档

在这里更希望对ByteBuf的数据结构和使用模式进行展开

数据结构

ByteBuf在数组间维护了两个指针 readerIndex与writerIndex,分别指向了读取和写入的下一个字节的位置,如图:

image.png 当然地,在读取内容时需要保障writerIndex大于等于readerIndex,试图越界读取会报出异常。

使用模式

ByteBuffer可以主要分为以下三种使用模式

  • Heap Buffer :将数据存储在JVM的堆空间中

  • Direct Buffer:将数据存储在本地内存中,这样做的好处有

    • 通过本地方法调用分配,除去了中间内存的拷贝,提高IO速度
    • 本地内存不在JVM堆空间中,因此可以降低堆溢出的可能性,并且不需要GC
  • Composite Buffer:将上述二者结合起来,即有存储在堆空间的部分,也有存储在本地内存的部分,使得ByteBuf的使用更加灵活

7、ChannelFuture

Future提供了任务完成时通知引用程序的方式,就如JUC下所定义的一样。

而在netty中,如同ByteBuf之于ByteBuffer,提供了ChannelFuture进行优化。可以通过添加Listener的方式来为future提供回调函数。直接看代码或许能够更好地帮助我们理解,请注意代码中的注释。

//channel进行远程连接,在netty中这是异步的,会立即返回
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1",8888));
//为future注册一个Listener,它重写的方法将会在future执行完毕后被调用
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        System.out.println("连接成功!");
    }
});
复制代码

8、引导客户端与服务器启动

在netty中,我们通过BootStrap类和ServerBootstrap引导启动。

引导启动事实上是一件相对固定的流程,同样的,使用代码加注释的方式在下方给出。

启动服务器

private void start() throws Exception{
    //创建自定义的channelHandler
    final EchoServerHandler serverHandler=new EchoServerHandler();
    //创建EventLoop组,如果这里指定为OIO便是阻塞式,
    //还可以指定如Epoll,Local,Embeded等方式
    EventLoopGroup group=new NioEventLoopGroup();

    try {
        //创建服务器引导
        ServerBootstrap b=new ServerBootstrap();
        //为ServerBootstrap绑定EventLoop组来处理事件
        b.group(group)
                 //指定是channel的实现类,如果这里指定为OIO便是阻塞式,
                 //还可以指定如Epoll,Local,Embeded等方式
                 //注意这里的传输方式需要和EventLoopGroup一致
                .channel(NioServerSocketChannel.class)
                //服务器需要绑定端口
                .localAddress(new InetSocketAddress(port))
                //添加ChannelHandler来处理网络信息
                //可以直接在里面填入一个ChannelHandler
                //当需要多个ChannelHandler时,需要像下方这样在initChannel方法中添加
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(serverHandler);
                    }
                });
        //绑定到端口上
        ChannelFuture f=b.bind();
        //添加回调函数
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                System.out.println("绑定成功");
            }
        });

    }finally {
        //在退出前要关闭group,通过sync()阻塞直到关闭成功
        group.shutdownGracefully().sync();
    }
}
复制代码

启动客户端

public void start() throws Exception {
    //创建EventLoop组,如果这里指定为OIO便是阻塞式,
    //还可以指定如Epoll,Local,Embeded等方式
    EventLoopGroup group=new NioEventLoopGroup();
    try {
        //创建客户端引导
        Bootstrap b = new Bootstrap();
        //为Bootstrap绑定EventLoop组来处理事件
        b.group(group)
                 //指定是channel的实现类,如果这里指定为OIO便是阻塞式,
                 //还可以指定如Epoll,Local,Embeded等方式
                 //注意这里的传输方式需要和EventLoopGroup一致
                .channel(NioSocketChannel.class)
                //这里填写服务器的socket地址
                .remoteAddress(new InetSocketAddress(host, port))
                //添加ChannelHandler来处理网络信息
                //可以直接在里面填入一个ChannelHandler
                //当需要多个ChannelHandler时,需要像下方这样在initChannel方法中添加
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new EchoClientHandler());
                    }
                });
        //这里当然也可以为future添加listener,只是为了演示如何主动关闭使用了下面的方式
        //通过sync()方法阻塞直到连接成功
        ChannelFuture f = b.connect().sync();
        //通过sync()方法阻塞直到channel关闭成功
        f.channel().closeFuture().sync();
    }finally {
        //在退出前要关闭group,通过sync()阻塞直到关闭成功
        group.shutdownGracefully().sync();
    }
}
复制代码

9、小结

至此,如何使用netty以及大致介绍完毕了,我们不妨再回看文章开头提到的netty优良特性,或许会有新的收获:

  • 支持异步:只需要在启动时指定EventLoopGroup和channel为异步的即可,netty进行封装
  • 事件驱动:netty的处理网络信息的过程本质上就是网络信息传入传出时在ChannelHandler中一步步的加工
  • 快速开发:netty本身作为框架就有规范代码便于开发的功能,而其中又提供了规范的ChannelHandler,我们只需要重写其中方法即可;又提供了大量的编解码器,使得我们可以拔插式的添加协议解析
  • 高性能:netty的异步通信、ByteBuf零拷贝与池化技术、Reactor事件驱动模型、异步future与回调、支持自定义协议解析、pipline中无锁的串行化设计、内部大量的并发优化如CAS等为netty提供了强大的性能。

10、其他

  • 本文只是简单地介绍了netty的使用与设计,详细的解析可以参阅Norman Maurer著的【Netty实战】,同时亦附上书中提供的代码,主要参阅第2章、第12章、第13章提供的实践案例
  • 本文中使用的netty版本为4.1.12.Final,maven依赖如下:
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.12.Final</version>
</dependency>
复制代码

参考:

猜你喜欢

转载自juejin.im/post/7150198854237814792