Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序
Netty特性
设计
统一的API,适用于不同的协议(阻塞和非阻塞)
基于灵活、可扩展的事件驱动模型(SEDA)
高度可定制的线程模型
可靠的无连接数据Socket支持(UDP)
性能
更好的吞吐量,低延迟
更省资源
尽量减少不必要的内存拷贝
安全
- 完整的SSL/TLS和STARTTLS的支持
易用
完善的Javadoc,用户指南和样例
仅依赖于JDK1.6(netty 4.x)
1 Netty主要组件:
TransportChannel —— 对应NIO中的channel
EventLoop—— 对应于NIO中的while循环
EventLoopGroup: 多个EventLoop
ChannelHandler和ChannelPipeline—-对应于NIO中的客户逻辑实现handleRead/handleWrite(interceptor pattern)
ByteBuf—— 对应于NIO 中的ByteBuffer
Bootstrap和 ServerBootstrap —-对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等
2 Netty Server启动主要流程
设置服务端ServerBootStrap启动参数
group(parentGroup,childGroup):
channel(NioServerSocketChannel):设置通道类型
handler():设置NioServerSocketChannel的ChannelHandlerPipeline
childHandler():设置NioSocketChannel的ChannelHandlerPipeline
通过ServerBootStrap的bind方法启动服务端,bind方法会在parentGroup中注册NioServerScoketChannel,监听客户端的连接请求
- 会创建一个NioServerSocketChannel实例,并将其在parentGroup中进行注册
3 Netty Server执行主要流程:
Client发起连接CONNECT请求,parentGroup中的NioEventLoop不断轮循是否有新的客户端请求,如果有,ACCEPT事件触发
ACCEPT事件触发后,parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到childGroup中
childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理
4 组件
4.1 传输层
提供了统一的API,支持不同类型的传输层:
OIO -阻塞IO
NIO -JavaNIO
Epoll - Linux Epoll(JNI)
Local Transport - IntraVM调用
Embedded Transport - 供测试使用的嵌入传输
UDS- Unix套接字的本地传输
4.2 EventLoop
EventLoopGroup
包括多个EventLoop
多个EventLoop之间不交互
EventLoop:
每个EventLoop对应一个线程
所有连接(channel)都将注册到一个EventLoop,并且只注册到一个,整个生命周期中都不会变化
每个EventLoop管理着多个连接(channel)
EventLoop来处理连接(Channel)上的读写事件
ServerBootstrap
包括2个不同类型的EventLoopGroup:
ParentEventLoop:负责处理Accept事件,接收请求
ChildEventLoop:负责处理读写事件
4.3 Buffers
相比JDKByteBuffer, 更加易于使用:
为读/写分别维护单独的指针,不需要通过flip()进行读/写模式切换
容量自动伸缩(类似于ArrayList,StringBuilder)
FluentAPI (链式调用)
更好的性能:
通过内置的CompositeBuffer来减少数据拷贝(Zerocopy)
支持内存池,减少GC压力
主要操作
ByteBuf通过两个索引(readerindex、writer index)划分为三个区域:
reader index前面的数据是已经读过的数据,这些数据可以丢弃
从reader index开始,到writerindex之前的数据是可读数据
从writer index开始,为可写区域
顺序读/写:-改变reader/writerindex
writeByte()
writeLong()
writeXXX()- 增加write index
readByte()
readLong()
readXXX()- 增加read index
随机读/写:- 不改变read/write index
getXXX(index)
setXXX(index,byte)
mark/reset:
markReaderIndex()
markWriterIndex()
resetReaderIndex()
resetWriterIndex()
writerIndex(index)
readerIndex(index)
discardReadBytes方法 -compact
clear方法
查询方法:
indexOf
bytesBefore
forEachByte(ByteBufProcessor)
DerivedBuffers(衍生缓冲区):
duplicate()
slice()
slice(start,stop)
unmodifiableBuffer(…),
DerivedBuffers(衍生缓冲区)
具有各自的index和mark
返回的ByteBuf 与原ByteBuf 共享底层存储
缓冲区拷贝
copy()或 copy(int, int)
返回的ByteBuf 有数据的独立副本
Bytebuf 类型
根据内存的位置
HeapByteBuf
基于数组-内部为一个字节数组(byte array)
hasArray()返回True
array()返回其内部的数组,可以对数组进行直接操作
DirectByteBuf
堆外内存
具有更好的性能
创建和释放开销更大
根据是否使用内存池
Pooledvs Unpooled
根据是否使用Unsafe操作(Unsafe)
Safe vs Unsafe
CompositeByteBuf
•复合缓冲区(CompositeByteBuf)
多个ByteBuf组合的视图
一个ByteBuf列表,可动态的添加和删除其中的ByteBuf
可能既包含堆缓冲区,也包含直接缓冲区
ByteBufHolder
除ByteBuf外,还另外存储除有效的实际数据各种属性值
ByteBuf分配
不直接通过new来创建,而是通过ByteBufAllocator来创建
UnpooledByteBufAllocator
PooledByteBufAllocator
Unpooled工具类
它提供了静态的辅助方法来创建未池化的ByteBuf实例
ChannelHandler
业务处理核心逻辑,用户自定义
Netty提供2个重要的 ChannelHandler 子接口:
ChannelInboundHandler- 处理进站数据和所有状态更改事件
ChannelOutboundHandler- 处理出站数据,允许拦截各种操作
Channel状态及转换
当 ChannelHandler 添加到 ChannelPipeline,或者从ChannelPipeline 移除后,对应的方法将会被调用
ChannelInboundHandler
当接收到数据或者与之关联的Channel 状态改变时调用
与 Channel 的生命周期接近
ChannelOutboundHandler
ChannelPipeline
ChannelPipeline是ChannelHandler容器
包括一系列的ChannelHandler 实例,用于拦截流经一个Channel 的入站和出站事件
每个Channel都有一个其ChannelPipeline
可以修改 ChannelPipeline 通过动态添加和删除ChannelHandler
定义了丰富的API调用来回应入站和出站事件
ChannelHandlerContext
ChannelHandlerContext表示ChannelHandler 和ChannelPipeline 之间的关联
在ChannelHandler 添加到 ChannelPipeline 时创建
ChannelHandlerContext表示ChannelHandler 和ChannelPipeline 之间的关联
Reactor模式 - Douglas
Reactor模式 - Doug Lea
三种形式
单线程Reactor
多线程Reactor
Multiple Reactor
单线程
多线程
多reactor
其他模式 — 主从Reactor
EvenLoop
EventExecutor视图
EventExecutorGroup里面有一个EventExecutor数组,保存了多个EventExecutor;
EventExecutorGroup是不干什么事情的,当收到一个请后,他就调用next()获得一个它里面的EventExecutor,再调用这个executor的方法;
next(): EventExecutorChooser.next()定义选择EventExecutor的策略;
问题:
1、channelRead方法是多线程调用吗?
channelRead方法不会被多线程调用,只会在channel对应的Eventloop所在的线程所调用。
2、一个serversocketchannel可以注册到多个selector上吗? serversocket只能注册到一个端口,怎么做到eventLoopgroup的?
Serversocketchannel不能注册到多个Selector上去,只会注册在一个Selector上。(注册到多个Selector, accept()在哪个线程上返回了?)
Serversocketchannel绑定一个端口,并且注册到Boss EventloopGroup上去,他也只会使用BossEventloopGroup中的某一个Eventloop,也就是一个线程。
3、ByteBuf 中 array() 在什么场景下能取到值?
bossgroup 如何多线程工作完成accept?
当ByteBuf.hasArray()返回True时,array()就能够返回底层对应的数组。只有当ByteBuf是HeapByteBuf时才能够返回其底层array,为DirectByteBuf时则是无法取得。
Accept只会在Boss EventLoopGroup中的一个线程上运行,不会在多线程中同时accept。
4、那个衍生缓冲区和符合缓冲区有什么区别?
衍生缓冲区和复合缓冲区都是一个视图,自身并没有真正的底层存储空间。
衍生缓冲层是一个原缓冲区的视图,与原缓冲区共享底层存储空间,但是维护自己的索引(readerIndex和writerIndex)。
复合缓冲层则是多个缓冲区的一个整体视图。
5、是不是换行符和定长这些方案就能解决半包粘包问题对吧,然后真正项目也是这么用的对吧?
换行符(或基于其他分隔符)和定长是解决半包和粘包的一种方案。在有些项目中有用到。用得最广的应该还是基于长度的解码器,也是就协议中有一个字段指明数据的长度。
6、socket编程,如果是聊天室类型的,页面连接socket,除了websocket之外,还有其他长连接方式么?
网页的话目前主要是Websocket。
7、EventLoopGroup包含多个EventLoop,每个EventLoop包含一个selector,这个selector会管理多个channel,一个channel可以有多个客服端连接(一个Channel只表示一个服务器和客户端连接),
每一个channel都有一个ChannelPipeline,ChannelPipeline包含多个channelHandler实例列表,
每一个channelHandler对应多(一个)channelHandlerContentext,channelHandlerContentext只能对应一个channelHandler
8、 8.1、ChannelPipeline通过高级截取过滤器模式来掌控ChannelHandler处理事件(入站[inbound]和出站[outbound])的流程;
8.2、ChannelHandler并没有方法处理事件(嗯,只有一些通用的生命周期方法,handlerAdded/handerRemoved等),而需要由子类处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。
8.3、ChannelHandlerContext,上下文处理,指的是ChannleHandler之间的关系以及ChannelHandler与ChannelPipeline之间的关系,ChannelPipeline中的事件传播主要依赖于ChannelHandlerContext实现,
由于ChannelHandlerContext中有ChannelHandler之间的关系(不是ChannelHandlerContext有ChannelHandler的关系,是各个ChannelHandler的ChannelHandlerContext之间是一条双向链表),所以能得到ChannelHandler的后继节点,从而将事件传播到下一个ChannelHandler。
9、 流程:一个客户端 —》 EventLoop -》 selector -》 channel -》 ChannelPipeline(开始数据处理),
(1)一个channel可以有多个客服端连接,每一个channel都有一个ChannelPipeline,所有可以处理多个客户端连接后的数据处理,(一个Channel只表示一个服务器和客户端连接)
因为Netty提供了Sharable注解,如果一个ChannelHandle状态无关,那么就标注为Sharable,这样就可以实现一个实例处理所有客户端的事件 ;(如果一个ChannelHandle无状态,有Sharable注册,那在所有Channel的ChannelPipeline中就可以使用同一个实例,而不需要为每个channel创建一个单独的实例)
(2)入站[inbound]:处理服务端接受客户端过来的数据;出站[outbound]:处理服务端发送客户端的数据
这样说不准确。你是从服务器程序角度来说的。而对于客户端程序角度来说,则相反:入站处理客户端接受服务端过来的数据;出站则是处理客户端发送服务端的数据。
入站则主要处理从Channel进来的事件,像读;出站主要处理从channel出去的事件,像写,连接。
1、netty怎么处理客户端或服务端异常关闭后,缓冲区的数据未及时处理造成丢失的问题,BlockingQueue是否可以运用?
连接异常关闭后,缓冲区的数据未能及时处理就只能丢弃,或者存储起来,等目标用户重连后再重新发送(像微信一样,某个用户的连接断了,等他重新连过来时重发),但是这时知道目标用户(因为连接重建后是一个新的,不能通过连接来判断要重发的目标)
2、netty怎么做到同步调用,对于客户端可以在同一channel中实现同步返回,还是需要单独建立一个用于接收返回消息的channel.
这里的同步调用是什么意思? 因为channel本身是双向的,可读可写,服务端写数据,客户端就能够同步返回。
3、客户端消息异常重发,基于exceptionCaught异常事件处理,怎么得到异常的消息。
异常发生后,就会捕获异常信息,并调用ChannelHandler中的exceptionCaught,把异常信息传递给他。至于要实现异常重发,可以有exceptionCaught中加入重发逻辑。
1. selector内部是一个线程循环维护注册在其中的channel状态,还是操作系统回调selector,通知channel状态改变?
不是selector自己维护,也不是操作系统回调。Selector实际上是通过JNI调用到操作系统的select系统调用( linux下的Epoll,或freebsd下的kqueue 等 ),由操作系统内核负责监控channel的状态改变,并返回给selector。
2. NIO任然是阻塞的,是说阻塞的从内核态到用户态吗?对应JAVA中就是从channel中读取数据到buffer吗
对,当内核态数据准备就绪,NIO select就会返回,但是数据从内核态到用户态数据拷贝仍然是阻塞的,也就是 Java中channel读取数据到buffer中的过程。
3. 为什么从内核态复制到堆内存上需要先复制到堆外用户态内存,再复制一次到JVM?
弄反了。是当要通过网络发送数据时,需要把JVM 堆内存上的数据复制到内核态,此时无法直接复制,而是需要创建一个临时的native的内存,先把数据从JVM堆上复制到临时native内存上,再从临时native内存复制到内核态进行发送。
至于为什么要有这一次中间临时复制,是因为JVM堆内存是动态的,可能会发生GC, 而GC操作会涉及到数据的移动操作,移动数据使得其地址发生变化,会导致数据复制出错。所以JVM堆内存不能直接往内核态复制。
Java nio基于事件驱动的四个事件,能不能分别解释一下这四个事件的具体情况么,视频里面直接一笔带过了,我不太明白accept和connect的区别,一个是接收到连接的事件和建立连接的事件,一般后者用在什么场景呢? 另外read可读事件,指的是数据已经到用户态了么,还是说才到核心态,如果是到了核心态的话,为什么我不可以等到用户态再响应呢,这样更可以减少阻塞时间,如果是用户态的话,对应第一节课的io分类,那不就是异步io了,和老师说的nio使用的是io复用有些矛盾,还是说老师说的是io复用指的是数据到用户态的阻塞,以及数据处理的阻塞,这两个过程的复用呢?不知道我想的对不对,非常期待您的解惑。
OP_ACCEPT事件就绪:当收到一个客户端的连接请求时,该事件就绪。这是ServerSocketChannel上唯一有效的操作。(服务器端)
OP_CONNECT事件就绪:只有客户端SocketChannel会注册该操作,当客户端调用SocketChannel.connect()时,该事件就绪。(客户端)
OP_READ事件就绪:该操作对客户端和服务端的SocketChannel都有效,当OS的读缓冲区中有数据可读时,该事件就绪。
OP_WRITE事件就绪:该操作对客户端和服务端的SocketChannel都有效,当OS的写缓冲区中有空闲的空间时,该事件就绪。
accept和connect的区别:accept是服务器端接收到连接,connect则是客户端发起连接成功。后者用在客户端,当连接成功后注册相关读写事件。
对,read就指数据已经到内核缓冲区了,将其拷贝到应用缓冲区的过程。如果等到用户态再响应就是异步IO了。目前Linux异步IO接口还不成熟,因此使用较少。
IO复用更多的是指一个线程监控多个IO通道,这个监控线程的复用。
3、针对图中这里实现多线程reactor模式,这里多线程是如何配置的呢?
就是在netty程序的ChannelHandler中的channelRead()方法中,创建新的线程(池)来执行处理操作。
4、如果根据字节长度分割数据流,那么接收到的数据长度不一致怎么办呢?(多个不同类型的物联网设备)
在发送时通过LengthFieldPrepender添加数据长度前缀,然后通过LengthFieldBasedFrameDecoder拆包即可。