Netty—核心组件的深度解析
文章目录
Netty入站和出站机制
基本思想:
- 入站操作主要是指读取数据的操作;而出站操作主要是指写入数据的操作
- 入站会从先读取,再执行入站的Handler;
- 出站会先执行出站的Handler,再写入
其中Decoder是ChannelInboundHandlerAdapter(入站),Encoder是ChannelOutboundHandlerAdapter(出站)
注意:
- 无论是解码还是编码处理器,接收的消息类型必须和待处理的消息类型一致,否则该处理器不会被执行
- 在解码器进行数据解码时,需要判断缓存区(byteBuf)的数据是否足够,否者接收到的结果和期望结果可能不一致
常用的编解码器:
-
解码器:
-
ReplayingDecoder
,拓展了ByteToMessageDecoder
,使用这个类,我们不必调用readableBytes方法来判断缓冲区大小-
//参数S指定了用户状态管理的类型,使用Void代表不需要状态管理 public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
-
并不是所有的bytrBuf操作都支持,如果调用了一个不支持的方法,会抛出UnsupportOperationException。
-
ReplayingDecoder
在某些情况下可能会稍慢于ByteToMessageDecoder
,例如网络缓慢并且消息格式复杂时,消息会拆分为多个碎片,速度变慢。
-
-
LineBasedFrameDecoder
,使用行尾控制符(\n或者\r\n)作为分隔符来解析数据 -
DelimiterBasedFrameDecoder
,使用自定义的特殊字符作为消息的分隔符 -
HttpObjectDecoder
,一个HTTP数据的解码器 -
LengthFieldBasedFrameDecoder
,通过指定长度来标识整包消息,这样就可以自动处理黏包和半包消息。
-
TCP粘包和拆包基本介绍
- TCP是面向连接的,面向流的,提供高性能可靠服务服务。收发两端(C/S)都要有一一成对的socket。因此,发送端为了将多个发给接受端的包更有效的发给对方,使用优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合成一个大的数据块,然后进行封包。这样虽然提高了效率,但是接受端就难于分辨出完整的数据包了,因为面向流的通信是没有消息保护边界的
- 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们说的粘包,拆包问题
解决方案
- 使用自定义协议+编解码器来解决
- 关键就是要解决服务器每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免TCP粘包,拆包
- 就是定义一个自定义一个协议(对象),每次传输消息时,先将消息长度(int)传输过去,在传输消息(object)------->编码(encoder)。接受端接收时,先接受到消息长度(int),然后按照消息长度接收相应长度的消息(object)------>解码(decoder)。
Netty接收请求过程
**总体流程:**接受请求—>创建一个新的NioSocketChannel---->注册到一个workEventLoop上----->注册selector read事件
- 服务器轮询Accept事件,获取事件后调用unsafe的read方法,这个unsafe时serverSocket的内部类,该方法内部由2部分组成
- doReadMessage用于创建NioSocketChannel对象,该对象包装JDK的NioChannel客户端。该方法会像创建ServerSocketChannel类似创建相关的pipeline,unsafe,config
- 随后执行pipeline.fireChannelRead方法,并将自己绑定到一个choose选择器选择的workGroup中的一个EventLoop上,并返回一个0,表示注册成功。
Pipeline调用handler
-
context包装handler,多个context在pipeline中形成双向链表,入站方向叫inbound,由head节点开始;出站方向叫outbound,由tail开始;
-
节点中间的传递通过AbstractChannelHandlerContext类内部的fire系列完成,找到当前节点的下一个节点不断循环传播。是一个过滤器形式,完成对handler 的调度。
-
过程
-
pipeline首先会调用Context的静态方法fireXXX,并传入context
-
然后静态方法调用context的invoker方法,而invoker方法内部会调用该context所包含的handler的真正XX方法,调用结束后,如果还需要向后传递,就调用context的firexxx2方法,循环往复。
-
Netty心跳服务
说明:
netty提供了IdleStateHandler,ReadTimeoutHandler,WriteTimeoutHandler三个handler检测连接的有效性
ReadTimeoutHandler和WriteTimeoutHandler会自动关闭连接,属于异常处理。重点是IdleStateHandler
基本属性:
-
private final boolean observeOutput; //是否考虑出站慢的情况,默认为false private final long readerIdleTimeNanos; //读事件的空闲时间,0则禁用事件 private final long writerIdleTimeNanos; //写事件的空闲时间,0则禁用事件 private final long allIdleTimeNanos; //读写事件的空闲时间,0则禁用事件
方法:
-
public void handlerAdded(ChannelHandlerContext ctx) //当handler被添加到pipeline中时,则调用initialize方法
-
initialize():
ReaderIdleTimeoutTask的run方法:
小结:
- IdleStateHandler可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会触发用户的handler的userEventTriggered方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关闭连接。
- IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个值,在定时任务运行的时候,通过计算当前时间和上次时间发生的时间的结果,来判断是否空闲。
- 内部有三个定时任务,分别对应读事件,写事件,读写事件,通常用户监听读写事件就足够了。
- 同时IdleStateHandler内部也考虑了一些极端情况,客户端接收缓慢,一次读写数据的速度超过了设置的空闲时间。netty通过构造方法中的observeOutput属性来决定是否对出站缓冲区的情况进行判断。
- 如果出站缓慢,netty不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。因为第一次判断无法判断是出站缓慢还是空闲。当然出站缓慢的话,可能会造成OOM,OOM比空闲时间问题大。
- 所以当应用出现内存溢出,oom之类,并且写空闲极少发生,那就需要注意是不是数据出站速度过慢。
- 还有一个问题就是ReadTimeoutHandler,它继承于IdleStateHandler,当触发读空闲事件的时候,就会触发 ctx.fireExceptionCaugh方法,并传入一个ReadTimeoutException,然后关闭socket。
- 而WriteTimeoutHandler的是实现不是基于IdleStateHandler的,它的原理是,当调用write方法时,会创建一个定时任务,任务内容是根据传入的promise的完成情况来判断是否超过了写的时间。当定时任务根据指定时间开始运行,发现promise的isDone返回false,表明还没有写完,说明超时了,则抛出异常。当write方法完成后,会打断定时任务
NioEventLoop剖析
说明:
- ScheduleExecutorService接口表示一个定时任务接口,EventLoop可以接受定时任务
- EventLoop接口,一旦channel注册,就注册该channel对应的所有IO操作。
- SingleThreadEventExecutor表示这是一个单个线程的线程池
- EventLoop是一个单例的线程池,里面含有一个死循环的线程,不断做着三件事情,监听端口,处理端口事件,处理队列事件。每个EventLoop都可以绑定多个channel,而每个channel始终只能由一个EventLoop来处理。
小结:
- 每次执行execute方法都是向队列中添加任务。当第一次添加时就启动线程,执行run方法,而run方法是整个eventLoop的核心,不停的loop,做三件事情
- 调用selector的selector方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上加上0.5秒进行阻塞。当执行execute方法时,也就是添加任务的时候,唤醒selector,防止selector阻塞时间过长。
- 当selector返回的时候,回调用processSelectedKeys方法对selectKey进行处理
- 当processSelectedKeys方法执行结束之后,则按照ioRatio的比例执行runAllTask方法,默认是IO任务时间和非IO任务时间是相同的,
- 你也可以根据应用特点进行调优:
- NioEventLoop.setIoRatio:设置事件循环中I/O所需时间的百分比。取值范围为1-100。默认值为50,这意味着事件循环将尝试花费与非I/O任务相同的时间用于I/O。该数字越小,可用于非I/O任务的时间就越多。如果值设置为100,则此功能将被禁用,事件循环将不会尝试平衡I/O和非I/O任务。
任务加入异步线程池
目的:
-
在netty中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响netty对socket的处理速度。
-
解决方法就是将任务加入到异步线程池中
-
handler中加入线程池
-
//定义一个业务线程池 static final DefaultEventExecutorGroup GROUP=new DefaultEventExecutorGroup(6); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { GROUP.submit(new Callable<Object>() { @Override public Object call() throws Exception { System.out.println("这里处理耗时的事务"); return null; } }); }
-
AbstractChannelHandlerContext.write()
-
-
-
context中添加线程
-
public class NettyServer { //在这里创建线程池 public static final DefaultEventExecutorGroup GROUP=new DefaultEventExecutorGroup(3); public static void main(String[] args) throws InterruptedException { EventLoopGroup boosGroup = new NioEventLoopGroup(1); EventLoopGroup workGroup = new NioEventLoopGroup(1); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boosGroup,workGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,128) .childOption(ChannelOption.SO_KEEPALIVE,true) .childHandler(new ChannelInitializer<SocketChannel>(){ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(GROUP,new NettyServerHandler()); } }); ChannelFuture channelFuture = bootstrap.bind(8888).sync(); }catch (Exception e){ e.printStackTrace(); } } }
-
-
-
小结:
- 第一种方法handler中添加异步,可能更加的自由,比如如果需要访问数据库,可以异步执行,如果不需要就不异步执行,异步会拖动接口响应时间。因为需要将任务放到mpscTask中。如果IO时间很短,task很多,可能一个循环下来都没有时间整个task,导致响应时间达不到指标
- 第二种方式是netty的标准方式,但是这么做整个netty都交给了业务线程,不论耗不耗时,都要加入队列里,不够灵活
- 各有优劣,从灵活度考虑,第一种较好。