Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor模型。
Netty模型简单版
Netty用于接收客户端请求的线程池(bossgroup)职责如下。
(1)接收客户端TCP连接,初始化Channel参数
(2)将链路状态变更事件通知给ChannelPipeline
Netty处理IO操作的Reactor线程池(workersgroup)职责如下。
(1)异步读取通信对端的数据报,发送读事件到ChannelPipeline:
(2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口:
(3)执行系统调用Task:
(4)执行定时任务Task,例如链路空闲状态监测定时任务
通过调整线程池的线程个数、是否共享线程池等方式,Netty的Reactor线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最大程度地满足不同用户的个性化定制。
Netty模型进阶版
-
Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
-
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
-
NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
-
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
-
NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
-
每个Boss NioEventLoop 循环执行的步骤有3步 轮询accept 事件 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector 处理任务队列的任务 , 即 runAllTasks
-
每个 Worker NIOEventLoop 循环执行的步骤 轮询read, write 事件 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理 处理任务队列的任务 , 即 runAllTasks
-
每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel ,即通过pipeline 可以获取到对应通道, 管道中维护了很多的处理器
1. 入门的Netty案例
服务端
package netty.netty.simple;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* https://my.oschina.net/javamaster/blog/2994961
* https://blog.csdn.net/zuixiaoyao_001/article/details/90198968
*/
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
//
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //bossGroup使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数 option主要是针对boss线程组,
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态 child主要是针对worker线程组
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//workerGroup使用 SocketChannel创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
package netty.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
/*
说明
1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
2. 这时我们自定义一个Handler , 才能称为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
/*
//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该channel 对应的
//NIOEventLoop 的 taskQueue中,
//解决方案1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
//解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
System.out.println("go on ...");*/
System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel 和 pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
NettyClient
package netty.netty.simple;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler
package netty.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
2 Netty的Bootstrap、ServerBootstrap
1) Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联
各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
2) 常见的方法有:
ServerBootstrap实例中需要两个NioEventLoopGroup实例,按照职责划分成boss和work,有着不同的分工: 1、boss负责请求的accept 2、work负责请求的read、write
NioEventLoopGroup
NioEventLoopGroup主要管理eventLoop的生命周期。 eventLoop是什么?姑且把它看成是内部的一个处理线程,数量默认是处理器个数的两倍。
NioEventLoop
每个eventLoop会维护一个selector和taskQueue,负责处理客户端请求和内部任务,如ServerSocketChannel注册和ServerSocket绑定等。
ServerBootstrap
通过serverBootstrap.bind(port)启动服务,过程如下:
/**
* Create a new {@link Channel} and bind it.
*/public ChannelFuture bind() {validate();
SocketAddress localAddress = this.localAddress;
if (localAddress == null) {
throw new IllegalStateException("localAddress not set")
;}
return doBind(localAddress);}
3 Netty服务端创建过程
步骤1:创建ServerBootstrap实例。ServerBootstrap是Netty服务端的启动辅助类它提供了一系列的方法用于设置服务端启动相关的参数。底层通过门面模式对各种能力进抽象和封装,尽量不需要用户跟过多的底层API打交道,以降低用户的开发难度。我们在创建ServerBootstrap实例时,会惊讲地发现ServerBootstrap只有一个无参的构造函数,作为启动辅助类这让人不可思议,因为它需要与多个其他组件或者类交互。ServerBootstrap构造函数没有参数的根本原因是因为它的参数太多了,而且未来也可能会发生变化,为了解决这个问题,就需要引入Builder模式。
步骤2:设置并绑定Reactor线程池。Netty的Reactor线程池是EventLoopGroup,实际就是EventLoop的数组。EveniLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel,Selector的轮询操作由绑定的EventLoop线程run方法驱动,在一个循环体内循环执行。EveniLoop不仅处理IO。用户自定义的Task和定时任务Task也统一由EventLoop负责处理,这样线程模型就实现了统一。从调度层面看,也不存在从EventLoop线程中再启动其他类型的线程用于异步执行另外的任务,这样就避免了多线程并发操作和锁竞争,提升了IO线程的处理和调度性能。
步骤3:设置并绑定服务端Channel。作为NIO服务端,需要创建ServerSocketChannel ,Netty对原生的NIO类库进行了封装,对应实现是NioServerSocketchannel。对于用户而言,不需要关心服务端Channel的底层实现细节和工作原理,只需要指定具体使用哪种服务端Channel即可。因此,Netty的ServerBootstrap方法提供了channel方法用于指定服务端Channel的类型。Netty通过工厂类,利用反射创建NioServerSocketChannel对象。由于服务端监听端口往往只需要在系统启动时才会调用,因此反射对性能的影响并不大。
步骤4:链路建立的时候创建并初始化ChannelPipeline。ChannelPipeline并不是NIO服务端必需的,它本质就是一个负责处理网络事件的职责链,负责管理和执行ChannelHandler。网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行。典型的网络事件如下。
步骤5: 初始化ChannelPipeline完成之后,添加并设置ChannelHandlerChannelHandler是Netty提供给用户定制和扩展的关键。利用ChannelHandler用户可以完成多数的功能定制,例好消息编解码、心跳、安全认证、TSL/SSL认证、流量控制和流量整形等。Netty同时也提供了大量的系统ChannelHandler供用户使用:
(1)系统编解码框架——ByteToMessageCodec;
(2)通用基于长度的半包解码器LengthFieldBasedFrameDecoder:
(3)码流日志打印HandlerLoggingHandler:
(4)SSL安全认证HandlerSslHandler:
(5)链路空闲检测HandlerIdleStateHandler;
(6)流量整形HandlerChannelTrafficShapingHandler:
(7)Base64编解码Base64Decoder和Base64Encoder
步骤6:绑定并启动监听端口。在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后,会启动监听端口,并将ServerSocketChannel注册到Selector上监听客户端连接。
步骤7:Selector轮询。由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合。
步骤8:当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行ChannelHandler。
步骤9:执行Netty系统ChannelHandler和用户添加定制的ChannelHandler
ChannelPipeline根据网络事件的类型,调度并执行ChannelHandler。
4 Netty服务端创建源码分析
首先通过构造函数创建ServerBootstrap实例,随后,通常会创建两个EventLoopGroup,并不是必须要创建两个不同的EventLoOpGroup,也可以只创建一个并共享)。
NioEventLoopGroup实际就是Reactor线程池,负责调度和执行客户端的接入、网络读写事件的处理、用户自定义任务和定时任务的执行。通过ServerBootstrap的group方法将两个EvcntLoopGroup实例传入。
线程组和线程类型设置完成后,需要设置服务端Channel用于端口监听和客户端链路接入。Netty通过Channel工厂类来创建不同类型的Channel,对于服务端,需要创建NioServerSocketChannel。所以,通过指定Channel类型的方式创建Channel工厂ServerBootstrapChannelFactory是ServerBootstrap的内部静态类,职责是根据Channel的类
型通过反射创建Channel的实例,服务端需要创建的是NioServerSocketChannel实例。
TCP参数设置完成后,用户可以为启动辅助类和其父类分别指定Handler。两类Handler的用途不同:子类中的Handler是NioServerSocketChannel对应的ChannelPipeline的Handler,父类中的Handler是客户端新接入的连接SocketChannel对应的ChannelPipeline的Handler。两者的区别可以通过图13-3来展示
本质区别就是:ServerBootstrap中的Handler是NioServerSocketChannel使用的,所有连接该监听端口的客户端都会执行它:父类AbstractBootstrap中的Handler是个工厂类,它为每个新接入的客户端都创建一个新的Handler
注释NO.1。首先创建Channel,createChannel由子类ServerBootstrap实现,创建新的NioServerSocketChannel。它有两个参数:参数1是从父类的NIO线程池中顺序获取个 NioEventLoop,它就是服务端用于监听和接收客户端连接的Reactor线程,参数2是所谓的workerGroup线程池,它就是处理I/0读写的Reactor线程组。
到此,Netty服务端监听的相关资源已经初始化完毕,就剩下最后一步注册NioServerSocketChannel到Reactor线程的多路复用器上,然后轮询客户端连接事件。
5 netty客户端创建过程
Netty为了向使用者屏蔽NIO通信的底层细节,在和用户交互的边界做了封装,目的为了减少用户开发工作量,降低开发难度。Bootstrap是Socket客户端创建工具类,用户通过Bootstrap可以方便地创建Netty的客户端并发起异步TCP连接操作。
步骤1:用户线程创建Bootstrap实例,通过API设置创建客户端相关的参数,异步发起客户端连接。
步骤2:创建处理客户端连接、IO读写的Reactor线程组NioEventLoopGroup。可以通过构造函数指定I/O线程的个数,默认为CPU内核数的2倍:
步骤3:通过Bootstrap的ChannelFactory和用户指定的Channel类型创建用于客户端连接的NioSocketChannel,它的功能类似于JDKNIO类库提供的SocketChannel:
步骤4:创建默认的ChannelHandlerPipeline,用于调度和执行网络事件;
步骤5:异步发起TCP连接,判断连接是否成功。如果成功,则直接将NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送:如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果;
步骤6:注册对应的网络监听状态位到多路复用器;
步骤7:由多路复用器在IO现场中轮询各Channel,处理连接结果;
步骤8:如果连接成功,设置Future结果,发送连接成功事件,触发ChannelPipeline执行;
步骤9:由ChannelPipeline调度执行系统和用户的ChannelHandler,执行业务逻辑