Netty通信技术
Java IO模型
同步/异步,阻塞/非阻塞
在I/O操作中有这么两组概念,其中同步/异步要和线程中的同步线程/异步线程要区分开,这里指的是同步 IO/异步IO
- 阻塞/非阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。非阻塞遇到这些情况,都是直接返回。
- 同步/异步:数据就绪后需要自己去读是同步,数据就绪后系统直接读好再回调给程序是异步。
JAVA BIO模型
同步阻塞IO
BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接口在java.io下 BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,I/O操作都是基于流Stream的操作
- 线程开销:客户端的并发数与后端的线程数成1:1的比例,线程的创建、销毁是非常消耗系统资源的,随着并发量增大,服务端性能将显著下降,甚至会发生线程堆栈溢出等错误
- 线程阻塞:当连接创建后,如果该线程没有操作时,会进行阻塞操作,这样极大的浪费了服务器资源
JAVA NIO模型
同步非阻塞 IO
NIO,称之为New IO 或是 non-block IO (非阻塞IO),这两种说法都可以,其实称之为非阻塞IO更恰当一些
NIO的三大核心组件
Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)
Buffer(缓冲区)
Buffer是一个对象,包含一些要写入或者读出的数据,体现了与原I/O的一个重要区别,在面向流的I/O中,数据读写是直接进入到Steam中,而在NIO中,所有数据都是用缓冲区处理的,读数据直接从缓冲区读,写数据直接写入到缓冲区。缓冲区的本质是一个数组,通常是一个字节数组(ByteBuffer),也可以使用其他类型,但缓冲 区又不仅仅是一个数组,它还提供了对数据结构化访问以及维护读写位置等操作。
Channel(通道)
Channel 是一个通道,管道,网络数据通过Channel读取和写入,Channel和流Stream的不同之处在于Channel是双向的,流只在一个方向上移动(InputStream/OutputStream),而Channel可以用于读写同时进行,即Channel是全双工的。
ServerSocketChannel和SocketChannel
Selector(选择器/多路复用器)
Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,即该Channel处于就绪状态,它就会被Selector轮询出来,然后通过selectedKeys可以获取就绪Channel的集合,进行后续的I/O操作。
JAVA AIO模型
异步非阻塞IO在NIO中,Selector多路复用器在做轮询时,如果没有事件发生,也会进行阻塞,如何优化?
AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。
AIO的基本流程
用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
AIO缺点
- windows下实现成熟,但很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
- Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以 Linux 下,实现高并发网络编程时都是以 NIO 多路复用模型模式为主。
Reactor 线程模型
并发编程模型
Reactor线程模型不是Java专属,也不是Netty专属,它其实是一种并发编程模型,是一种思想,具有指导意义
Reactor模型中定义了三种角色:
- Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
- Acceptor:处理客户端新连接,并分派请求到处理器链中。
- Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel
单Reactor-单线程
NIO下Reactor单线程
所有的接收连接,处理数据的相关操作都在一个线程中来完成,性能上有瓶颈
单Reactor-多线程
NIO下Reactor多线程
把比较耗时的数据的编解码运算操作放入线程池中来执行,提升了性能但还不是最好的方式
主从Reactor-多线程
主从Reactor多线程
主从多线程,对于服务器来说,接收客户端的连接是比较重要的,因此将这部分操作单独用线程去操作
主从Reactor工作模式
工作流程
- Reactor主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。
- 当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由SubReactor监听后面的IO事件。)
- SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。
- 当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。
- Handler通过read从连接上读取请求数据,将请求数据分发给Worker线程池进行业务处理。
- Worker线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过send向客户端发送响应数据。
- 一个 MainReactor可以对应多个SubReactor,即一个MainReactor 线程可以对应多个 SubReactor 线程
主从Reactor优势
- MainReactor线程与SubReactor线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor线程完成后续的业务处理。
- MainReactor线程与SubReactor线程的数据交互简单,MainReactor线程只需要把新连接传给SubReactor 线程,SubReactor 线程无需返回数据。
- 多个SubReactor线程能够应对更高的并发请求。
这种模式的缺点是编程复杂度较高。但是由于其优点明显,在许多项目中被广泛使用,包括 Nginx、Memcached、Netty 等。
这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,1 只是表示相对较少)连接建立线程+M 个IO 线程+N个业务处理线程。这是业界成熟的服务器程序设计模式。
Netty概述
Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供非阻塞的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
- 本质:网络应用程序框架
- 实现:异步、事件驱动
- 特性:高性能、可维护、快速开发
- 用途:开发服务器和客户端
参考资料
关于异步
线程同步、异步是相对的,在请求或执行过程中,如果会阻塞等待,就是同步操作,反之就是异步操作
Netty核心架构
核心:
- 可扩展的事件模型
- 统一的通信api,简化了通信编码
- 零拷贝机制与丰富的字节缓冲区
传输服务:
- 支持socket以及datagram(数据报)
- http传输服务
- In-VM Pipe(管道协议,是jvm的一种进程)
协议支持:
- http以及websocket
- SSL安全套接字协议支持
- Google Protobuf (序列化框架)
- 支持zlib、gzip压缩
- 支持大文件的传输
- RTSP(实时流传输协议,是TCP/IP协议体系中的一个应用层协议)
- 支持二进制协议并且提供了完整的单元测试
netty对比原生jdk
开发网络应用不选JDK原生API而选Netty的理由
- API: Netty更友好更强大,JDK中NIO的一些API功能薄弱且复杂,Netty隔离了JDK中NIO的实现变化及实现细节譬如:ByteBuffer -> ByteBuf主要负责从底层的IO中读取数据到ByteBuf,然后传递给应用程序,应用程序处理完之后封装为ByteBuf,写回给IO
- 并发编程: Netty自身线程安全使用JDK原生API需要对多线程要很熟悉因为NIO涉及到Reactor设计模式,得对里面的原理要相当的熟悉
- 高可用: 完整的高可用机制,JDK原生方式要实现高可用,需要自己实现断路重连、半包读 写、粘包处理、失败缓存处理等相关操作,而Netty则做的更多,它解决了传输的一些问题譬如粘包半包现象,它支持常用的应用层协议,完善的断路重连,idle等异常处理
- Bug: JDK NIO BUG,JDK的NIO存在bug,如经典的epoll bug,会导致CPU 100%,而Netty封装的更完善。
使用Netty的典型项目
- 数据库: Cassandra
- 大数据处理: Spark、Hadoop
- Message Queue:RocketMQ
- 检索: Elasticsearch
- 框架:gRPC、Apache Dubbo、Spring5(响应式编程WebFlux)
- 分布式协调器:ZooKeeper
- 工具类: async-http-client
Netty对三种IO的支持
Netty的使用
Netty中的Reactor实现
Netty对Reactor的支持
Netty线程模型是基于Reactor模型实现的,对Reactor三种模式都有非常好的支持,并做了一定的改进,也非常的灵活,一般情况,在服务端会采用主从架构模型。
Netty工作流程
- Netty抽象出两组线程池:BossGroup和WorkerGroup,每个线程池中都有EventLoop 线程(可以是OIO,NIO,AIO)。BossGroup中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写, EventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环
- EventLoop 表示一个不断循环的执行事件处理的线程,每个EventLoop 都包含一个Selector,用于监听注册在其上的Socket网络连接(Channel)。
- 每个BossEventLoop中循环执行以下三个步骤:
- select:轮训注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
- processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个SocketChannel,并将其注册到某个WorkerEventLoop上的Selector上
- runAllTasks:再去以此循环处理任务队列中的其他任务
- 每个WorkerEventLoop中循环执行以下三个步骤:s
- select:轮训注册在其上的SocketChannel 的read/write事件(OP_READ/OP_WRITE 事件)
- processSelectedKeys:在对应的SocketChannel上处理 read/write 事件
- runAllTasks:再去以此循环处理任务队列中的其他任务
- 在以上两个processSelectedKeys步骤中,会使用Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。
Netty线程模型总结
- Netty的线程模型基于主从多Reactor模型。通常由一个线程负责处理OP_ACCEPT事件,拥有CPU 核数的两倍的IO线程处理读写事件
- 一个通道的IO操作会绑定在一个IO线程中,而一个IO线程可以注册多个通道
- 在一个网络通信中通常会包含网络数据读写,编码、解码、业务处理。默认情况下网络数据读写,编码、解码等操作会在IO线程中运行,但也可以指定其他线程池。
- 通常业务处理会单独开启业务线程池(看业务类型),但也可以进一步细化,例如心跳包可以直接在IO线程中处理,而需要再转发给业务线程池,避免线程切换
- 在一个IO线程中所有通道的事件是串行处理的。
- 通常业务操作会专门开辟一个线程池,那业务处理完成之后,如何将响应结果通过 IO 线程写入到网卡中呢?业务线程调用 Channel对象的write方法并不会立即写入网络,只是将数据放入一个待写入缓存区,然后IO线程每次执行事件选择后,会从待写入缓存区中获取写入任务,将数据真正写入到网络中
Pipeline和Handler
ChannelPipeline & ChannelHandler
ChannelPipeline 提供了 ChannelHandler 链的容器。以服务端程序为例,客户端发送过来的数据要接收,读取处理,我们称数据是入站的,需要经过一系列Handler处理后;如果服务器想向客户端写回数据,也需要经过一系列Handler处理,我们称数据是出站的。
ChannelHandler分类
inbound/outbound
对于数据的出站和入站,有着不同的ChannelHandler类型与之对应:
- ChannelInboundHandler 入站事件处理器
- ChannelOutBoundHandler 出站事件处理器
- ChannelHandlerAdapter提供了一些方法的默认实现,可减少用户对于ChannelHandler的编写
- ChannelDuplexHandler:混合型,既能处理入站事件又能处理出站事件。
ChannelHandler 体系结构
inbound/outbound
- inbound入站事件处理顺序(方向)是由链表的头到链表尾,outbound事件的处理顺序是由链表尾到链表头。
- inbound入站事件由netty内部触发,最终由netty外部的代码消费。
- outbound事件由netty外部的代码触发,最终由netty内部消费。
netty的简单实例
服务端
Netty如何使用Reactor模式
入站事件传播
inbound/outbound 加载顺序和执行顺序
执行顺序
- InboundHandler是按照Pipleline的加载顺序(addLast),顺序执行
- OutboundHandler是按照Pipeline的加载顺序(addLast),逆序执行
回写数据时会经过哪些outboundHandler?
回写数据事件流转规则
- 如果是通过Channel对象进行数据回写,事件会从pipeline尾部流向头部
- 如果是通过ChannelHandlerContext对象进行数据回写,事件会从当前handler流向头部
如何让outboundHandler 一定能执行到?
影响因素
如果想让所有的OutboundHandler都能被执行到,可以选择把OutboundHandler放在最后一个有效的InboundHandler之前
- 还有一种做法是通过addFirst加载所有OutboundHandler,再通过addLast加载所有InboundHandler;
- 另外也推荐:使用addLast先加载所有OutboundHandler,然后加载所有InboundHandler(注意考虑加载顺序和执行顺序)
出站事件传播和outboundHandler中的数据修改
- 在outboundhandler中最好不要再通过Channel写数据,会导致事件再次从尾部流动到头部,造成类似递归问题
- 可以在事件向前传播出去之后通过ChannelHandlerContext写数据
客户端
核心组件分析
Bootstrap
作用和类型
- Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动Netty服务
- Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器,区别在于:
- ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的
- 引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个
Channel
概念和作用
- Netty中的Channel是与网络套接字相关的,可以理解为是socket连接,在客户端与服务端连接的时候就会建立一个Channel,它负责基本的IO操作,比如:bind()、connect(),read(),write()等
- 主要作用:
- 通过Channel可获得当前网络连接的通道状态。
- 通过Channel可获得网络连接的配置参数(缓冲区大小等)。
- Channel提供异步的网络I/O操作,比如连接的建立、数据的读写、端口的绑定等。
- 不同协议、不同的I/O类型的连接都有不同的 Channel 类型与之对应
EventLoopGroup&EventLoop
作用和类型
- Netty是基于事件驱动的,比如:连接注册,连接激活;数据读取;异常事件等等,有了事件,就需要一个组件去监控事件的产生和事件的协调处理,这个组件就是EventLoop(事件循环/EventExecutor),在Netty 中每个Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。每个EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。
- EventLoopGroup 是用来生成 EventLoop 的,包含了一组EventLoop(可以初步理解成Netty线程池)
eventLoopThreads 是多少?
- 核心线程数默认:cpu核数*2
- 核心线程数在创建时可通过构造函数指定
- 对于boss group,我们其实也只用到了其中的一个线程,因为服务端一般只会绑定一个端口启动
ChannelHandler ChannelHandlerContext ChannelPipeline
@Sharable
ChannelHandler复用
- 每个客户端Channel创建后初始化时,均会向与该Channel绑定的Pipeline中添加handler,此种模式下,每个Channel享有的是各自独立的Handler
- 使用@Sharable注解,注解只是标注可以被复用,至于线程安全问题需要开发者自行保证。
ChannelInboundHandlerAdapter or SimpleChannelInboundHandler
- 继承SimpleChannelInboundHandler需要重写channelRead0方法,且可以通过泛型指定msg类型
- SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源
注意事项:
- 服务端异步处理数据,服务端想把客户端发送来的数据再写回等等场景下最好不要继承SimpleChannelInboundHandler
- 客户端推荐使用SimpleChannelInboundHandler,服务端看场景
ByteBuf
概念和作用
- Java NIO 提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty使用ByteBuf来替代ByteBuffer,它是一个强大的实现,既解决了JDK API的局限性, 又为网络应用程序的开发者提供了更好的API
- 从结构上来说,ByteBuf由一串字节数组构成。数组中每个字节用来存放信息,ByteBuf提供了两个索引,一个用于读取数据(readerIndex ),一个用于写入数据(writerIndex)。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。而JDK的ByteBuffer只有一个索引,因此需要使用flip方法进行读写切换
ByteBuf的三个指针
- readerIndex:指示读取的起始位置,每读取一个字节,readerIndex自增累加1。如果readerIndex与writerIndex 相等,ByteBuf 不可读。
- writerIndex:指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity() 容量相等,表示 ByteBuf 已经不可写,但是这个时候,并不代表不能往ByteBuf中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容ByteBuf,直到扩容到底层的内存大小为maxCapacity
- maxCapacity:指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足,可以进行扩容的最大容量
常用API
容量API
- capacity():表示ByteBuf底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实现机制有不同的计算方式。
- maxCapacity(): ByteBuf 底层最大能够占用多少字节的内存,当向ByteBuf中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到maxCapacity,超过这个数,就抛异常。
- readableBytes()与isReadable():readableBytes()表示ByteBuf当前可读的字节数,它的值等于writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false
- writableBytes()isWritable()、maxWritableBytes():writableBytes()表示 ByteBuf当前可写的字节数,它的值等于capacity()-writerIndex,如果两者相等,则表示不可写,isWritable()返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了,如果发现往ByteBuf中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩 容到底层的内存大小为maxCapacity,而maxWritableBytes() 就表示可写的最大字节数,它的值等于maxCapacity-writerIndex。
读写指针相关的API
- readerIndex()与readerIndex(int readerIndex):前者表示返回当前的读指针 readerIndex, 后者表示设置读指针
- writeIndex()与writeIndex(int writerIndex):前者表示返回当前的写指针 writerIndex, 后者表示设置写指针
- markReaderIndex() 与markWriterIndex():表示把当前的读指针/写指针保存起来,操作形式为:markedReaderIndex = readerIndex / markedWriterIndex = writerIndex;
读写操作API
- writeBytes(byte[] src):表示把字节数组src 里面的数据全部写到 ByteBuf,src字节数组大小的长度通常小于等于writableBytes()
- readBytes(byte[] dst):把 ByteBuf 里面的数据全部读取到dst,dst字节数组的大小通常等于readableBytes()
- writeByte(int value)、readByte():writeByte() 表示往ByteBuf中写一个字节,而readByte()表示从ByteBuf中读取一个字节,类似的API还有writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble()等等
丢弃、清理,释放
- discardReadBytes():丢弃已读取的字节空间,可写空间变多
- clear():重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容
- release():真正去释放bytebuf中的数据,
- ReferenceCountUtil.release(buf):工具方法,内部还是调用release()
wrap
通过Wrap操作可以快速转换或得到一个ByteBuf对象,Unpooled 工具类中提供了很多重载的wrappedBuffer方法
wrappedBuffer方法不会发生数据的拷贝,而copiedBuffer会发生数据 的拷贝
ByteBuf三类使用模式
- 堆缓冲区(HeapByteBuf):内存分配在jvm堆,分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
- 直接缓冲区(DirectByteBuf):内存分配的是堆外内存(系统内存),相比堆内存,它的分配和回收速度会慢一些,但是将它写入或从Socket Channel中读取时,由于减少了一次内存拷贝,速度比堆内存块。
- 复合缓冲区(CompositeByteBuf):顾名思义就是将两个不同的缓冲区从逻辑上合并,让使用更加方便。
Netty默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,则需要进行系统参数的设置
关于堆外内存的理解
JVM内部
堆(heap)+非堆(non heap)
JVM外部
堆外(off heap)优点:
- 减轻gc压力
- 避免复制
不足:
- 创建速度稍慢
- 受操作系统管理
ByteBuf 的分配器
BufAllocator
Netty 提供了两种 ByteBufAllocator 的实现,分别是:
- PooledByteBufAllocator:实现了 ByteBuf 的对象的池化,提高性能减少并最大限度地减少内存碎片,池化思想通过预先申请一块专用内存地址作为内存池进行管理,从而不需要每次都进行分配和释放
- UnpooledByteBufAllocator:没有实现对象的池化,每次会生成新的对象实例
Netty默认使用了PooledByteBufAllocator,但可以通过引导类设置非池化模式
//引导类中设置非池化模式
bootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
//或者通过系统参数设置
System.setProperty("io.netty.allocator.type", "pooled"); System.setProperty("io.netty.allocator.type", "unpooled");
ByteBuf-pooled & unpooled
pooled & unpooled
- 对于Pooled类型的ByteBuf,不管是PooledDirectByteBuf还是PooledHeapByteBuf都只能由Netty内部自己使用(构造是私有和受保护的),开发者可以使用Unpooled类型的ByteBuf。
- Netty提供Unpooled工具类创建的ByteBuf都是unpooled类型,默认采用的Allocator是direct类型;当然用户可以自己选择创建UnpooledDirectByteBuf和UnpooledHeapByteBuf
ByteBuf 的释放
引用计数
ByteBuf如果采用的是堆缓冲区模式的话,可以由GC回收,但是如果采用的是直接缓冲区,就不受GC的管理,就得手动释放,否则会发生内存泄露,Netty自身引入了引用计数,提供了ReferenceCounted接口,当对象的引用计数>0时要保证对象不被释放,当为0时需要被释放
如何释放?
关于ByteBuf的释放,分为手动释放与自动释放:
- 手动释放,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放,这种方式的弊端就是一旦忘记释放就可能会造成内存泄露
- 自动释放有三种方式,分别是:入站的TailHandler(TailContext)、继承SimpleChannelInboundHandler、HeadHandler(HeadContext)的出站释放
- TailContext:Inbound流水线的末端,如果前面的handler都把消息向后传递最终由TailContext释放该消息,需要注意的是,如果没有进行向下传递,是不会进行释放操作的
- SimpleChannelInboundHandler:自定义的InboundHandler继承自SimpleChannelInboundHandler,在SimpleChannelInboundHandler中自动释放
- HeadContext:outbound流水线的末端,出站消息一般是由应用所申请,到达最后一站时,经过一轮复杂的调用,在flush完成后终将被release掉
总结
对于入站消息:
- 对原消息不做处理,依次调用 ctx.fireChannelRead(msg)把原消息往下传,如果能到TailContext,那不用做什么释放,它会自动释放
- 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那需要将原消息release掉
- 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,需要把原消息release掉。
对于出站消息:
则无需用户关心,消息最终都会走到HeadContext,flush之后会自动释放。
Future & Promise
Netty的异步编程模型
Future/Promise异步模型
- future和promise,目的是将值(future)与其计算方式(promise)分离,从而允许更灵活地进行计算,特别是通过并行化。Future 表示目标计算的返回值,Promise 表示计算的方式,这个模型将返回结果和计算逻辑分离,目的是为了让计算逻辑不影响返回结果,从而抽象出一套异步编程模型。而计算逻辑与结果关联的纽带就是 callback。
- Netty中有非常多的异步调用,譬如:client/server的启动,连接,数据的读写等操作都是支持异步的。
Netty Future
ChannelFuture
跟Channel的操作有关,Netty中的Handler处理都是异步IO,通过ChannelFuture添 加事件监听,可获取Channel异步IO操作的结果;当然也可等待获取,但最好不要在handler中通过future的sync或await来获取异步操作的结果。
Netty Promise
Promise机制
- Netty的Future,只是增加了监听器。整个异步的状态,是不能进行设置和修改的,于是Netty的 Promise接口扩展了Netty的Future接口,可以设置异步执行的结果。在IO操作过程,如果顺利完成、或者发生异常,都可以设置Promise的结果,并且通知Promise的Listener们。
- 在Java的Future中,业务逻辑为一个Callable或Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。
ChannelPromise
ChannelPromise接口,则继承扩展了Promise和ChannelFuture。所以,ChannelPromise既绑定了Channel,又具备了设置监听回调的功能,还可以设置IO操作的结果,是Netty实际编程使用的最多的接口。
TCP 粘包,半包
场景
TCP 粘包拆包分析
粘包
- 发送方每次写入数据<套接字缓冲区大小
- 接收方读取套接字缓冲区数据不够及时
拆包(半包)
- 发送方写入数据> 套接字缓冲区大小
- 发送的最大报文长度大于MSS, 数据包大于协议的MTU(最大传输单元,1500字节),必须拆包
从两个角度看粘包和拆包:
收发角度:
一个发送可能被多次接收(半包),多个发送可能被一次接收(粘包)
传输角度:
一个发送可能占用多个传输包(半包),多个发送可能共用一个传输包(粘包)
根本原因:
TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,是一种流式协议,消息无边界。
TCP 粘包,拆包解决方案
消息边界是个问题
解决TCP粘包,半包问题的根本:找出消息的边界
Netty一次编解码器
netty解决tcp粘包,半包
Netty提供了针对封装成帧这种形式下不同方式的拆包器,所谓的拆包其实就是数据的解码,所谓解码就是将网络中的一些原始数据解码成上层应用的数据,那对应在发送数据的时候要按照同样的方式进行数据的编码操作然后发送到网络中
解码器
分隔符解码器
LineBasedFrameDecoder
DelimiterBasedFrameDecoder
基于长度的域解码器
LengthFieldBasedFrameDecoder
四个重要信息
- lengthFieldOffset:length域的偏移,正常情况下读取数据从偏移为0处开始读取,如果有需要可以从其他偏移量处开始读取
- lengthFieldLength:length域占用的字节数
- lengthAdjustment:在length域和content域中间是否需要填充其他字节数
- initialBytesToStrip:解码后跳过的字节数
编码器
基于长度的域编码器
LengthFieldPrepender
其他编解码器
开箱即用的编解码器
Netty中提供了ByteToMessageDecoder的抽象实现,自定义解码器只需要继承该类,实现decode()即可。Netty也提供了一些常用的解码器实现,用于数据入站的解码操作,基本都是开箱即用的;当然数据出站也需要采用对应的编码器
Netty 消息二次编解码器
Netty codec
二次编解码
- 我们把解决半包粘包问题的常用三种解码器叫一次解码器,其作用是将原始数据流(可能会出现粘包和半包的数据流)转换为用户数据(ByteBuf中存储),但仍然是字节数据,所以我们需要二次解码器将字节数组转换为java对象,或者将一种格式转化为另一种格式,方便上层应用程序使用。
- 一次解码器继承自:ByteToMessageDecoder;二次解码器继承自:MessageToMessageDecoder;但他们的本质都是继承ChannelInboundHandlerAdapter
二次编解码方式
二次编解码
- 用户数据(ByteBuf )和 Java Object之间的转换,或者将将一种格式转化为另一种格式(譬如将应用数据转化成某种协议数据)。
- Java 序列化:不推荐使用,占用空间大,也只有java语言能用
- Marshaling:比java序列化稍好
- XML :可读性好,但是占用空间大
- JSON :可读性也好,空间较小
- MessagePack :占用空间比JSON小,可读性不如JSON,但也还行
- Protobuf :性能高,体积小,但是可读性差
- hessian :跨语言、高效的二进制序列化协议,整体性能和protobuf差不多。
- 其他
常用的二次编解码器
常用二次编解码器使用示例
StringDecoder & StringEncoder
ProtobufDecoder & ProtobufEecoder
Protostuff编解码
github
- protostuff是一个基于protobuf实现的序列化方法,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写.proto文件来实现序列化
- 项目地址:github.com/protostuff/…
编码实现
HTTP 编解码
http服务器
- HTTP服务器在我们日常开发中,常见的实现方式就是实现一个Java Web项目,基于Nginx+Tomcat的方式就可以提供HTTP服务。但是很多场景是非Web容器的场景,这个时候再使用Tomcat就大材小用了。这个时候就可以使用基于Netty的HTTP协议。而且基于Netty开发的HTTP服务器有如下优势:
- Netty的线程模型和异步非阻塞特性能够支持高并发
- 相比于Tomcat HTTP,Netty HTTP更加轻量、小巧、可靠,占用资源更少
HTTP 协议抽象
HTTP 协议抽象-请求
HTTP 协议抽象-响应
HTTP 协议抽象-Content-Length,chunked
HTTP 协议抽象-响应压缩
Netty HTTP 协议抽象的实现-请求和响应
HttpRequestDecoder & HttpObjectAggregator & HttpResponseEncoder
Keepalive 与 idle监测
Keepalvie
场景 | 对端应用 |
---|---|
需要keepalive | 异常崩溃 |
需要keepalive | 网络可达,但是服务处理不过来 |
需要keepalive | 服务崩溃,但网络不可达 |
不做keepalive | 连接已坏,但仍然占用资源 |
TCP Keepalvie
三个参数
- net.ipv4.tcp_keepalive_time = 7200 : 当启用(默认关闭)keepalive 时,TCP 在连接没有数据通过的7200秒后发送keepalive 探测消息.
- net.ipv4.tcp_keepalive_intvl = 75 : 当探测没有确认时,按75秒的重试频率重发。
- net.ipv4.tcp_keepalive_probes = 9 :一直发9 个探测包都没有确认,就认定连接失效。
- 所以总耗时一般为:2 小时11 分钟(7200 秒+ 75 秒* 9 次)
应用层 Keepalvie
应用心跳
除了在tcp网络层开启keepalive之外,我们普遍还需要在应用层启动keepalive,一般称之为:应用心跳(心跳机制),原因如下:
- 协议分层,各层关注点不同,网络传输层关注网络是否可达,应用层关注是否能正常提供服务
- tcp的keepalive默认关闭,并且经过路由等中转设备后keepalive包有可能被丢弃 3、tcp层的keepalive时间太长,默认>2小时,虽然可改,但是属于系统参数一旦改动影响该机器上的所有应用
另外需要注意:http虽然属于应用层协议,因此会经常听到 HTTP 的头信息:Connection: Keep-Alive,HTTP/1.1默认使用Connection:keep-alive进行长连接。在一次TCP连接中可以完成多个 HTTP 请求,但是对每个请求仍然要单独发header,Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中 设定这个时间。这种长连接是一种“伪链接”,而且只能由客户端发送请求,服务端响应。 HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接
idle监测
Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定Idle 监测的最终用途,一般用来配合keepalive ,减少keepalive 消息
idle配合keepalive的发展阶段
- v1:定时keepalive 消息,keepalive 消息与服务器正常消息交换完全不关联,定时就发送;
- v2:空闲监测+ 判定为Idle 时才发keepalive,有其他数据传输的时候,不发送keepalive ,无数据传输超过一定时间,判定为Idle,再发keepalive
idle带来的好处
- 快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态
- 实际应用中:结合起来使用。按需keepalive ,保证不会空闲,如果空闲,关闭连接
idle 案例
案例目标
- 服务器添加read idle check,10s接收不到channel数据就断掉连接,保护自己,瘦身
- 客户端添加write idle check + keepalive,5s不发送数据就发送一个keepalive,避免连接被断,也避免频繁keepalive
性能优化,高级参数设置
参数调优
System 参数
linux系统参数/Netty支持的系统参数
- linux系统参数,例如:/proc/sys/net/ipv4/tcp_keepalive_time
- netty支持的系统参数设置,例如:serverbootstrap.option(ChannelOption.SO_BACKLOG,1024),且设置形式有两种:
- 针对ServerSocketChannel:通过.option设置
- 针对SocketChannel:通过.childOption设置
Linux参数:
进行tcp连接时,系统为每个tcp连接都会创建一个socket句柄,其实就是一个文件句柄(linux一切皆为文件),但是系统对于每个进程能够打开的文件句柄数量 做了限制,超出则报错:Too many open file 设置方式:有很多种,ulimit -n [xxx] 注意:该命令修改的数值,只对当前登录用户目前使用的环境有效,系统重启或用户退出后失效,所以建议的做法是可以作为启动脚本的一部分,在启动程序前执 行
Netty支持的 System 参数
针对ScoketChannel,7个,通过.childOption设置,常用的两个如下:
- SO_KEEPALIVE,tcp层keepalvie,默认关闭,一般选择关闭tcp keepalive 而使用应用keepalive
- TCP_NODELAY:设置是否启用nagle算法,该算法是tcp在发送数据时将小的、碎片化的数据拼接成一个大的报文一起发送,以此来提高效率,默认是false(启用),如果启用可能会导致有些数据有延时,如果业务不能忍受,小报文也需要立即发送则可以禁用该算法
针对ServerScoketChannel,3个,通过.Option设置,常用的一个如下:
- SO_BACKLOG:最大等待连接数量,netty在linux下该值的获取是通过:io.netty.util.NetUtil完成的
- 先尝试获取:/proc/sys/net/core/somaxconn
- 然后尝试:sysctl
- 最终没有获取到使用默认值:PlatformDependent.isWindows() ? 200 : 128;
应用诊断
完善线程名
添加Handler名称 & 日志
线程模型优化
业务类型分类
高级特性
零拷贝-Zero Copy
Netty中的零拷贝
Netty 中的 Zero-copy 与上面我们所提到到 OS 层面上的 Zero-copy 不太一样, Netty的 Zero-coyp 完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念,Netty的Zero-copy主要体现在如下几个方面:
- Direct Buffer: 直接堆外内存区域分配空间而不是在堆内存中分配, 如果使用传统的堆内存分配,当我们需要将数据通过socket发送的时候,需要将数据从堆内存拷贝到堆外直接内存,然后再由直接内存拷贝到网卡接口层,通过Netty提供的Direct Buffers直接将数据分配到堆外内存,避免多余的数据拷贝
- Composite Buffers:传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝;同时也支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
- 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作
- 通过 FileRegion 包装的FileChannel.tranferTo (Java nio)实现文件传输, 可以直接将文件缓冲区的数据发送到目标Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题