netty基本概念

netty作为java阵营的网络库。因为其性能优势,使用相当广泛。

  • 很好的抽象,能够方便切换各种实现(select,poll,epoll)模型
  • 一个给定channel的io操作都是由相同的Thread(对应的EventLoop)执行的,实际上消除了对于同步的需要。
  • 处理链,channelPipeline。

netty了解了下面几个类,对netty就有了初步的理解了。

  1. ByteBuf:封装ByteBuffer,提供对网络数据的支持
  2. Channel:代表socket。关联EventPoll:代表work线程。
  3. ChannelHandler:对于事件对具体的业务处理。也是程序员关心最多的接口
  4. ChannelFuture:扩展系统的Future。不用阻塞等待,可以添加listenner,并且用于channle关联的线程执行listenner。

1. ByteBuf

jdk提供的ByteBuffer,提供能在分配直接内存,在堆外的内存,提供更高的性能,更适合用来做io。netty在其基础上实现了ByteBuf。扩展为更加易用的功能。包括提供读写双index,动态扩展,池化,和自定义扩展。

+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex   <=   writerIndex    <=    capacity

直接内存

直接内存,是堆外内存。Netty默认的I/O buffer 使用的是DirectByteBuf直接内存,可以在调用系统api的时候,直接传入,少了从java堆到本机堆的拷贝,提供所谓的零拷贝。

1:传送文件的时候,直接从文件到网络接口。不用先读取文件到用户空间,再发送。(对应的系统层面,Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间,) 2:CompositeByteBuf,消除了用户层,逻辑和逻辑之间的copy。两个生成的ByteBuf,当做一个直接发送。 3:ByteBuffer,在调用io,系统api的时候,少了本地堆和系统堆的copy

ByteBuf的管理

  1. 内存池 在网络请求,返回中,频繁申请的ByteBuf内存,往往是“朝生夕灭”。通过内存池的方式,可以大大降低GC频率,提高性能。
ByteBuf buf = Unpooled.buffer(10*1024)
buf.release();

\\内存池
ByteBufAllocator alloc = channel.alloc();
ByteBuf buffer = alloc.buffer();

提到内存池,也顺带提下netty实现的FastThreadLocal。比JDK版本提供更高的访问效率。用自增的index而不是hash来查找变量。并且解决了伪共享的问题。 netty处理消息,需要频繁的分配内存。内存池就用到了FastThreadLocal。一批channel公用一个内存池。

  1. 引用计数

平常使用bytebuf的时候,一般不用手都调用release释放内存,因为netty在许多地方会隐私调用release情况,情况如下:

  1. 继承SimpleChannelInboundHandler。在channelRead0后,基类会调用ReferenceCountUtil.release(msg)
  2. 解码器的自动释放。解码器的基类在解码后会嗲用rellease释放bytebuf。
  3. 调用发送自动释放。ctx.writeAndFlush(byteBuf)

2. netty的线程模型

eventLoop

eventLoop也是一种设计模式。通常和reactor配合使用。 reactor和proactor是常拿来比较的两种高性能IO设计模式。区别在于谁负责真正的读取操作,reactor相当于linux的epoll,proactor相当于windows的iocp)

EventLoopGroup由EventLoopGroup指定线程数。创建对应的EventLoop。

继承ScheduledExecutorService接口,能像java线程池一样执行任务。其实和标准的epoll实现上,有点类似。

eventLoop是维护一个单线程,并且循环处理所有io事件和任务。不同的eventLoop继承自SingleThreadEventLoop并实现各自的run方法。

任务队列

基类用LinkedBlockingQueue实现的任务队列,NioEventLoop用JCTOOls里的队列,专门优化多个生产者一个消费者的情况。最大化的减少锁的竞争。消息发送就封装成任务被执行。

每次run的时候,会根据ioRatio来合理分配io和任务的执行时间。

传输模型

提供多种传输模型,并且能够方便的切换。

常用的非阻塞,nio,NioEventLoopGroup。利用jdk 1.4 提供的Selector API。底层可能使用poll或者epoll来实现的。如果是linux就是epoll实现的。但是已更加通用的方式使用epoll。所以性能上有妥协

  • nioEventLoop

用Selector的Api,实现对事件的等待。每个EventLoop,只会等待,对应的socket。

  • epollEventLoop

netty自己实现native的代码,调用epool的api,来等待事件。每个epollEventLoop都有自己的epoll内核对象,保证了特点2.

Executor

eventLoop还有executor。ThreadPerTaskExecutor。为什么用eventLoopGroup,而不用线程池。因为标准的线程池,设计成为不阻塞的任务。但是netty的任务都是循环,selector的结构。

线程绑定

新建立的channel,会将channel和其中一个eventLoop绑定。一个eventLoop会负责与其绑定的所有channel。这样,同一个连接的所有请求都是串行执行,避免了channelHandler的并发问题。

即使增加了线程池,将channelhandler和io线程eventloop分开。同一个channel还是会分配对应的业务线程,保证串行。

pipeline.addLast(executor, "handler", handler);

eventloop执行的时候会判断是否在eventloop。channel.eventLoop()

if (inEventLoop()) {
        register0(ch, interestOps, task);
    } else {
        try {
            submit(new Runnable() {
                @Override
                public void run() {
                    register0(ch, interestOps, task);
                }
            }).sync();
        } catch (InterruptedException ignore) {
            Thread.currentThread().interrupt();
        }
    }

3. 其他

channelPipeline

pipeline用到了两个设计模式。

  • adapter:ChannelInboundHandlerAdapter。提供了默认实现,传递给下一个handler,可以继承来修改需要修改的地方
  • 拦截过滤器:Pipeline,事件链

netty提供了很多可以方便使用的channelhandle或者解码器。

ByteToMessageDecoder提供字节到了msg的解码基类。有很多继承于它的实现可以直接使用,满足大部分使用场景。LineBasedFrameDecoder可以每隔每行输入。LengthFieldBasedFrameDecoder通过长度分割。

服务器的初始化

public void start() {
    final CmdChannelHandler cmdChannelHandler = new CmdChannelHandler();
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LineBasedFrameDecoder(100)).addLast(cmdChannelHandler);
                }
            });
    bootstrap.bind("0.0.0.0", 9977).sync().channel();
}
  • 设置服务器的监听和work的线程组,bossGroup和workerGroup。
  • 指定class NioServerSocketChannel,去创建NIO的Channel
  • 自定义一个channelHandler来创建channel,并设置其pipeline。

Updated: