Netty官网说明
官网:https://netty.io/
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
netty基于事件驱动的网络应用框架,为了快速开发高性能的服务器端和客户端
core核心:零拷贝,API库、可扩展的事件模型
protocol support:支持的相关协议,http/websocket,ssl安全连接等
transport services:传输服务
线程模型基本介绍
- 不同的线程模式,对程序的性能有很大影响,为了搞清Netty线程模式,我们来系统的讲解下,各线程模式,最后看看Netty线程模型有什么优越性
- 目前存在的线程模型有:
- 传统阻塞I/O服务模型
- Reactor模式
- 根据Reactor的数量和处理资源池线程的数量不同,有三种典型的实现
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
- Netty线程模式(Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程有多个Reactor)
传统阻塞I/O服务模型
工作原理图:
黄色框表示对象,蓝色的框表示线程,白框表示方法
模型特点:
- 采用阻塞IO模式获取输入的数据
- 每个连接都需要独立的线程完成数据的输入,业务处理、数据返回
问题分析:
- 当并发数很大,就会创建大量的线程,占用很大的系统资源
- 连接创建后,如果当前线程暂时没有数据处理或者可读,该线程会阻塞在read方法上,导致这个线程资源的浪费
解决方案(Reactor模式)
- 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池复用线程资源,不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务
示意图
Reactor模式
又称反应器模式、分发者模式、通知者模式
黄色代表对象,白色代表方法
说明:
- Reactor模式,通过一个或者多个输入同时传递给服务器的模式,基于事件驱动
- 服务器端程序处理传入的多个请求,并将他们同步分派到不同线程进行处理
- Reactor模式使用IO复用监听事件,收到事件后进行分发给某个线程,这就是网络服务器高并发处理关键
Reactor模式中核心组成:
- Reactor(ServiceHandler):Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序对IO事件作出反应,他就想公司的电话接线员,他接听来自客户的电话并将线路转移到适当的联系人
- Handlers(图中EventHandler):处理程序执行IO事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员,Reactor通过调度适当的处理程序来相应IO事件,处理程序执行非阻塞操作
Reactor分类
根据Reactor的数量和处理资源线程池的数量不同,有三种典型的实现:
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程(Netty从这里实现的)
单Reactor单线程
工作原理示意图
过程:
多个客户端发送请求给Reactor,Reactor中的Select通过轮询判断事件类型,在进行dispatch分发,如果事件为连接,会分发给Acceptor进行accept处理,如果是读事件的话,会分发给Handle进行相应业务的处理,之前我们写的都是方法,我们可以将方法封装到一个类中,这样就变成了相应的Handler
目前存在的问题:
单线程,如果有多个连接发送请求的话,在处理read的时候会发生等待
单Reactor多线程
工作原理图
主从Reactor多线程
工作原理示意图
Reactor子线程可以有多个
Netty模型
工作原理示意图
Netty主要基于主从Reactors多线程模型做了一定的改进,其中主从Reactor多线程有多个Reactor
工作流程
工作原理示意图2
Netty主要基于主从Reactors多线程模型(如图)做了一定的改进,其中主从Reactor多线程模型有多个Reactor
工作原理示意图(详细版)
Netty快速入门实例-TCP服务
- 实例要求:使用IDEA创建Netty项目
- Netty服务器在6668端口监听,客户端能发送消息给服务器“hello,服务器”
- 服务器可以回复消息给客户端“hello,服务器”
- 目的:对Netty线程模型有一个初步认识,便于理解Netty模型理论
编写服务端程序
public class NettyServer {
public static void main(String[] args) throws Exception{
//创建BossGroup和WorkerGroup
//说明:
//创建两个线程租bossGroup和workerGroup
//bossGroup只是处理连接请求,真正的和客户端业务处理,会交给workerGroup完成
//两个都是无限循环
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置启动参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程进行设置
bootstrap.group(bossGroup, workerGroup)//设置两个线程组
.channel(NioServerSocketChannel.class)//针对bossGroup 使用NioServerSocketChannel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列等待连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true )//设置保持活动连接状态
//给我们的workerGroup的EventLoop对应的管道设置处理器
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象(匿名对象)
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());//添加处理器
}
});
System.out.println("...服务器 is ready");
ChannelFuture channelFuture = bootstrap.bind(6668).sync();//绑定一个端口并且同步,生成ChannelFuture对象
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
处理器
/*
说明:
1.我们自定义一个handler 需要继承netty 规定好的某个handlerAdapter(有规范需要我们遵守)
2.这时我们自定义一个handler,才能称之为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据的事件(这里我们可以读取客户端发送的消息)
/*
1.channelHandlerContext 为上下文对象,包含管道pipeline(可以关联很多handler)、通道channel、地址
2.msg 就是客户端发送的数据,以对象的形式发过来,默认object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx=" + ctx);
//将msg转成ByteBuf,这个是netty提供,不是nio的bytebuffer
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + byteBuf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + ctx.channel().remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//数据写到缓存中,同时刷新该缓存(将缓存中的数据写到管道中),是write+flush方法
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
}
//处理异常,一般需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// ctx.channel().close();
ctx.close();
}
}
编写客户端程序
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要一个事件循环组
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是ServerBootstrap 而是Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(eventLoopGroup).//设置线程组
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();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
处理器
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();
}
}
对Netty程序进行分析
BossGroup和WorkerGroup
- bossGroup和workerGroup含有的子线程(NioEventGroup)的个数,默认为cpu核数*2
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
//NettyRuntime.availableProcessors()是处理器核数
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
每一个子线程都是eventLoop类型,默认8个 ,就是计算机核数*2,靠eventExecutor数组进行管理,如果设置线程数量直接在括号中填写个数
如果有超过8个客户端进行连接,会重头开始使用线程
pipeline
- 本质是一个双向链表,涉及到出栈入栈问题
- 有head和tail
- 通过pipeline可以拿到channel,是一一对应的关系
ChannelHandlerContext ctx
- 属于defaultChannelHandlerContext类
- 里面含有next和prev,双向链表的特点
- inbound为入站状态,不是outbound状态
- 可以通过ctx拿到pipeline对象
channel对象
任务队列中的Task有3种典型使用场景
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前Reactor线程调用Channel的各种方法
- 例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景,最终的write会提交到任务队列中后被异步消费
解决方案一:用户程序自定义的普通任务
就不会阻塞在这个方法里了,要不然的话就得等这个任务执行10s之后再去执行后面的操作
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如这里我们有一个非常耗时长的业务->异步执行->
//提交该channel对应的NIOEventLoop的taskQueue中
System.out.println("server ctx=" + ctx);
System.out.println("看看channel和pipeline的关系");
Channel channel = ctx.channel();
EventLoop eventLoop = channel.eventLoop();
eventLoop.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
ctx里面有个eventLoop,eventLoop里面有taskQueue,size为1,有一个任务
起了两个任务,但是这两个任务都是一个线程进行执行的,相当于第二个任务的话需要30s后才能执行,此时的size值为2
解决方案二:用户程序自定义的定时任务
//用户自定义定时任务->该任务是提交到scheduleTaskQueue中
/*
* command 就是任务对象
* delay 延时多长时间去执行
* timeunit 时间单位
*/
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},5, TimeUnit.SECONDS);
解决方案三:非当前Reactor线程调用Channel的各种方法
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());//添加处理器
}
可以使用一个集合管理SocketChannel,再推送消息时,可以将业务加入到各个channel对应的NIOEventLoop的taskQueue或者scheduleTaskQueue
下图不同客户端产生不同的hash值
异步模型
基本介绍
Future说明
工作原理示意图
链式操作示意图
Future-Listener机制
举例说明