简介:reactor模式 和 Netty线程模型
设计模式——Reactor模式(反应器设计模式),是一种基于 事件驱动的设计模式,在事件驱动的应用中,将一个或多个客户的 服务请求分离(demultiplex)和调度(dispatch)给应用程序。在 事件驱动的应用中,同步地、有序地处理同时接收的多个服务请求 一般出现在高并发系统中,比如Netty,Redis等。
通俗理解:KTV例子 前台接待,服务人员带领去开机器. Reactor模式基于事件驱动,适合处理海量的I/O事件,属于同步非阻塞IO(NIO)
优点
响应快,不会因为单个同步而阻塞,虽然Reactor本身依然 是同步的;
编程相对简单,最大程度的避免复杂的多线程及同步问题, 并且避免了多线程/进程的切换开销;
可扩展性,可以方便的通过增加Reactor实例个数来充分利 用CPU资源;
缺点
Reactor单线程模型( 比较少用)
1)作为NIO服务端,接收客户端的TCP连接;作为NIO客户 端,向服务端发起TCP连接;
2)服务端读请求数据并响应;客户端写请求并读取响应
使用场景:
对应小业务则适合,编码简单;对于高负载、大并发的应用场 景不适合,一个NIO线程处理太多请求,则负载过高,并且可能响应 变慢,导致大量请求超时,而且万一线程挂了,则不可用了
Reactor多线程模型
内容:
1)一个Acceptor线程,一组NIO线程,一般是使用自带的线程池,包含一个任务队列和多个可用的线程
使用场景:
可满足大多数场景,但是当Acceptor需要做复杂操作的时候, 比如认证等耗时操作,再高并发情况下则也会有性能问题
Reactor主从线程模型
内容:
Acceptor不再是一个线程,而是一组NIO线程;IO线程也 是一组NIO线程,这样就是两个线程池去处理接入连接和处理IO
使用场景: 满足目前的大部分场景,也是Netty推荐使用的线程模型 BossGroup WorkGroup
补充:
为什么Netty使用NIO而不是AIO,是同步非阻塞还是异步非阻塞?
答:在Linux系统上,AIO的底层实现仍使用EPOLL,与NIO相同,因此在性能上没有明显的优势.
Netty整体架构是reactor模型, 采用epoll机制,所以往深的说,还是IO多路复用模式,所以也可说 netty是同步非阻塞模型
很多人说这是netty是基于Java NIO 类库实现的异步通讯框架
特点:异步非阻塞、基于事件驱动,性能高,高可靠性和高可定制 性。
Echo服务和Netty项目搭建
io.netty netty-all 4.1.32.Final
Echo服务-服务端程序编写 对应的启动类和handler处理器:
EchoServer.java
/**
* @Description: Echo服务端
*/
public class EchoServer {
private int port;// 服务端端口号
public EchoServer(int port) {
this.port = port;
}
/** * 启动服务的run 方法 */
public void run() throws InterruptedException {
// 配置服务端线程组
NioEventLoopGroup bossGroup = new NioEventLoopGroup(); // 老板:后台线程组
NioEventLoopGroup workGroup = new NioEventLoopGroup(); // 员工:前台线程组
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();// 服务启动引导类
serverBootstrap.group(bossGroup,workGroup) // 线程组交给服务引导类
.channel(NioServerSocketChannel.class) //指定服务端和客户端链接的管道
.childHandler(
new ChannelInitializer() { // 自定义个handler
protected void initChannel(SocketChannel socketChannel) throws Exception { // 添加自己创建的 EchoServerHandler 逻辑
socketChannel.pipeline().addLast(
new EchoServerHandler());
}
});
System.out.println("Echo 服务器启动 ing..."); //绑定端口,同步等待成功
ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); //等待服务端监听端口关闭
channelFuture.channel().closeFuture().sync();
}finally {
//优雅退出,释放线程池
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String [] args)
throws InterruptedException {
int port = 8080;
if(args.length > 0){
port = Integer.parseInt(args[0]);
}
new EchoServer(port).run();
}
}
EchoServerHandler.java
/**
* @Auther: csp1999
* @Date: 2020/09/17/21:15
* @Description: Echo服务端逻辑处理器
*/
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
/*
* 从管道中读取数据
* @param: ctx
* @param: msg
* @return: void
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf data = (ByteBuf) msg;
System.out.println("服务端收到数据: " + data.toString(CharsetUtil.UTF_8));
ctx.writeAndFlush(data);
}
/*
* 从管道中读取数据完毕
* @param: ctx
* @return: void
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("EchoServerHandlechannelReadComplete 从管道中读取数据完毕...");
}
/*
* 异常捕获
* @param: ctx
* @param: cause
* @return: void
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
cause.printStackTrace();// 打印异常
ctx.close();
}
}
EchoClient.java
/**
* @Description: echo 客户端
*/
public class EchoClient {
private String host;// 客户端ip
private int port;// 客户端端口号
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
/** * 启动服务的run 方法 */
public void run() throws InterruptedException {
// 定义线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap(); // 客户端服务启动引导类
bootstrap.group(group)// 线程组交给服务 引导类
.channel(NioSocketChannel.class) // 指定服务端和客户端链接的管道 NioServerSocketChannel
.remoteAddress(new InetSocketAddress(host, port))// 地址和端口
.handler(
new ChannelInitializer() {
protected void initChannel(SocketChannel ch) throws Exception {
// EchoClientHandler 逻辑
ch.pipeline().addLast(new EchoClientHandler());
}
});
//连接到服务端,connect是异步连接,在调用同步 等待sync,等待连接成功
ChannelFuture channelFuture = bootstrap.connect().sync(); //阻塞直到客户端通道关闭
channelFuture.channel().closeFuture().sync();
} finally {
//优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1", 8080).run();
}
}
EchoClientHandler.java
/**
* @Description: echo 客户端逻辑处理器
*/
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Client received 客户端收到的数据: " + msg.toString(CharsetUtil.UTF_8));
}
/*
* 管道激活
* @param: ctx
* @return: void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Active 管道激活...");
ctx.writeAndFlush(Unpooled.copiedBuffer("711 改了密码:",CharsetUtil.UTF_8));
}
/*
* 管道数据读取完毕
* @param: ctx
* @return: void
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("EchoClientHandlerchannelReadComplete 管道读取完毕...");
}
/*
* 异常捕获
* @param: ctx
* @param: cause
* @return: void
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwablecause) throws Exception {
cause.printStackTrace();// 抛出异常
ctx.close();
}
}
EventLoop和EventLoopGroup线程模型
1)高性能RPC框架的3个要素:IO模型、数据协议、线程模型。
2)EventLoop 就是一个线程,1个 EventLoop 可以服务多个Channel 管道,1个 Channel 只有一个EventLoop;可以创建多个 EventLoop 来优化资源利用,也就是EventLoopGroup。
3)EventLoopGroup 负责分配 EventLoop 到新创建的Channel。
4)源码分析默认线程池数量 = CPU 核数*2
# EventLoop -> 维护一个 Selector(单线程的方 式管理多个Channel)
学习资料:http://ifeve.com/selectors/
\2. Netty启动引导类Bootstrap模块
简介:Netty启动引导类Bootstrap作用和tcp通道参数设置
参考:https://blog.csdn.net/QH_JAVA/article/details/78383543
ServerBootstrap serverBootstrap = new ServerBootstrap();
Channel模块
简介: Channel 的作用,核心模块,生命周期等
他们是什么关系:
一个Channel包含一个ChannelPipeline,所有ChannelHandler 都会顺序加入到ChannelPipeline中.
创建Channel时会自动创建一 个ChannelPipeline,每个Channel都有一个管理它的pipeline, 这关联是永久性的。
Channel当状态出现变化,就会触发对应的事件:
状态:
例如:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Active 管道激活...");
ctx.writeAndFlush(Unpooled.copiedBuffer("711改了密码:", CharsetUtil.UTF_8));
}
ChannelHandler和ChannelPipeline模块
简介:ChannelHandler和ChannelPipeline核心作用和生命周期
方法:
Netty异步操作模块ChannelFuture
Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而ChannelFuture会提供有关的信息I/O操作的结果或状态。
ChannelFuture状态:
注意:不要在IO线程内调用future对象的sync或者await方 法,不能在channelHandler中调用sync或者await方法。
ChannelPromise:继承于ChannelFuture,进一步拓展用于设 置IO操作的结果
什么是编码、解码
高性能RPC框架的3个要素:IO模型、数据协议、线程模型
最开始接触的编码:java序列化/反序列化(就是编解码)、url编 码、base64编解码
为啥jdk有编解码,还要netty自己开发编解码?( java自带序列化 的缺点 )
业界里面也有其他编码框架: google的 protobuf(PB)、 Facebook的Trift、Jboss的Marshalling、Kyro等
Netty里面的编解码:
Netty里面提供默认的编解码器,也支持自定义编解码器:
:Decoder对应的就是ChannelInboundHandler,主要就是字节数组 转换为消息对象。
主要是两个方法:
decode :一般都使用该方式
decodeLast:用于最后的几个字节处理,也就是channel 关闭 的时候,产生的最后一个消息。
抽象解码器 :
ByteToMessageDecoder:用于将字节转为消息,需要检查缓 冲区是否有足够的字节。
ReplayingDecoder:继承ByteToMessageDecoder,不需要检 查缓冲区是否有足够的字节,但是R速度略慢 于ByteToMessageDecoder,不是所有的ByteBuf都支持。
MessageToMessageDecoder:用于从一种消息解码为另外一 种消息(例如:POJO到POJO)
选择:项目复杂性高则使用ReplayingDecoder,否则使用 ByteToMessageDecoder
解码器具体的实现,用的比较多的是(更多是为了解决TCP底层的粘 包和拆包问题): DelimiterBasedFrameDecoder: 指定消息分隔符的解码器 xxx&aaa&bbb
LineBasedFrameDecoder: 以换行符为结束标志的解码器
FixedLengthFrameDecoder:固定长度解码器
LengthFieldBasedFrameDecoder:message = header+body, 基于长度解码的通用解码器
StringDecoder:文本解码器,将接收到的对象转化为字符串, 一般会与上面的进行配合,然后在后面添加业务handle
Encoder对应的就是ChannelOutboundHandler,消息对象转换为 字节数组。
Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对 等的:
MessageToByteEncoder:消息转为字节数组,调用write方 法,会先判断当前编码器是否支持需要发送的消息类型,如果不 支持,则透传;
MessageToMessageEncoder:用于从一种消息编码为另外一 种消息(例如POJO到POJO)
组合解码器和编码器,以此提供对于字节和消息都相同的操作。
优点:成对出现,编解码都是在一个类里面完成。
缺点:耦合在一起,拓展性不佳
Codec:组合编解码
ByteToMessageCodec
MessageToMessageCodec
decoder:解码
ByteToMessageDecoder
MessageToMessageDecoder
encoder:编码
ByteToMessageEncoder
MessageToMessageEncoder
什么是TCP粘包拆包?
TCP拆包: 一个完整的包可能会被TCP拆分为多个包进行发送
TCP粘包: 把多个小的包封装成一个大的数据包发送, client发送的若干数据包 Server接收时粘成一包
发送方和接收方都可能出现这种情况:
发送方的原因:TCP默认会使用Nagle算法
接收方的原因: TCP接收到数据放置缓存中,应用程序从缓存中 读取
UDP: 是没有粘包和拆包的问题,有边界协议
发送方:可以关闭Nagle算法
接受方:TCP是无界的数据流,并没有处理粘包现象的机制, 且协议 本身无法避免粘包,半包读写的发生需要在应用层进行处理,应用 层解决半包读写的办法。
设置定长消息 (10字符): xdclass000xdclass000xdclass000xdclass000
设置消息的边界 ($$ 切割): sdfafwefqwefwe$$dsafadfadsfwqehidwuehfiw$$879329832 r89qweew$$
使用带消息头的协议,消息头存储消息开始标识及消息的长度信 息:Header+Body
时间轮算法管理延迟任务队列
在网络通信中管理上万的连接,每个连接都有超时任务,如果为每个任务启动一个TImer超时器,那么会占用大量资源。为了解决这个问题,可用Netty工具类HashedWheelTimer。
这个类用来计划执行非精准的I/O超时。可以通过指定每一格的时间间隔来改变执行时间的精确度。在大多数网络应用中,I/O超时不需要十分准确,因此,默认的时间间隔是100 毫秒,这个值适用于大多数场合。HashedWheelTimer内部结构可以看做是个车轮,简单来说,就是TimerTask的hashTable的车轮。车轮的size默认是512,可以通过构造函数自己设置这个值。注意,当HashedWheelTimer被实例化启动后,会创建一个新的线程,因此,你的项目里应该只创建它的唯一一个实例。
(这个类源自一位教授的论文 Tony Lauck)
意思是说,时间间隔越小精度越高?因为没必要完全精准,所以用时间轮每隔一段时间去处理?
看起来是单例模式
tick: 时钟“嘀嗒”一声,代表时间轮转动一下,到下一个hash桶
提交任务的线程,只要把任务往虚线上面的任务队列中存放即可返回。工作线程是单线程,一旦开启,不停地在时钟上绕圈圈。
超时任务设置为1000毫秒,超时之后,由hashedWheelTimer类中的worker线程,执行超时之后的任务。
hashedWheelTimer有32个槽(类比HashMap中的桶),默认每100毫秒移动下一个槽。
任务需要经过的tick数为: 1000 / 100 = 10次 (等待时长 / tickDuration)
任务需要经过的轮数为 : 10次 / 32次/轮 = 0轮 (tick总次数 / ticksPerWheel)
因为任务超时后不能马上被worker线程执行,需要等到worker线程移到相应卡槽位置时才会执行,因此说执行时间不精确。
hashedWheelTimer的核心是Worker线程,主要负责每过tickDuration时间就累加一次tick. 同时, 也负责执行到期的timeout任务, 此外,还负责添加timeou任务到指定的wheel中。
TimerTask 非常简单,就一个 run()
方法:
public interface TimerTask {
void run(Timeout timeout) throws Exception;
}
这里有点意思的是,它把 Timeout 的实例也传进来了,我们平时的代码习惯,都是单向依赖。
这样做也有好处,那就是在任务执行过程中,可以通过 timeout 实例来做点其他的事情。
Timeout 也是一个接口类:
public interface Timeout {
Timer timer();
TimerTask task();
boolean isExpired();
boolean isCancelled();
boolean cancel();
}
它持有上层的 Timer 实例,和下层的 TimerTask 实例,然后取消任务的操作也在这里面。
Dubbo 的集群调用策略 FailbackClusterInvoker
中:
它在调用 provider 失败以后,返回空结果给消费端,然后由后台线程执行定时任务重试,多用于消息通知这种场景。
- 那么其他的降级是怎么实现的?
- feture和Promise?