Netty知识整理
2023-01-11 22:43:08 0 举报
AI智能生成
整理netty相关的知识点,包括IO模型,粘包拆包,编解码,ByteBuf等
作者其他创作
大纲/内容
socket
本地IO和网络IO
四要素
源IP
源端口
目标IP
目标端口
1.Socket起源于Unix,而Unix/Linux基本思想之一就是“一切皆文件”,也称为文件描述符
2.既然一切都是文件,那么就可以把对Socket的操作就是对“open—write/read—close”模式的一种实现
3.Socket是对TCP/IP协议的封装,Socket本身不是协议,通过Socket才能使用TCP/IP协议
unix五种IO模型
同步阻塞IO模型
应用进程从发起 IO 系统调用,至内核返回成功标识,这整个期间是处于阻塞状态的。
同步非阻塞IO模型
应用进程可以将 Socket 设置为非阻塞,这样应用进程在发起 IO 系统调用后,会立刻返回。应用进程可以轮询的发起 IO 系统调用,直到内核返回成功标识。
IO复用模型
可以将多个应用进程的 Socket 注册到一个 Select(多路复用器)上,然后使用一个进程来监听该 Select(该操作会阻塞),Select 会监听所有注册进来的
Socket。只要有一个 Socket 的数据准备好,就会返回该Socket
Socket。只要有一个 Socket 的数据准备好,就会返回该Socket
信息驱动IO模型
可以为 Socket 开启信号驱动 IO 功能,应用进程需向内核注册一个信号处理程序,该操作并立即返回。当内核中有数据准备好,会发送一个信号给应用进程,应用进程便可以在信号处理程序中发起 IO 系统调用,来完成数据读取了。
异步非阻塞IO模型
应用进程发起 IO 系统调用后,会立即返回。当内核中数据完全准备后,并且也复制到了用户空间,会产生一个信号来通知应用进程。
java 四种IO模型
BIO
同步阻塞IO模型
ServerSocket serverSocket = new ServerSocket(8080);
使用ServerSocket创建一个socket,然后用循环一直阻塞接收到的消息
NIO
同步非阻塞IO模型
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketChannel socketChannel = serverSocketChannel.accept();,这里不会阻塞,在处理业务的时候还是会阻塞的
IO复用模型
selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
将多个channel注册到selecter上,selecter一直循环到有消息时,通知channel进行读写操作
select/poll模型
优点
在连接个数较小,连接的活跃度较高情况下,效率高于epoll
select的跨平台做的很好,几乎每个平台都支持;
缺点
单个进程能够监视的文件描述符的数量存在最大限制;
select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增长,其在用户态和内核的地址空间的复制所引发的开销也会线性增长;
由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()还是会对所有的socket进行一次线性扫描 ,会造成一定的开销;
epoll模型
优点
支持的FD上限为系统定义的进程打开的最大FD个数,与系统内存相关;
IO效率不随FD数目增加而线性下降(FD中有大量的空闲连接),它只会对"活跃"的socket进行操作(根据每个fd上面的callback函数实现)。
epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对,提高了效率。
缺点
在连接数量较小,或者连接活跃度都比较高的情况下,epoll的效率可能比不上select。
epoll只有Linux2.6以上才有实现 ,而其他平台都没有,跨平台性存在缺陷
AIO
异步非阻塞IO模型
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
在实现accept方法时,会调用使用实现CompletionHandler接口内的completed或者是failed,整个过程都不是阻塞的
Reactor模型和Proactor模型
Reactor模型
单Reactor单线程模型
首先客户端访问服务端,在服务端这边首先是使用Reactor监听accept事件和read事件,当有连接过来,就交给acceptor处理accept事件,当触发read事件,同时accept或把read事件交给handler处理
单Reactor多线程模型
首先客户端访问服务端,在服务端这边首先是使用Reactor监听accept事件和read事件,当有连接过来,就交给acceptor处理accept事件,当触发read事件,同时accept或把read事件交给线程池中的handler处理
主从Reactor多线程模型
首先客户端访问服务端,在服务端这边首先是使用Reactor监听accept事件和read事件,当有连接过来,就交给acceptor处理accept事件,当触发read事件,交给多个subReactor处理,选中自己selector对应的handler处理read事件
Proactor模型
启动初始化
Proactorinitiator创建Proactor
Proactorinitiator创建handler
Proactor通过内核提供的Asynchronous Operation processor把Proactor和handler注册到内核
Proactorinitiator创建handler
Proactor通过内核提供的Asynchronous Operation processor把Proactor和handler注册到内核
客户端调用
当IO操作完成是内核通知Proactor
Proactor根据不同的IO事件回调不同的handler完成业务处理
两者之间的区别
Reactor 是在事件发生时就通知事先注册的事件(读写在应用程序线程中处理完成);
Proactor 是在事件发生时基于异步 I/O 完成读写操作(由内核完成),待 I/O 操作完成后才回调应用程序的处理器来进行业务处理;
Reactor 模式是基于同步非阻塞 I/O 的;
Proactor 模式基于异步非阻塞 I/O
byteBuf
对比java的byteBuffer,做了优化
读指针readerIndex
当创建时,为0,读取一段数据后,读指针后移
写指针writerIndex
当创建时为0,写数据时,写多少后移多少
容量 capacity
创建的容量
废弃字节
已读的字节,可以通过resetReaderInder()重置读指针
可读字节
读指针到写指针的范围
可写字节
写指针到容量的范围
可扩容字节
容量到数字最大值范围
扩容规则
当容量小于4M时,每次扩列容量翻倍
当容量大于等于4M时,每次扩容添加4M
扩大到2^64为止
netty编码解码
粘包拆包
粘包
数据从客户端传输到服务端,可能会把多个小的数据包封装成一个大包发送
拆包
数据从客户端传输到服务,可能会把一个大的数据包拆成多个小数据包发送
编码
将消息对象转换成字节或其他序列形式在网络上传输,有两个抽象类可以继承
MessageToByteEncoder:将消息编码为字节
LengthFieldPrepender
对应解码器中的LengthFieldBasedFrameDecoder
解码
继承抽象类ByteToMessageDecoder,作用是通过ByteBuf去传递数据,我们把这种把ByteBuf(原始数据流) -> ByteBuf(用户数据) 称之为一次解码器
netty提供四种的解码器
FixedLengthFrameDecoder
固定长度解码器
LineBasedFrameDecoder
发送信息加上换行符\n
DelimiterBasedFrameDecoder
自定义分隔符解码器
LengthFieldBasedFrameDecoder
byteOrder
采用大端序列还是小端
lengthFieldLength
表示报文的长度字段有4个字节表示,这个只能是1,2,3,4,8
lengthAdjustment
需要调整的长度数,最后和长度的值相加为你要得到的值的长度
lengthIncludesLengthFieldLength
为true时,length字段的值=length字段的长度+Content的长度。为false时,length字段的值=Content的长度
二次编码解码
将数据帧 转为正确的byte数组我们称之为一次解码,将byte数据流转换为java对象,称之为二次解码,
那么在netty中的二次解码器都要去继承MessageToMessageDecoder类
那么在netty中的二次解码器都要去继承MessageToMessageDecoder类
序列化和反序列化
使用作用
一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。
需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。这是针对网络传输来说的,因为我们讲的是Netty,一个网络通信的框架,所以序列化也是针对网络传输来说的
java序列化
序列化的对象必须实现Serializable接口
尽量定义serialVersionUID,如果没有定义,系统在编码的时候会自动附一个值,对于应用是不可见的。数据修改后,可能这个值不同导致序列化失败
xml序列化
将对象序列化成xml格式,由此可以看出,这类序列化消息体比较大,不利用高并发的网络应用
json序列化
将数据序列化成json对象
hassian序列化
dubbo默认就是使用Hassian序列化
使用HessianInput和HessianOutput调用
avro序列化
可以使用数据库的结构使用
protobuf序列化
谷歌提供的一个序列化方案,它是一个灵活高效的用于序列化数据的协议,相比较XML和JSON格式,protobuf更小、更快、更便捷
protobuf中正数使用varint算法,负数使用zigzag算法
优点
速度快,编码解码方式简单,采用ProtocolBuffer自身的框架代码和编译器共同完成
序列化的数据量体积小:独特的编码方式,如Varint,Zigzag算法,采用T-L-V的数据存储方式
选型建议
对性能要求不高的场景,可以采用基于XML的soap协议
对性能和间接性有比较高要求的场景,那么Hessian,Protobuf序列化
基于前后端分离,或者独立的对外的api服务,选用json是比较好的,对于调试,可读性都很不错
基于netty实现RPC的原理
1,各个子系统在不同的服务器上,通过注解实现远程依赖注入
2,通过注解将远程的服务中的接口通过网络传输得到服务类
3,使用序列化和反序列化将传输内容得到原始数据流
4,使用编码解码得到原始数据
5,通过反射得到具体的方法
6,利用spring的启动流程,将反射出来的bean注入到spring容器
7,接口调用的时候,即可远程方法的调用过程
netty源码
对比的NIO流程
1,得到一个selector
2,得到一个serverSocketChannel
3,设置channel为非阻塞
4,关联selector和channel的关系,并没有关注事件
5,绑定端口
6,给channel注册accept事件
7,客户端连接过来之后,通过selector获取channel
8,通过accept获取socketchannel
9,给selector上监听op_read事件
netty整体实现是通过NIO实现的,所以,整体的原理同上流程,封装了一层,对外提供简明的api
main线程
初始化Boss和work,其实无论是boss还是work都是NioEventLoopGroup,每个NioEventLoopGroup里
面都有一个EventExecutor的数组,其实这个数组里面就是存放的NioEventLoop
面都有一个EventExecutor的数组,其实这个数组里面就是存放的NioEventLoop
在每个NioEventLoop中都会有一个executor的执行器,因为NioEventLoop其实本身不是线程,只是里
面有一个线程的变量,而这个执行器会创建一个线程,然后去执行任务
面有一个线程的变量,而这个执行器会创建一个线程,然后去执行任务
我们会生成多个boss和work的NioEventLoop对象,所以我们处理选择那个work和boss就需要使
用选择器选择一个来处理任务,当然,这里boss其实只会使用到一个
用选择器选择一个来处理任务,当然,这里boss其实只会使用到一个
创建的NioEventLoop这个对象,这个对象的构造函数中创建了很多的东西,比如多路复
用器,也对应到我们之前说的每一个线程都有一个多路复用器,任务队列,任务都会放在这个队列中,
然后run方法就是不停的从这个队列中获取任务执行
用器,也对应到我们之前说的每一个线程都有一个多路复用器,任务队列,任务都会放在这个队列中,
然后run方法就是不停的从这个队列中获取任务执行
线程,就是刚才说的执行器会创建一个线程,并且会赋值给这个成员变量,刚初始化的时候是null
保存执行器,每一个NioEventLoop都有一个执行器
ServerBoostStrap中配置了很多信息,比如boss和work,这些都要放在指定的位置的,比如Boss
的所有配置信息都会放入到AbstractBootstrap对象中,Work的所有配置信息就会放在
ServerBootstrap中
的所有配置信息都会放入到AbstractBootstrap对象中,Work的所有配置信息就会放在
ServerBootstrap中
总结
实例化boss和work的NioEventLoop,每个NioEventLoop都有一个selector
实例化NioServerSocketChannel实例,其实就是实例化ServerSocketChannel和设置channel为非阻塞,同时绑定一个pipeline
NioEventLopGroup
boss线程
注册空事件
绑定了端口和ip
最终注册了ACCEPT事件
selector.select()方法阻塞线程
客户端连接过来,获取到的SocketChannel封装成NIOSocketChannel
选择一个Work线程去处理,这个是由ServerBootstrapAcceptor类中的channelRead方法处理的
work线程
注册Read事件
调用pipeline的handler上的channelRead方法,获取客户端的数据
通过ctx.writeAndFlush方法给客户端回写数据。
pipeLine原理
创建NioServerSocketChannel对象的时候,就创建了一个Pipeline,在获取到客户端的连接之后,
把SocketChannel封装成NioSocketChannel,在它的构造函数中也创建一个Pipeline
把SocketChannel封装成NioSocketChannel,在它的构造函数中也创建一个Pipeline
handle的生命周期
1.channelRegistered: channel被注册到EventLoop时调用; register0>pipeline.fireChannelRegistered()
2.channelActive:channel以激活; register0>pipeline.fireChannelActive()
3.channelRead:从当前channel的对端读取消息; processSelectedKey()>unsafe.read()>pipeline.fireChannelRead(byteBuf)
4.channelReadComplete:消息读取完执行;processSelectedKey()>unsafe.read()>pipeline.fireChannelReadComplete(byteBuf)
5.exceptionCaught:处理异常;从unsafe.read()异常>handleReadException>pipeline.fireExceptionCaught(cause)
6.channelInactive:channel结束生命周期;从unsafe.read()异常>handleReadException>pipeline.fireChannelInactive()
7.channelUnregistered:把channel从EventLoop中注销;pipeline.fireChannelInactive()>pipeline.fireChannelUnregistered()
8.userEventTriggered:一个用户事件被触发;再AllIdleTimeoutTask类中的run方法中>channelIdle(ctx, event)>handler.userEventTriggered()
9.channelWritabilityChanged:如果写入ctx.writeAndFlush写入的数据大于outBuffer的最高水位了,那么可以触发该方法。从ctx.writeAndFlush>outboundBuffer.addMessage(msg, size, promise)>setUnwritable(invokeLater)>pipeline.fireChannelWritabilityChanged()
数据结构
双向的链表数据结构,每个Pipeline初始化出来的时候都会有一个HeadContext和TailContext节点,
每一个节点都是AbstractChannelHandlerContext,HeadContext既属于inbound,也属于
outBound,TailContext只属于inbound
每一个节点都是AbstractChannelHandlerContext,HeadContext既属于inbound,也属于
outBound,TailContext只属于inbound
对NIO的优化
对空轮训的死循环操作,使用的了512次的检查,如果出现会清除码,重新分配
selector结构优化,对原有的hashset数据结构转换,转换成数组,查询效率增加
线程优化
FastThreadLocalThread
线程优化
FastThreadLocal
内存泄露处理
netty和其他框架的对比
对比NIO
编写代码更加简明,bug出现少
解决了粘包拆包,序列化等api
性能优化,ByteBuffer优化成ByteBuf,ThreadLocal优化成了FastThreadLocal
对比mina
mina是apach维护,而且3.x会有很大的重构,api不兼容
netty的开发迭代迅速,api更加简洁优秀
mina将内核和一些特性的联系过于紧密,使得用户在不需要这些特性的时候无法脱离,相比之下性能会有所下降
netty比mina使用更加简单快速
0 条评论
下一页