Netty总结
Netty,异步事件驱动网络框架。
高效原因:
1、使用的是IO多路复用。
2、零拷贝,堆外直接内存。
3、强大网络API。
netty核心组成jdk的nio三剑客:Channel、Buffer、Selector.
服务器端Netty具体线程模型
1、定义引导类ServerBootstrap
2、ServerBootstrap 绑定两个事件循环组NioEventLoopGroup。
NioEventLoopGroup由一个或者多个NioEventLoop组成,一个循环组用于接收链接请求(bossGroup),一个循环组进行事件处理(workerGroup)。
3、每个NioEventLoop会绑定一个Selector,Selector进行监听就绪事件进行循环处理,每个请求会创建一个Channel,Channel注册到Selector上。
4、从3可以得出结论,每个Channel会绑定一个NioEventLoop,可以理解为一个请求绑定一个线程,因此每个连接都会按照顺序执行所有ChannelHandler,都会在同一个线程上执行。不会存在线程先后顺序问题。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。
Netty工作原理
1、server端工作原理
server端启动时绑定本地某个端口,将自己NioServerSocketChannel注册到某个boss NioEventLoop的selector上。
server端包含1个boss NioEventLoopGroup和1个worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。
每个boss NioEventLoop循环执行的任务包含3步:
- 轮询accept事件;
- 处理io任务,即accept事件,与client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个worker NioEventLoop的selector上;
- 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。
每个worker NioEventLoop循环执行的任务包含3步:
- 轮询read、write事件;
- 处理io任务,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理;
- 处理任务队列中的任务,runAllTasks。
2、client端工作原理
client端启动时connect到server,建立NioSocketChannel,并注册到某个NioEventLoop的selector上。
client端只包含1个NioEventLoopGroup,每个NioEventLoop循环执行的任务包含3步:
- 轮询connect、read、write事件;
- 处理io任务,即connect、read、write事件,在NioSocketChannel连接建立、可读、可写事件发生时进行处理;
- 处理非io任务,runAllTasks。
ByteBuf
ByteBuf是一个节点容器,里面数据包括三部分:
- 已经丢弃的数据,这部分数据是无效的
- 可读字节,这部分数据是ByteBuf的主体
- 可写字节
这三段数据被两个指针给划分出来,读指针、写指针。
ByteBuf 本质上就是,它引用了一段内存,这段内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对 ByteBuf 的读写,可以理解为是外观模式的一种使用
基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法
多个 ByteBuf 可以引用同一段内存,通过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则
ByteBuf和ByteBuffer的区别
- 可扩展到用户定义的buffer类型中
- 通过内置的复合buffer类型实现透明的零拷贝(zero-copy)
- 容量可以根据需要扩展
- 切换读写模式不需要调用ByteBuffer.flip()方法
- 读写采用不同的索引
- 支持方法链接调用
- 支持引用计数
NioEventLoop
NioEventLoop除了要处理IO事件,还有主要:
非IO操作的系统Task
- 定时任务
- 非IO操作和IO操作各占默认值50%,底层使用Selector(多路复用器)
Selector BUG出现的原因
若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,
Netty的解决办法
- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
Netty 自带的拆包器
1、固定长度的拆包器 FixedLengthFrameDecoder
如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。
2、行拆包器 LineBasedFrameDecoder
从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。
3、分隔符拆包器 DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。
4、基于长度域拆包器 LengthFieldBasedFrameDecoder
这种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。
Netty如何保证异步串行无锁?
NioEventLoop封装了一个线程,这个线程叫IO线程,专门处理客户端链接, 读写事件, 还有一个重要的事情也是由这个IO线程去做的, 是啥呢? 就是处理队列中的任务. 没错, 每个NioEventLoop都有一个队列, 这个队列是在创建NioEventLoop时被初始化。
源码继承关系:
SingleThreadEventExecutor中定义的IO线程
private volatile Thread thread;
@Override
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}
同样此类定义了任务队列:
Mpsc意思是多个生产者单个消费者Mp(producer)s(single)c(consumer)
其他的线程, 也就是多个生产者, 如果需要进行读写操作, 就把读写操作封装成任务放在这个队列中, 然后由这个NioEventLoop封装的IO线程去消费. 当然这个队列的设计是线程安全的。
线程执行队列任务先判断是否是封装的线程,如果是正常执行。
Netty是在哪里检测有新连接接入的?
答:Boss线程通过服务端Channel绑定的Selector轮询OP_ACCEPT事件,通过JDK底层Channel的accept()方法获取JDK底层SocketChannel创建新连接
新连接是怎样注册到NioEventLoop线程的?
答:Worker线程调用Chooser的next()方法选择获取NioEventLoop绑定到客户端Channel,使用doRegister()方法将新连接注册到NioEventLoop的Selector上面