Netty快速上手:Netty没有你想象的那么难

该文章是Netty相关文章。目的是让读者可以快速的了解netty的相关知识以及开发方法。所以本文章在正式介绍Netty开发前先介绍了Netty的前置相关内容:线程模型,JavaNIO,零拷贝等。本文章以大纲框架的形式总体介绍了Netty,但愿对读者有些帮助。文中图片多来自于百度网络,若是有侵权,能够联系我进行删除。内容如有不当欢迎在评论区指出。

Netty

netty是由JBOSS提供的一个Java开源框架,是一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能,高可靠性的网络IO程序.java

NIO模型

  1. 阻塞IO:发起请求就一直等待,直到数据返回。在IO执行的两个阶段都被block了

  1. 非阻塞IO:应用程序不断在一个循环里调用recvfrom,轮询内核,看是否准备好了数据,比较浪费CPU

  1. io复用:一个或一组线程处理多个链接能够同时对多个读/写操做的IO函数进行轮询检测,直到有数据可读或可写时,才真正调用IO操做函数

  1. 信号驱动IO:事先发出一个请求,当有数据后会返回一个标识回调,而后经过recvfrmo去请求数据

  1. 异步io:发出请求就返回,剩下的事情会异步自动完成,不须要作任何处理

异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程能够开始 I/O。

Java NIO

  1. 三大核心Channel(通道),Buffer(缓冲区),Selector(选择器)。数据老是从通道读取到缓冲区中,或者从缓冲区写入到通道中,Selector用于监听多个通道的事件。
  2. Channel:是双向的,既能够用来进行读操做,又能够用来进行写操做bootstrap

    • FileChannel 文件IO,不支持非阻塞模式,没法同Selector一同使用。
    • DatagramChannel 用于处理UDP的链接。
    • SocketChannel 用于处理TCP客户端的链接。
    • ServerSocketChannel 用于处理TCP服务端的链接。
  3. Buffer:它经过几个变量来保存这个数据的当前位置状态:设计模式

    • capacity:缓冲区数组的总长度
    • position:下一个要操做的数据元素的位置
    • limit:缓冲区数组中不可操做的下一个元素的位置
  4. 向Buffer中写数据:api

    • 从Channel写到Buffer (fileChannel.read(buf))
    • 经过Buffer的put()方法 (buf.put(…))
  5. 从Buffer中读取数据:数组

    • 从Buffer读取到Channel (channel.write(buf))
    • 使用get()方法从Buffer中读取数据 (buf.get())
  6. Buffer经常使用方法缓存

    1. flip():写模式下调用flip()以后,Buffer从写模式变成读模式。limit设置为position,position将被设回0
    2. clear()方法:position将被设回0,limit设置成capacity,Buffer被清空了,但Buffer中的数据并未被清除。
    3. compact():将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素正后面,limit设置成capacity,准备继续写入。读模式变成写模式
    4. Buffer.rewind()方法将position设回0,因此你能够重读Buffer中的全部数据
  7. Selector:Selector一块儿使用时,Channel必须处于非阻塞模式下。经过channel.register,将channel登记到Selector上,同时添加关注的事件(SelectionKey),经常使用方法以下:安全

    • select()阻塞到至少有一个通道在你注册的事件上就绪了。
    • select(long timeout)和select()同样,除了最长会阻塞timeout毫秒(参数)。
    • selectNow()不会阻塞,无论什么通道就绪都马上返回
    • selectedKeys()方法访问就绪的通道。Selector不会本身从已选择键集中移除SelectionKey实例。

NIO其余功能:

  1. MappedByteBuffer是NIO引入的文件内存映射方案,读写性能极高。
  2. transferFrom & transferTo:FileChannel的transferFrom()方法能够将数据从源通道传输到FileChannel中.
  3. 分散(scatter)从Channel中读取是指在读操做时将读取的数据写入多个buffer中。所以,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
  4. 汇集(gather)写入Channel是指在写操做时将多个buffer的数据写入同一个Channel,所以,Channel 将多个Buffer中的数据“汇集(gather)”后发送到Channel。

Linux的NIO:

  1. select:阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间。当select函数返回后能够遍历文件描述符,找到就绪的描述符

缺点:服务器

1. 单进程所打开的FD是具备必定限制的,
2. 套接字比较多的时候,每次select()都要经过遍历Socket来完成调度,无论哪一个Socket是活跃的,都遍历一遍。这会浪费不少CPU时间
3. 每次都须要把fd集合从⽤用户态拷贝到内核态,这个开销在fd不少时会很⼤大
  1. poll:本质上和select没有区别,fd使用链表实现,没有最大链接数的限制。网络

    • 缺点:多线程

      1. 大量的fd数组都须要从用户态拷贝到内核态。
      2. poll的“水平触发”:若是报告了fd后,没有被处理,则下次poll还会再次报告该fd。
  2. epoll:
    epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

    • LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll都会返回它的事件,提醒用户程序去操做;
    • ET(边缘触发)模式下,对于每个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,不然下次的 epoll不会返回余下的数据,会丢掉事件(只通知一次)。
**epoll底层原理**:调用epoll_create后,内核cache里建了个红黑树用于存储之后epoll_ctl传来的socket,创建一个rdllist双向链表,用于存储准备就绪的事件。在epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据便可。有数据就返回,没有数据就阻塞。

零拷贝:

对一个操做系统进程来讲,它既有内核空间(与其余进程共享),也有用户空间(进程私有),它们都是处于虚拟地址空间中。进程没法直接操做I/O设备,必须经过操做系统调用请求内核来协助完成I/O动做。将静态文件展现给用户须要先将静态内容从磁盘中拷贝出来放到内存buf中,而后再将这个buf经过socket发给用户
问题:经历了4次copy过程,4次内核切换

1. 用户态到内核态:调用read,文件copy到内核态内存
2. 内核态到用户态:内核态内存数据copy到用户态内存
3. 用户态到内核态:调用writer:用户态内存数据到内核态socket的buffer内存中
4. 最后内核模式下的socket模式下的buffer数据copy到网卡设备中传送
5. 从内核态回到用户态执行下一个循环

Linux:零拷贝技术消除传输数据在存储器之间没必要要的中间拷贝次数,减小了用户进程地址空间和内核地址空间之间由于上下文切换而带来的开销。

常见零拷贝技术

  • mmap():应用程序调用mmap(),磁盘上的数据会经过DMA被拷贝到内核缓冲区,而后操做系统会把这段内核缓冲区与应用程序共享,这样就不须要把内核缓冲区的内容往用户空间拷贝。数据向网络中写时,只须要把数据从这块共享的内核缓冲区中拷贝到socket缓冲区中去就好了,这些操做都发生在内核态.
  • sendfile():DMA将磁盘数据复制到kernel buffer,而后将内核中的kernel buffer直接拷贝到socket buffer;一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、表明数据转化的完成。
  • splice():从磁盘读取到内核buffer后,在内核空间直接与socket buffer创建pipe管道,不须要内核支持。
  • DMA scatter/gather:批量copy

零拷贝不只仅带来更少的数据复制,还能带来其余的性能优点,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

netty 介绍

Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。

  1. 设计优雅:适用于各类传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,能够清晰地分离关注点;
  2. 高度可定制的线程模型 - 单线程,一个或多个线程池.
  3. 使用方便:详细记录的 Javadoc,用户指南和示例;没有其余依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就 足够了。
  4. 高性能、吞吐量更高:延迟更低;减小资源消耗;最小化没必要要的内存复制。
  5. 安全:完整的 SSL/TLS 和 StartTLS 支持。
  6. 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 能够被及时修复,同时,更多的新功能会被加入
  7. Java原生NIO使用起码麻烦须要本身管理线程,Netty对JDK自带的NIO的api进行了封装,提供了更简单优雅的实现方式。因为netty5使用ForkJoinPool增长了复杂性,而且没有显示出明显的性能优点,因此netty5如今被废弃掉了。

netty线程模型

Reactor模式:是事件驱动的,多个并发输入源。它有一个服务处理器,有多个请求处理器;这个服务处理器会同步的将输入的客户端请求事件多路复用的分发给相应的请求处理器。

单Reactor单线程:多路复用、事件分发和消息的处理都是在一个Reactor线程上完成。

* 优势:
    * 模型简单,实现方便
* 缺点:
    
    * 性能差:单线程没法发挥多核性能,
    * 可靠性差:线程意外终止或死循环,则整个模块不可用

单Reactor多线程
一个Reactor线程负责监听服务端的链接请求和接收客户端的TCP读写请求;NIO线程池负责消息的读取、解码、编码和发送

优势:能够充分的利用多核cpu的处理能

缺点:Reactor处理全部事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈.

主从 Reactor 多线程
MainReactor负责监听服务端的链接请求,接收到客户端的链接后,将SocketChannel从MainReactor上移除,从新注册到SubReactor线程池的线程上。SubReactor处理I/O的读写操做,NIO线程池负责消息的读取、解码、编码和发送。

netty工做原理图

NioEventLoopGroup:主要管理 eventLoop 的生命周期,能够理解为一个线程池,内部维护了一组线程,每一个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程
ChannelHandler用于处理Channel对应的事件
示例代码

public class NettyServer {
    public static void main(String[] args) throws Exception {

        //bossGroup和workerGroup分别对应mainReactor和subReactor
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup)
                //用来指定一个Channel工厂,mainReactor用来包装SocketChannel.
                .channel(NioServerSocketChannel.class)
                //用于指定TCP相关的参数以及一些Netty自定义的参数
                .option(ChannelOption.SO_BACKLOG, 100)
                //childHandler()用于指定subReactor中的处理器,相似的,handler()用于指定mainReactor的处理器
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    //ChannelInitializer,它是一个特殊的Handler,功能是初始化多个Handler。完成初始化工做后,netty会将ChannelInitializer从Handler链上删除。
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //addLast(Handler)方法中不指定线程池那么将使用默认的subReacor即woker线程池执行处理器中的业务逻辑代码。
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new MyServerHandler());
                    }
                });
        //sync() 同步阻塞直到bind成功
        ChannelFuture f = bootstrap.bind(8888).sync();
        //sync()同步阻塞直到netty工做结束
        f.channel().closeFuture().sync();

    }
}

线程组

  • NioEventLoopGroup:

    1. NioEventLoopGroup初始化时未指定线程数,那么会使用默认线程数。
    2. 每一个NioEventLoopGroup对象内部都有一组可执行的NioEventLoop数组。
    3. 当有IO事件来时,须要从线程池中选择一个线程出来执行,这时候的NioEventLoop选择策略是由EventExecutorChooser实现的,并调用该类的next()方法。
    4. 每一个NioEventLoopGroup对象都有一个NioEventLoop选择器与之对应,其会根据NioEventLoop的个数,EventExecutorChooser(若是是2的幂次方,则按位运算,不然使用普通的轮询)
  • NioEventLoop
    NioEventLoop 肩负着两种任务:

    1. 做为 IO 线程, 执行与 Channel 相关的 IO 操做, 包括 调用 select 等待就绪的 IO 事件、读写数据与数据的处理等;
    2. 做为任务队列, 执行 taskQueue 中的任务, 例如用户调用 eventLoop.schedule 提交的定时任务也是这个线程执行的

BootStrap和ServerBootstrap

ServerBootstrap是一个工具类,用来配置netty

  1. channel():提供一个ChannelFactory来建立channel,不一样协议的链接有不一样的 Channel 类型与之对应,常见的Channel类型:

    • NioSocketChannel, 表明异步的客户端 TCP Socket 链接.
    • NioServerSocketChannel, 异步的服务器端 TCP Socket 链接.
    • NioDatagramChannel, 异步的 UDP 链接
  2. group():配置工做线程组,用于处理channel的事件
  3. ChannelHandler():用户自定义的事件处理器

出站和入站:

ChannelHandler下主要是两个子接口

  1. ChannelInboundHandler(入站): 处理输入数据和Channel状态类型改变。

    • 适配器: ChannelInboundHandlerAdapter(适配器设计模式)
    • 经常使用的: SimpleChannelInboundHandler
  2. ChannelOutboundHandler(出站): 处理输出数据

    • 适配器: ChannelOutboundHandlerAdapter

ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操做,一个贯穿 Netty 的链。每一个新的通道Channel,Netty都会建立一个新的ChannelPipeline,并将器pipeline附加到channel中。DefaultChinnelPipeline它的Handel头部和尾部的Handel是固定的,咱们所添加的Handel是添加在这个头和尾以前的Handel。

ChannelHandlerContext:ChannelPipeline并非直接管理ChannelHandler,而是经过ChannelHandlerContext来间接管理。

image

Netty编码器

网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端须要对数据进行解码

  • encoder 负责把业务数据转换成字节码数据
  • decoder 负责把字节码数据转换成业务数据

Netty提供了一些默认的编码器:
StringEncoder:对字符串数据进行编码
ObjectEncoder:对 Java 对象进行编码
StringDecoder:对字符串数据进行解码
ObjectDecoder:对 Java 对象进行解码

抽象解码器

  1. ByteToMessageDecoder: 用于将字节转为消息,须要检查缓冲区是否有足够的字节
  2. ReplayingDecoder: 继承ByteToMessageDecoder,不须要检查缓冲区是否有足够的字节,可是ReplayingDecoder速度略慢于ByteToMessageDecoder,同时不是全部的ByteBuf都支持。

    • 选择:项目复杂性高则使用ReplayingDecoder,不然使用 ByteToMessageDecoder
  3. MessageToMessageDecoder: 用于从一种消息解码为另一种消息

TCP粘包:

UDP是基于帧的,包的首部有数据报文的长度.TCP是基于字节流,没有边界的。TCP的首部没有表示数据长度的字段。

  • 发生TCP粘包或拆包的缘由:

    1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
    2. 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
    3. 要发送的数据小于TCP发送缓冲区的大小,TCP将屡次写入缓冲区的数据一次发送出去,将会发生粘包。
    4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
  • 解决方式:
  1. 发送定长消息,若是位置不够,填充特殊字符
  2. 在每个包的尾部加一个特殊分割符
  3. 发送端给每一个数据包添加包首部,首部中应该至少包含数据包的长度。
  • Netty 已经提供了编码器用于解决粘包。

    1. LineBasedFrameDecoder 能够基于换行符解决。
    2. DelimiterBasedFrameDecoder可基于分隔符解决。
    3. FixedLengthFrameDecoder可指定长度解决。

netty的零拷贝

Netty彻底工做在用户态的,Netty的零拷贝更多的对数据操做的优化。

Netty的零拷贝(或者说ByteBuf的复用)主要体如今如下几个方面:

  1. DirectByteBuf经过直接在堆外分配内存的方式,避免了数据从堆内拷贝到堆外的过程
  2. 经过组合ByteBuf类:即CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf, 而不须要进行数据拷贝
  3. 经过各类包装方法, 将 byte[]、ByteBuffer等包装成一个ByteBuf对象,而不须要进行数据的拷贝
  4. 经过slice方法, 将一个ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免了内存的拷贝,这在须要进行拆包操做时很是管用
  5. 经过FileRegion包装的FileChannel.tranferTo方法进行文件传输时, 能够直接将文件缓冲区的数据发送到目标Channel, 减小了经过循环write方式致使的内存拷贝。可是这种方式是须要获得操做系统的零拷贝的支持的,若是netty所运行的操做系统不支持零拷贝的特性,则netty仍然没法作到零拷贝。
相关文章
相关标签/搜索