- 蓝色的框表示线程
- 黄色的框表示对象,
- 白色的框表示方法(API)
反应器模式
2. 分发者模式(Dispatcher)
3. 通知者模式(notifier)
Reactor
:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;Handlers
:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。在
Netty核心技术三
中第四章节的应用实例六--群聊系统
就是单 Reactor 单线程
- 结合实例:服务器端用一个线程通过多路复用搞定所有的IO 操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的NIO案例就属于这种模型。
- 但是实例中没有写对应的handler,只需要将对应处理方法封装为一个个的handler就与图中一致了
针对单 Reactor 多线程模型中,Reactor 在单线程中运 行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行
结合实例:这种模型在许多项目中广泛使用,包括Nginx 主从Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持
Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从Reactor多线程模型有多个 Reactor
Netty 主要基于主从 Reactors 多线程模型(如 图)做了一定的改进,其 中主从 Reactor 多线程模 型有多个 Reacto
下图中的标识有些不准确:
- NioEventGroup应为NioEventLoop
- BossGroup和NioEventLoop之间应该再包一层NioEventLoopGroup
实例要求:
- 使用IDEA 创建Netty项目
- Netty 服务器在 6668 端口监听,客户端能发送消息给服务器"hello, 服务器~"
- 服务器可以回复消息给客户端 “hello, 客户端~”
- 目的:对Netty 线程模型 有一个初步认识, 便于理解Netty 模型理论
- 编写服务端
- 编写客户端
- 对netty 程序进行分析,看看netty模型特点
我创建的项目是maven项目,所以我用maven的方式引入,但是我看maven仓库老师使用的版本有漏洞,我就使用的目前使用人最多的没有漏洞的版本
4.1.42.Final
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.42.Finalversion>
dependency>
代码解读及注意:
自定义Handler需要继承ChannelInboundHandlerAdapter
该handler主要重新三个方法
channelRead(ChannelHandlerContext ctx, Object msg)
- ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
- Object msg: 就是客户端发送的数据 默认Object
- 读取通道时
channelReadComplete(ChannelHandlerContext ctx)
- 读取通道后
exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
- 处理异常, 一般是需要关闭通道
ctx.writeAndFlush()方法:
- writeAndFlush 是 write + flush
package site.zhourui.nioAndNetty.netty;
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;
import java.nio.charset.StandardCharsets;
/*
说明
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 {
System.out.println("server ctx =" + ctx);
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" +buf.toString(StandardCharsets.UTF_8));
System.out.println("客户端地址:" + ctx.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();
}
}
代码解读及注意:
服务端需要BossGroup 和 WorkerGroup
new NioEventLoopGroup();不填写参数时
- bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数默认实际 cpu核数 * 2
创建服务器端的启动对象,配置参数
绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
对关闭通道进行监听
最后不要忘了关闭BossGroup 和 WorkerGroup
package site.zhourui.nioAndNetty.netty;
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.nio.charset.StandardCharsets;
/*
说明
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 {
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();
}
}
该handler和服务端的handler规范一致,主要是看实现的业务如何从而选择重新对应的方法
- 通道启动时会向服务端发送一条信息
- 通道发生读事件时会打印服务端的地址及消息
package site.zhourui.nioAndNetty.netty;
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);
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();
}
}
客户端与服务端基本都是一样的但有以下区别
- 客户端只需要一个事件循环组
- 注意客户端启动对象使用的不是 ServerBootstrap 而是 Bootstrap
- 客户端启动对象配置完成后是连接到服务器bootstrap.connect,而不是bootstrap.bind
package site.zhourui.nioAndNetty.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 InterruptedException {
//客户端需要一个事件循环组
NioEventLoopGroup 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 socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture cf = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
- 当我们不传入参数时默认值为0
- 实际上调用的方法是
MultithreadEventLoopGroup
,所以线程数为DEFAULT_EVENT_LOOP_THREADS
- 而DEFAULT_EVENT_LOOP_THREADS在静态代码块中赋值为NettyRuntime.availableProcessors() * 2
- 即为我们cpu核数*2
我的cpu有24核
debug有48个NioEventLoop
首先修改NettyServer的workerGoup和workerGoup的NioEventLoop数量为指定数量
连接第一个客户端
连接第二个客户端
连接第三个客户端
连接第八个客户端
连接第九个客户端
结论:workerGoup在NioEventLoop为8时,多个客户端与服务器进行通讯,workerGoup将按序分配NioEventLoop,如果超出NioEventLoop个数将会从头再次循环分配
ctx数据
channel数据
pipeline数据
结论:
ctx中有head和tail说明是双向链表
可以通过ctx拿到pipeline,然后通过pipeline拿到channel
channel 和 pipeline是相互包含的关系
pipeline本质是一个双向链接, 出站入站,里面有head和tail
经过前面的学习:每个channel都有对应的一个pipeline,而pipeline是一个存放handler的一个双向链表,当其中某个handler执行时间较长时,就会阻塞pipeline,所以我们考虑将执行时间较长的handler提交到taskQueue中异步执行
模拟
自定义的普通任务执行较长时间
不用taskQueue从而导致服务端阻塞将NettyServerHandler中的channelRead修改为如下代码:
本质上是模拟长时间handler阻塞
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
System.out.println("go on ...");
从代码实验结果:
服务端打印
go on ...
在10秒之后,证明服务端被阻塞
解决方案:
用户程序自定义的普通任务
用户自定义定时任务
非当前 Reactor 线程调用 Channel 的各种方法
例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的Write会提交到任务队列中后被异步消费
用户程序自定义的普通任务提交给提交该channel 对应的NIOEventLoop 的 taskQueue中
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
执行结果:
结论:
服务端在刚启动时服务端就打印
go on ...
客户端在刚启动时就收到
服务器回复的消息:hello, 客户端~(>^ω^<)喵1
都证明了服务端没有阻塞
执行结果:
客户端
结论:多个handler被提交到同一个taskQueue时是单线程排队执行的
结论:证明handler确实是被提交到taskQueue中
使用方法和6.2.1
用户自定义的普通任务
一致,只有如下几处不同
//解决方案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);
例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的Write会提交到任务队列中后被异步消费
每次客户端连接时打印socketChannel的hashCode
开启一个服务端,一个客户端
再开一个客户端
结论:每个客户端都有一个不同的channel
Future-Listener 机制
,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果链式操作示意图:
isDone
方法来判断当前操作是否完成;isSuccess
方法来判断已完成的当前操作是否成功; getCause
方法来获取已完成的当前操作失败的原因;isCancelled
方法来判断已完成的当前操作是否被取消;addListener
方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器演示:绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("监听端口 6668 成功");
}else {
System.out.println("监听端口 6668 失败");
}
}
});
执行结果:
当我们启动服务端时,只要服务器绑定端口成功就可以通过Future来监听我们关注的事件的状态,本案例的事件即绑定事件
小结:相比传统阻塞 I/O,执行 I/O 操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在 I/O 操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量
- 实例要求:使用IDEA 创建Netty项目
- Netty 服务器在 6668 端口监听,浏览器发出请求"http://localhost:6668/ "
- 服务器可以回复消息给客户端 "Hello! 我是服务器5 " , 并对特定请求资源进行过滤.
- 目的:Netty 可以做Http服务开发,并且理解Handler实例和客户端及其请求的关系.
- 在加入我们的自定义Handler之前我们还加入了一个HttpServerCodec
- HttpServerCodec 是netty 提供的处理http的 编-解码器
- codec =>[coder - decoder]
- 如果浏览器发出请求"http://localhost:6668/ "访问不了,就把端口改为8000以上的再试试,我这里bind(8080)
package site.zhourui.nioAndNetty.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供的httpServerCodec codec =>[coder - decoder]
//HttpServerCodec 说明
//1. HttpServerCodec 是netty 提供的处理http的 编-解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//2. 增加一个自定义的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
System.out.println("ok~~~~");
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package site.zhourui.nioAndNetty.netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import java.net.URI;
/*
说明
1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
2. HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//channelRead0 读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断 msg 是不是 httprequest请求
if (msg instanceof HttpRequest){
System.out.println("msg 类型=" + msg.getClass());
System.out.println("客户端地址" + ctx.channel().remoteAddress());
//回复信息给浏览器 [http协议]
ByteBuf content = Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_8);
//构造一个http的响应,即 httpresponse
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
//设置响应headers 响应格式 ;charset=utf-8
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
//设置响应headers 响应数据长度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
//将构建好 response返回
ctx.writeAndFlush(response);
}
}
}
package site.zhourui.nioAndNetty.netty.http;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供的httpServerCodec codec =>[coder - decoder]
//HttpServerCodec 说明
//1. HttpServerCodec 是netty 提供的处理http的 编-解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//2. 增加一个自定义的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
System.out.println("ok~~~~");
}
}
抽取后感觉服务端清爽很多,我们处理业务的代码全在TestServerInitializer的handler中
发现问题:
只需要在服务端回复之前对uri的路径进行判断如果包含过滤路径的请求就过滤掉
//获取到
HttpRequest httpRequest = (HttpRequest) msg;
//获取uri, 过滤指定的资源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico, 不做响应");
return;
}
在接到请求时打印pipeline hashcode和TestHttpServerHandler hashcode
System.out.println("ctx 类型="+ctx.getClass());
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());
发现两次请求的pipeline的hashCode不一致,说明每次请求都会创建一个新的pipeline
原因:http协议请求完成过后会断开,再次请求时会重新生成一个pipeline