该文章是Netty相关文章。目的是让读者能够快速的了解netty的相关知识以及开发方法。因此本文章在正式介绍Netty开发前先介绍了Netty的前置相关内容:线程模型,JavaNIO,零拷贝等。本文章以大纲框架的形式整体介绍了Netty,希望对读者有些帮助。文中图片多来自于百度网络,如果有侵权,可以联系我进行删除。内容若有不当欢迎在评论区指出。
netty是由JBOSS提供的一个Java开源框架,是一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能,高可靠性的网络IO程序.
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
FileChannel 文件IO,不支持非阻塞模式,无法同Selector一同使用
DatagramChannel UDP
SocketChannel TCP
ServerSocketChannel TCP
Buffer:它通过几个变量来保存这个数据的当前位置状态:
capacity:缓冲区数组的总长度
position:下一个要操作的数据元素的位置
limit:缓冲区数组中不可操作的下一个元素的位置
从Channel写到Buffer (fileChannel.read(buf))
通过Buffer的put()方法 (buf.put(…))
从Buffer中读取数据:
从Buffer读取到Channel (channel.write(buf))
使用get()方法从Buffer中读取数据 (buf.get())
clear()方法:position将被设回0,limit设置成capacity,Buffer被清空了,但Buffer中的数据并未被清除。
compact():将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面,limit设置成capacity,准备继续写入。读模式变成写模式
Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据
select()阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。
selectNow()不会阻塞,不管什么通道就绪都立刻返回
selectedKeys()方法访问就绪的通道。Selector不会自己从已选择键集中移除SelectionKey实例。
MappedByteBuffer是NIO引入的文件内存映射方案,读写性能极高。
transferFrom & transferTo:FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中.
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
缺点:
1.单进程所打开的FD是具有一定限制的,
2.套接字比较多的时候,每次select()都要通过遍历Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间
3.每次都需要把fd集合从⽤用户态拷贝到内核态,这个开销在fd很多时会很⼤大
poll:本质上和select没有区别,fd使用链表实现,没有最大连接数的限制。
缺点:大量的fd数组都需要从用户态拷贝到内核态。poll的“水平触发”:如果报告了fd后,没有被处理,则下次poll还会再次报告该fd
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll都会返回它的事件,提醒用户程序去操作;
ET(边缘触发)模式下,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,否则下次的 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 校验和计算。
完全是在用户态的,更多的是数据操作的优化
1.Netty的零拷贝(或者说ByteBuf的复用)主要体现在以下几个方面:
 DirectByteBuf通过直接在堆外分配内存的方式,避免了数据从堆内拷贝到堆外的过程
 通过组合ByteBuf类:即CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf, 而不需要进行数据拷贝
 通过各种包装方法, 将 byte[]、ByteBuffer等包装成一个ByteBuf对象,而不需要进行数据的拷贝
 通过slice方法, 将一个ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免了内存的拷贝,这在需要进行拆包操作时非常管用
 通过FileRegion包装的FileChannel.tranferTo方法进行文件传输时, 可以直接将文件缓冲区的数据发送到目标Channel, 减少了通过循环write方式导致的内存拷贝。但是这种方式是需要得到操作系统的零拷贝的支持的,如果netty所运行的操作系统不支持零拷贝的特性,则netty仍然无法做到零拷贝。
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池.
加入
Java原生NIO使用起码麻烦需要自己管理线程,Netty对JDK自带的NIO的api进行了封装,提供了更简单优雅的实现方式。由于netty5使用ForkJoinPool增加了复杂性,并且没有显示出明显的性能优势,所以netty5现在被废弃掉了。
Reactor模式:是事件驱动的,多个并发输入源。它有一个服务处理器,有多个请求处理器;这个服务处理器会同步的将输入的客户端请求事件多路复用的分发给相应的请求处理器。
优点:模型简单,实现方便
缺点:性能差:单线程无法发挥多核性能,
可靠性差:线程意外终止或死循环,则整个模块不可用
一个Reactor线程负责监听服务端的连接请求和接收客户端的TCP读写请求;NIO线程池负责消息的读取、解码、编码和发送
优点:可以充分的利用多核cpu的处理能
缺点:Reactor处理所有事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈.
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() { //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:
NioEventLoop
NioEventLoop 肩负着两种任务:
ServerBootstrap是一个工具类,用来配置netty
channel():提供一个ChannelFactory来创建channel,不同协议的连接有不同的 Channel 类型与之对应,常见的Channel类型:
ChannelHandler下主要是两个子接口
ChannelInboundHandler(入站): 处理输入数据和Channel状态类型改变。
ChannelOutboundHandler(出站): 处理输出数据
ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,一个贯穿 Netty 的链。每个新的通道Channel,Netty都会创建一个新的ChannelPipeline,并将器pipeline附加到channel中。DefaultChinnelPipeline它的Handel头部和尾部的Handel是固定的,我们所添加的Handel是添加在这个头和尾之前的Handel。
ChannelHandlerContext:ChannelPipeline并不是直接管理ChannelHandler,而是通过ChannelHandlerContext来间接管理。
网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码
Netty提供了一些默认的编码器:
StringEncoder:对字符串数据进行编码
ObjectEncoder:对 Java 对象进行编码
StringDecoder:对字符串数据进行解码
ObjectDecoder:对 Java 对象进行解码
抽象解码器
ReplayingDecoder: 继承ByteToMessageDecoder,不需要检查缓冲区是否有足够的字节,但是ReplayingDecoder速度略慢于ByteToMessageDecoder,同时不是所有的ByteBuf都支持。
UDP是基于帧的,包的首部有数据报文的长度.TCP是基于字节流,没有边界的。TCP的首部没有表示数据长度的字段。
发生TCP粘包或拆包的原因:
Netty 已经提供了编码器用于解决粘包。