Netty框架入门实践

一、创建项目

netty项目可以通过maven创建,需要用到io.netty.netty-all的jar包

pom.xml中添加依赖:


        io.netty
        netty-all
        4.1.32.Final

编写一个简单的服务器实现简单地将客户端的内容返回回去

EchoServer.java和EchoServerHandller.java:

import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;

import java.net.InetSocketAddress;

public class EchoServer {
    private final int port;

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

    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    // 使用NIO的Channel
                    .channel(NioServerSocketChannel.class)
                    // 绑定到本地地址的某个端口
                    .localAddress(new InetSocketAddress(port))
                    // 为每个channel的pipe   line添加handler
                    .childHandler(new ChannelInitializer() {
                        @Override
                        public void initChannel(SocketChannel socketChannel) throws Exception {
                            // 将handler添加到pipeline中
                            socketChannel.pipeline().addLast(serverHandler);
                        }
                    });
            // 绑定,获取future回调
            ChannelFuture future = bootstrap.bind().sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        new EchoServer(9999).start();
    }
}
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

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

可以看到这个Server里用到的了netty里重要的几个组件:

  • 1.NioServerSocketChannel(Channel)
  • 2.NioEventLoopGroup(EventLoopGroup)
  • 3.ChannelHandler和ChannelPipeline
  • 4.ByteBuf
  • 5.ServerBootstrap(Bootstrap)
  • 6.ChannelFuture(Future)

下面分别讲解。

 

二、底层传输抽象Channel

 netty的网络传输抽象为Channel接口,因为netty支持多种传输的实现,上面的各种组件也都有实现了不同传输的不同版本。

  • Nio传输对应io.netty.channel.socket.nio包。如NioServerSocketChannel。
  • Oio传输对应io.netty.channel.socket.oio包。如OioServerSocketChannel。
  • Epoll传输对应io.netty.channel.epoll包。这是一种在linux可用的比Nio传输更高效的传输,基于linux的epoll系统调用。
  • Local传输对应io.netty.channel.local包。是在VM内部通过管道通信的本地传输,用于一个VM上的C/S异步通信。
  • Embedded传输对应io.netty.channel.embedded包。设计成方便测试用的Channel。

上面每个版本的Channel只能和对应版本的组件合作,如要用NioServerSocketChannel就必须使用NioEventLoopGroup。接下来再说和Channel相关的组件:

  • ChannelPipeline:每个Channel创建时都分配有一个Pipeline,入站出站数据和事件都在pipeline上传输和发生。通过在pipeline上注册ChannelHandler响应对应事件并处理入站数据和推送出站数据。
  • ChannelConfig:每个Channell创建时都分配有一个Config,包含了Channel的所有配置设置,支持热更新。
  • EventLoop:每个Channel绑定一个EventLoop,这个EventLoop从一个EventLoopGroup中取出,代表分配给Channel执行传输的线程的模型。每个Channel和EventLoop唯一对应,而EventLoop对应一个线程,相当于每个Channel对应一个线程,这样简化了多线程模型。

Channel可以调用方法返回pipeling,config和eventLoop的引用,还有一些方法:

isActive:判断Channel的状态是否是活动的

localAddress/remoteAddress:返回本地/远程地址

write/flush/writeAndFlush:将数据传递给pipeline出站

 

三、线程模型和EventLoop

EventLoop是事件循环的抽象。

while (!terminated) {
    // 阻塞,直到有事件可以执行
    List readyEvents = blockUntilEventReady();
    for (Runnable event: readyEvents) {
        // 循环遍历处理所有的事件
        event.run();
    }
}

每个Channel的IO操作和事件都交给同一个EventLoop的线程来处理(写操作可以由其他线程触发,写操作传到pipline时处理仍是该EventLoop来处理),但是一个EventLoop可以绑定多个Channel,(EventLoop能识别分配给自己的线程,如果是分配的线程则执行,否则将任务入队留给其他EventLoop识别)只要每个Channel都是在一个EventLoop的线程里执行IO操作和事件就仍具有线程上的简单性。

不过由于一个EventLoop的多个Channel的ThreadLocal都在一个Thread中,故可能产生冲突要慎用,而且一个EventLoop上执行耗时操作可以会阻塞这个EventLoop上的其他Channel事件,所以单个耗时操作尽量将它交给专门的执行耗时任务的线程池来处理,主线程继续去EventLoop,当子线程计算完毕再讲结果交给主线程。

实现细节:

EventLoopGroup,和EventLoop间接继承了ScheduledExecutorService接口。它们的底层为一个线程池,拥有调度任务的接口方法。而且EventLoop调度任务添加了新的方法如定时任务,解决ScheduledExecutorService接口方法的一些问题:

// 执行一个定时任务
ScheduledFuture future = channel.eventLoop().schedule(
    new Runnable() {
        @Override
        public void run() {
            System.out.println("60 seconds later");
        }
    }, 60, TimeUnit.SECONDS);
// ...
boolean mayInterruptIfRunnning = false;
// 取消该任务
future.cancel(mayInterruptIfRunnning);

在非阻塞传输实现中:

一个EventLoopGroup对应多个EventLoop,每个EventLoop和一个Thread关联。一个EventLoop对应多个Channel这里server和client有所不同,client有一个EventLoopGroup,而server有两个EventLoopGroup:BossEventLoopGroup和WorkerEventLoopGroup。

BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例,BoosEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,然后将accept得到的SocketChannel(ChildChannel)交给WorkerEventLoopGroup,WorkerEventLoopGroup会由next选择其中一EventLoop来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。BossEventLoopGroup主要是对多线程的扩展,而每个EventLoop的实现涵盖IO事件的分离,和分发。

在阻塞传输实现中:

一个EventLoopGroup对应多个EventLoop,每个EventLoop和一个Thread关联。一个EventLoop对应一个Channel

四、ChannelHandler和ChannelPipeline

ChannelPipeline

    ChannelPipeline 是一系列ChannelHandler的实例,流经一个Channel的事件(Channel生命周期改变和进行读写操作等事件)和可以被ChannelPipeline拦截,然后交给它上面的ChannelHandler来处理。入站和出站方向只会触发该方向上的Handler。

Channel 生命周期

  • channelRegistered: 注册。
  • channelActive: 活跃状态,可接收和发送数据。
  • channelInactive: 处于非活跃状态,没有连接到远程主机。
  • channelUnregistered: 已创建但未注册到一个 EventLoop。

ChannelHandler

当ChannelPipeline拦截到某个事件时Handler的对应监听方法将被调用,处理事件和数据。

生命周期:

  • handlerAdded: 当 ChannelHandler 添加到 ChannelPipeline 调用
  • handlerRemoved: 当 ChannelHandler 从 ChannelPipeline 移除时调用
  • exceptionCaught: 当 ChannelPipeline 执行抛出异常时调用

ChannelHandler两个重要的接口:

(各种触发事件可以查看API文档)

  • ChannelInboundHandler : 处理入站数据和所有状态更改事件。通常实现类为ChannelInboundHandlerAdapter(需要调用ReferenceCountUtil.release(msg)手动释放资源)和SimpleChannelInboundHandler(自动释放了资源)。
  • ChannelOutboundHandler : 处理出站数据,允许拦截各种操作。通常实现类为ChannelOutboundHandlerAdapter

ChannelHandlerContext

    ChannelHandler添加到Pipeline时自动为该Handler生成一个对应的Context,Context的监听方法被调用时同时会触发下一个Handler的监听方法,由此实现多个Handler的链式调用。调用context的事件方法将会从下一个Handler开始触发,而Channel,ChannelPipeline的方法会触发整条Handler的监听方法,前者可以避免多余的Handler方法被触发。

Netty框架入门实践_第1张图片

 

五、ByteBuf

ByteBuf的类型:

1.Heap Buffer 堆缓冲区

这是最常用的类型,ByteBuf将数据存储在JVM的堆空间,通过将数据存储在支撑数组(hasArray方法返回的值是区分ByteBuf类型的方法)中实现的。

  • 堆缓冲的优点是:由于数据存储在JVM的堆中可以快速创建和快速释放,并且提供了数组的直接快速访问的方法。
  • 堆缓冲缺点是:每次读写数据都要先将数据拷贝到缓冲区再进行传递。

2.Direct Buffer 直接缓冲区

Direct Buffer在堆之外直接分配内存,直接缓冲区不会占用堆的容量。

  • Direct Buffer的优点是:在使用Socket传递数据时性能很好,由于数据直接在内存中,不存在从JVM拷贝数据到缓冲区的过程,性能好。
  • 缺点是:因为Direct Buffer是直接在内存中,所以分配内存空间和释放内存比堆缓冲区更复杂和慢。

虽然netty的Direct Buffer有这个缺点,但是netty通过内存池来解决这个问题。直接缓冲池不支持数组访问数据,但可以通过间接的方式访问数据数组:

ByteBuf directBuf = Unpooled.directBuffer(16);   
if(!directBuf.hasArray()){   
    int len = directBuf.readableBytes();   
    byte[] arr = new byte[len];   
    directBuf.getBytes(0, arr);   
} 

但是上面的操作太过复杂,所以在使用时如果需要对Buffer进行修改,建议一般是用heap buffer。

不过对于一些IO通信线程中读写缓冲时建议使用DirectByteBuffer,因为这涉及到大量的IO数据读写。对于后端的业务消息的编解码模块使用HeapByteBuffer。

3.Composite Buffer 复合缓冲区

复合缓冲区就类似于一个ByteBuf的聚合视图,在这个视图里面我们可以创建不同的ByteBuf(可以是不同类型的)。 这样,复合缓冲区就类似于一个列表,我们可以动态的往里面添加和删除其中的ByteBuf。

ByteBuf默认第一个字节的索引为0,最后一个字节的索引为capacity()-1。另外ByteBuf维护了两个索引readerIndex和writerIndex,读写操作时会分别移动这两个索引。这些索引将ByteBuf分为了三个区域。

  • 可丢弃字节:[0, readerIndex)
  • 可读字节:[readerIndex, writerIndex)
  • 可写字节:[writerIndex, capacity)

清理可丢弃字节可以使用discardReadBytes(),这个方法将清理可丢弃字节然后将数据向前移动清理的字节数,readerIndex和writerIndex也会移动相同数目的字节。还有一个clear()方法,它直接将readerIndex和writerIndex置0。

ByteBuf拥有查找字节,派生(复制,裁剪),读写等常用操作,netty还提供了一些类来帮助管理字节:

  • 有效存储ByteBuf的接口:ByteBufHolder
  • 按需分配ByteBuf的接口:ByteBufAllocator,Channel和ChannelHandlerContext都保存了一个ByteBufAllocator的引用,可以用alloc()方法获取,返回的引用默认为池化实现PooledByteBufAllocator。还有一个非池化实现UnpooledByteBufAllocator。
  • 提供辅助的ByteBufUtil类
  • 非池化缓冲区Unpooled类

 

六、引导Bootstrap

bootstap的功能是将netty的各个部件组装在一起,并运行起来。因为server和client在结构上由所不同,server只需要绑定地址端口而client有绑定和连接两个操作,而且server要为每个请求创建响应的子连接,故netty提供了Bootstrap和ServerBootstrap。

将上面的各个必要的组件(Channel,EventLoopGroup,ChannelHandler等)提供给bootstrap,bootstrap就会自动处理各组件之间的关系,隐藏他们的交互细节。使用bind方法或connect方法进行绑定与连接。

可能需要引导添加多个ChannelHandler时可以在某个Handler的重写方法中获取对应的ChannelPipeline再添加Handler。还可以使用ChannelInitializer类,它继承了ChannelInboundHandlerAdapter类可以使用handler方法或childHandler方法添加到Bootstrap。可以重写initChannel方法在里面获取pipeline再添加Handler。当Channel注册了EventLoop时initChannel方法会被调用。

你可能感兴趣的:(Netty框架入门实践)