Netty
四 Netty概述
- 原生NIO存在的问题
- NIO的类库和API十分的繁杂,使用麻烦。需要掌握Selector、ServerSocketChannel、ByteBuffer等使用
- 同时还要熟悉Java多线程和网络编程,因为NIO涉及到Reactor模式,因此需要对多线程和网络编程非常熟悉才能编写出高质量的NIO程序
- 开发工作量和难度都很大:如短线重连、半包读写、失败缓存、异常流的处理等
- JDK NIO存在Epoll Bug,会使得Selector空轮询,最终导致CPU过载。直到JDK7也没有根本解决
五 Netty高性能架构设计
-
线程模型基本介绍
- 目前存在的线程模型有:传统阻塞IO服务模型、Reactor模式
- 根据Reactor的数量和处理资源的线程数量不同,有三种典型实现:单Reactor单线程、单Reactor多线程、主从Reactor多线程
- Netty主要基于主从Reactor模型进行了一定的改进,形成了Netty的线程模式
-
传统阻塞IO服务模型
采用阻塞IO模式获取输入的数据,每个连接都需要独立的线程完成数据的处理。当并发数很大时,会创建大量的线程,导致资源浪费;而且当某个连接没有数据可用时,该线程就会在read处阻塞,造成资源浪费 -
Reactor模式(反应器模式、分发者模式、通知者模式)
多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象内等待,无需阻塞等待所有的连接。
不必为每个连接都创建线程,将连接完成后的任务分配给线程池处理,一个线程池可以处理多个连接的业务
-
Reactor模式说明
- 核心组成有:
- Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发到适当的处理程序来对IO事件做出反应
- Handlers:处理程序执行IO事件要完成的实际事件。通过调度适当的Handler来响应IO事件
- 模式分类
- 单Reactor单线程 :服务器端用一个线程通过多路复用搞定所有IO操作,但是如果客户端连接数量较多时则无法支撑
- 单Reactor多线程:使用Reactor主线程进行事件判断和分发,使用Worker线程池进行多个线程的业务处理
- 主从Reactor多线程:单Reactor多线程模型中,Reactor在高并发场景容易出现性能瓶颈,因此可以让Reactor在多线程中运行。使用Reactor主线程来处理客户端的连接请求,随后将客户端连接后的其他请求交由子Reactor子线程来处理分发。分发到Worker线程池由多线程进行业务处理
- 核心组成有:
-
Netty模型
Netty主要基于主从Reactors多线程模型做了一定的改进。- Netty抽象出两组线程池。BossGroup专门负责接受客户端的连接,WorkerGroup专门负责网络的读写。他们都是NioEventLoopGroup
- NioEventLoopGroup相当于一个事件循环组,组中包含多个事件循环(NioEventLoop线程)
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个Loop都有一个Selector,用于监听绑定在其上的socket网络通讯
- BossLoop循环执行:轮询accept事件;处理accept事件并连接,生成NioSocketChannel绑定到WorkerGroup的某个线程的selector上;处理任务队列的任务
- WorkerLoop循环执行:轮询读写事件;处理IO事件,在对应的NioSocketChannel中处理;处理任务队列的任务
- 每个WorkerLoop在处理业务时会使用pipeline。pipeline中包含了channel,即通过pipeline可以获取到对应通道,pipeline中维护了很多的处理器handler
- Netty模型的再说明
- NioEventLoopGroup下包含多个NioEventLoop
- 每个Loop包含有一个Selector,一个taskQueue
- 每个Channel会绑定在唯一的NioEventLoop上
- 每个NioChannel都绑定有自己的一个ChannelPipeline
-
异步模型
基本介绍- 异步的概念和同步相对。当一个异步过程调用发生时,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
- Netty中的IO操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture
- 调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果
- Netty的异步模型是建立在future和callback之上的。callback即回调操作;Future的核心思想是:假设一个方法fun过程十分耗时,等待其返回显然不合适,可以在调用fun的时候,立刻返回一个Future,后续可以通过Future去监听方法fun的处理过程(即Future-Listener机制)
Future说明
- 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等
- ChannelFuture是一个接口,可以添加监听器,当监听的事件发生时,就会通知到监听器
Future-Listener机制
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作- 通过isDone方法判断当前操作是否完成
- 通过isSuccess方法来判断已完成的当前操作是否成功
- 通过getCause方法来获取已完成的当前操作失败的原因
- 通过isCancelled方法来判断已完成的当前操作是否被取消
- 通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知指定的监听器
ChannelFuture cf = bootstrap.bind(6688).sync();
cf.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(ChannelFuture future) throws Exception{
if(future.isSuccess()){
System.out.println("监听绑定6688端口成功");
}else {
System.out.println("监听绑定6688端口失败");
}
}
});
六 Netty核心组件
- Bootstrap、ServerBootstrap
- 一个Netty应用通常由一个Bootstrap开始,主要作用是,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
- 常用的方法如下
//用于服务器端,用来设置两个EventLoop
public ServerBootstrap group(EventLoopGroup parentGroup , EventLoopGroup childGroup);
//用于客户端,用来设置一个EventLoop
public B group(EventLoopGroup group);
//用来给服务端设置一个通道实现
public B channel(Class<? extends C> channelClass)
//用来给ServerChannel添加配置
public <T> B option(ChannelOption<T> option , T value);
//用来给接收到的通道添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> option , T value);
//用来设置业务处理类(自定义的handler或者编码解码处理器)
public ServerBootstrap childHandler(ChannelHandler handler);
//用于服务器端,设置占用的端口号
public ChannelFuture bind(int inetPort);
//用于客户端,用来连接服务器端
public ChannelFuture connect(String inetHost,int inetPort);
-
Future 、ChannelFuture
Netty中所有的IO操作都是异步的(bind、connect、读写操作等),不能立刻得知消息是否被正确处理。但是可以过一段时间等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,可以在Future中注册监听,当操作执行成功或者失败时会自动触发注册的监听事件。
常见方法为:Channel channel();
,返回当前正在进行IO操作的通道ChannelFuture sync();
,等待异步操作执行完毕,返回Future对象用作监听执行的步骤
-
Channel
- Netty网络通信的组件,能够用于执行网络IO操作
- 通过Channel可获得当前网络连接的通道的状态
- 通过Channel可获得网络连接的配置参数(例如接收缓冲区大小)
- Channel提供异步的网络IO操作(如建立连接、读写、绑定端口等),异步调用意味着任何IO调用都将立刻返回,并且不保证在调用结束时所请求的IO操作已完成
- 调用立刻返回一个ChannelFuture实例,可以通过注册监听器addListener到uture对象上,来在IO操作成功、失败或者取消时回调通知调用方
- 支持关联IO操作与对应的处理程序(即Channel关联pipeline、pipeline中维护ChannelHandlerContext链)
- 不同协议、不同的阻塞类型的连接有不同的Channel与之对应,如
- NioServerSocketChannel、NioSocketChannel:异步的TCP Socket连接
- NioDatagramChannel:异步的UDP连接
- NioSctpChannel、NioServerSctpChannel:异步的Sctp连接
-
Selector
- Netty基于Selector对象实现IO多路复用,通过Selecto一个线程可以监听多个连接的Channel事件
- 当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询这些注册的Channel是否有已就绪的IO事件,这样程序就可以很简单的使用一个线程高效的管理多个Channel
- ChannelHandler及其实现类
- ChannelHandler是一个接口,处理IO事件或者拦截IO操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序
- ChannelHandler本身并没有提供很多方法,因为这个接口有许多方法需要实现
- 一般来说,自定义Handler类是集成ChannelHandlerAdapter实现的,然后通过重写类中方法完成业务处理
public void channelRegistered(ChannelHandlerContext ctx);
public void channelUnregistered(ChannelHandlerContext ctx);
public void channelActive(ChannelHandlerContext ctx);
public void channelInactive(ChannelHandlerContext ctx);
public void channelRead(ChannelHandlerContext ctx);
public void channelReadComplete(ChannelHandlerContext ctx);
public void exceptionCaught(ChannelHandlerContext ctx);
-
Pipeline 和 ChannelPipeline
- ChannelPipeline是一个Handler的集合,他负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链
- ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何交互
- 在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应
- 一个Channel中包含了一个pipeline,而一个pipeLine中维护着一个由ChannelHandlerContext组成的双向链表,每个ChannelHandlerContext中关联着一个ChannelHandler
- 入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler;出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰
- 常用方法有:
ChannelPipeline addFirst(ChannelHandler... handlers)
,将一个handler添加到链中的第一个位置ChannelPipeline addLast(ChannelHandler... handlers)
,将一个handler添加到链中的最后一个位置
-
ChannelHandlerContext
用于保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象。即该对象中包含了一个具体的事件处理器Handler,同时也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用。
ChannelFuture close();//关闭通道
ChannelOutboundInvoker flush();//刷新
ChannelFuture writeAndFlush(Object msg);//将数据写到ChannelPipeline中的ChannelHandler的下一个ChannelHandler开始处理(出站)
-
ChannelOption
- Netty在创建Channel实例后,一般都需要设置ChannelOption参数
- ChannelOption参数如下:
- ChannelOption.SO_BACKLOG:用来初始化服务器可连接队列的大小
- ChannelOption.SO_KEEPALIVE:保证该Channel一直保持连接活动状态
-
EventLoopGroup和其实现类NioEventLoopGroup
- EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多核EventLoop同时工作,每个EventLoop维护着一个Selector实例
- EventLoopGroup 提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务 。在Netty服务器端编程中一般提供两个EventLoopGroup。
- 通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossLoop负责接收客户端连接,并将获取到的SocketChannel交给WorkerGroup处理
- 常用方法为
- NioEventLoopGroup(); 构造方法
- shudownGracefully(); 断开连接关闭线程
-
Unpooled类
- 是Netty提供的专门用来操作缓冲区的工具类
- 使用UnPooled获取到的ByteBuf和NIO中的ByteBuffer类似,但有一些区别。同时ByteBuf中包含readerIndex、writerIndex和capacity属性。在使用write()和read()方法时会对前两个属性产生影响。
- 常用方法如下
public static ByteBuf copiedBuffer(CharSequence String , Charset charset);
- Netty心跳机制和心跳处理器
Netty的心跳监测机制用于检测连接是否有效,一般在客户端指定时间内没有进行读写操作时使用心跳检测机制进行检测。在服务器端给管道添加心跳处理器以完成心跳检测。
public class MyServer{
public static void main(String args[]){
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();//根据处理器核数进行创建
try{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChanel ch) throws Exception{
Pipeline pipepine = ch.pipeline();
//加入一个netty提供的IdleStateHandler
/*
说明
1. IdleStateHandler是netty提供的处理空闲状态的处理器
2. long readerIdleTime ,参数表示客户端对应的Channel多长时间没有读事件,就会发送一个心跳检测包
3. long writerIdleTime ,参数表示客户端对应的Channel多长时间没有写事件,就会发送一个心跳检测包
4. long allIdleTime ,参数表示客户端对应的Channel多长时间没有读写事件,就会发送一个心跳检测包
5. 文档说明为:
triggers an {@link IdleStateEvent} when a {@link Channel} has not performed
read, write, or both operation for a while.
6. 当IdleStateEvent触发后,会传递给pipeline的下一个handler处理器进行处理
通过调用(触发)下一个handler的userEventTriggered方法,在该方法中去处理IdleStateEvent(读空闲、写空闲、度写空闲)
*/
pipeline.addLast(new IdleStateHnadler(7000,7000,10,TimeUnit.SECONDS));
//加入一个自定义的对空闲检测进一步处理的handler,在该handler中的userEventTriggered方法中处理
pipeline.addLast(new MyServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}catch(Exception e){
e.printStackTrace();
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
class MyServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void userEventTriggered(ChannelHandlerContext ctx , Object evt) throws Exception{
if(evt instanceof IdleStateEvent){
//将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent)evt;
String eventType = null;
//event.state()方法用于获取事件类型
switch(event.state()){
case READER_IDLE:
event="读空闲";
break;
case WRITER_IDLE:
event="写空闲";
break;
case ALL_IDLE:
event="读写空闲";
break;
System.out.println(ctx.channel().remoteAddress() + "发生了" + eventType );
System.out.println("服务器做相应处理....");
//如果发生空闲则可以对通道进行关闭处理
//ctx.channel().close();
}
}
}