图解高性能服务器开发两种模式,第四章 NETTY高性能架构设计

目录

一、NIO存在问题以及Netty的优点

二、线程模型基本介绍

三、工作原理图(传统同步阻塞式IO)

四、Reactor模式

五、单Reactor单线程

六、单Reactor多线程

七、主从REACTOR多线程

八、Reactor模式小结

九、Netty模型

十、异步模型

一、NIO存在问题以及Netty的优点

e39132fd6a40

image-20201026190253791.png

原生NIO存在的问题

1.NIO的库和API繁杂,使用麻烦

需要掌握ServerSocketChannel,SocketChannel,Selector,ByteBuffer

需要掌握多线程,网络编程

2.传输遇到各种问题

1.断连重连,网络闪断,网络拥塞

2.半包读写

3.失败缓存

4.异常流的处理

3.JDK的NIO的Epoll Bug会导致Selector空轮询,最终导致CPU100%占用,直到JDK1.7版本这个问题依旧存在,没有根本解决

NETTY的优点

1.官网翻译

1.JBOSS 提供的一个 Java 开源框架,Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序

2.简化和流程化了 NIO 的开发过程

3.Netty获得了广泛的应用知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。

2.优点

(1)高性能,吞吐量,延迟低,资源消耗低,不必要的内存复制(零拷贝)

(2)设计优雅,使用各种传输类型(扩展性强)的阻塞和非阻塞Socket,灵活的可扩展事件模型

定制线程模型----线程池

(3)社区活跃,版本Bug修复快,迭代周期短

3.Netty版本说明

(1)Netty5出现重大bug,已经被官网废弃了,目前推荐使用的是Netty4.x的稳定版本

(2)netty 下载地址

二、线程模型基本介绍

1.基本介绍

(1)线程模型有:传统阻塞 I/O 服务模型 Reactor 模式

(2)根据 Reactor的三种实现方式

#Reactor数量和处理的资源池中的线程数量不同,有3中实现方式

1.单 Reactor 单线程

2.单 Reactor 多线程

3.主从 Reactor 多线程(多个Reactor)

Netty线程模式(Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从 Reactor多线程模型有多个Reactor)

三、工作原理图(传统同步阻塞式IO)

e39132fd6a40

image-20201027122414241.png

四、Reactor模式

e39132fd6a40

image-20201027124634068.png

1.解决传统阻塞I/O模型的2个缺点

(1)多连接对应一个阻塞对象

1.1 基于I/O复用模型,多个连接共用一个阻塞对象,服务器端无需阻塞所有的连接,只需要阻塞公用的阻塞对象即可

1.2 当某个连接有数据可以处理(读事件),则OS会通知引用程序,线程从阻塞状态返回,开始业务处理

(2)复用线程池

2.1 server端不必为每个连接创建线程,连接完成后的业务处理 --分配--> 线程处理

2.2 一个线程就可以处理多个连接对象呢

(3)Reactor模式高并发关键

3.1 server端收到多个请求,分发到相应的线程,这就是网络服务器高并发的处理关键

2.I/0复用结合线程池,就是Reactor模式基本设计思想

1.复用handler和使用了线程池

2.NIO + 线程池

3.selector是单线程,而reactor是用共享的线程池处理

4.BIO没有多路复用,是简单递归,多路复用是操作系统底层实现的

3.Reactor模式中核心组成(Reactor和Handler)

e39132fd6a40

image-20201027130605590.png

4.Reactor 模式分类和叫法

#分类

单 Reactor 单线程

单 Reactor 多线程

主从 Reactor 多线程

#叫法

1.反应器模式

2.分发者模式(Dispatcher)----server收到多个请求分派到相应的处理线程处理(从线程池)

3.通知者模式(notifier)---有新数据,OS通知application,线程从阻塞状态返回,进行业务处理

五、单Reactor单线程

e39132fd6a40

image-20201027133929810.png

优缺点 || 场景

1.优点:模型简单,没有多线程和竞争的问题,一个线程中完成全部功能(连接,读写业务逻辑)

2.缺点:

(1)性能低: 单线程无法发挥多核 CPU 的性能。Handler在处理某个连接的业务时,整个进程则无法处理其他连接,导致应用的性能瓶颈。

(2)可靠性: 线程如果进入死循环,会导致整个系统通信模块不可用,造成节点故障

3.使用场景:Redis在业务处理的时间复杂度 O(1) 的情况

(1)客户端的数量有限,业务处理非常快速

(2)连接少,处理快,就不会阻塞单线程

六、单Reactor多线程

原理图

e39132fd6a40

image-20201027181129115.png

优缺点分析

1.优点:可以充分的发挥多核CPU的性能

2.缺点:虽然处理逻辑可以由线程池来处理(并发),但是Reactor依然是单线程运行的,它需要处理所有事件的监听和响应(accept和handler都是单线程)

在高并发的场景容易出现性能瓶颈,多线程的数据共享和数据访问

七、主从REACTOR多线程

工作原理图

e39132fd6a40

image-20201027184406226.png

优缺点分析

1.优点

(1)父子线程的职责清晰,MainReactor负责接收新连接,SubReactor负责后续的业务处理(read数据----多线程),在把业务处理交给线程池处理(多线程)

(2)父子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据

2.缺点

(1)编程复杂度高

3.场景:

(1)Nginx主从Reactor多进程模型

(2)Netty主从多线程模型

(3)Memcached主从多线程模型

八、Reactor模式小结

总结

#3种模式用生活案例来理解

1.单Reactor单线程,前台接待员和服务员是同一个人(select是一个线程处理)

2.单Reactor多线程,1个前台接待员,多个服务员(线程池),接待员只负责接待

3.主从 Reactor 多线程,多个前台接待员(多个线程subReactor来handler),多个服务生(线程池)

#Reactor模式具有如下的优点

1.响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的

可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销

2.扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源

3.复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性

九、Netty模型

工作原理示意图1-简单版

Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor

e39132fd6a40

image-20201027192256016.png

工作原理示意图-详细版

e39132fd6a40

e39132fd6a40

image-20201027204823544.png

Pipeline管道的处理器Handler是关键,最后都是由它来处理读写事件

(1)管道像水流一样,中间有许多的handler处理器对水进行加工,形成矿泉水-------业务逻辑------处理数据

(2)通道更多的是传输数据,读写的操作--------read,write操作----传输数据

1.Netty快速入门实例-TCP服务

io.netty

netty-all

4.1.20.Final

2.编写客户端和服务器端代码

//1.NettyServer.java服务器

public class NettyServer {

public static void main(String[] args) throws InterruptedException {

// 1.创建BossGroup 和 WorkerGroup,两个都是无限循环

// 含有多少子线程(NioEventLoop),默认是cpu核心数 * 2

EventLoopGroup bossGroup = new NioEventLoopGroup();

EventLoopGroup workerGroup = new NioEventLoopGroup();

// 2.创建服务器端的启动对象bootstrap,配置各种参数

ServerBootstrap bootstrap = new ServerBootstrap();

try{

// 3.使用链式编程设置参数(底层其实是一堆的set方法)

bootstrap.group(bossGroup,workerGroup)//设置两个线程组

.channel(NioServerSocketChannel.class)//使用NioServerSocketChannel作为服务器的通道实现

.option(ChannelOption.SO_BACKLOG,128)//设置线程队列得到的连接个数

.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态

.childHandler(new ChannelInitializer() {

//给pipeline设置处理器(管道和通道可以相互获取)

protected void initChannel(SocketChannel socketChannel) throws Exception {

socketChannel.pipeline().addLast(new NettyServerHandler());

}

});//给我们的worker Group的EventLoop对应的管道设置处理器

System.out.println("...服务器 is ready...");

// 4.绑定一个端口并同步(启动服务器),生成一个ChannelFuture对象

ChannelFuture cf = bootstrap.bind(6668).sync();

// 5.对关闭通道进行监听

cf.channel().closeFuture().sync();

}finally {

bossGroup.shutdownGracefully();

workerGroup.shutdownGracefully();

}

}

}

//2.NettyClient.java客户端

public class NettyClient {

public static void main(String[] args) throws Exception{

// 1.客户端需要一个事件循环组

NioEventLoopGroup group = new NioEventLoopGroup();

// 2.创建客户端启动对象bootstrap,并进行相应的配置

Bootstrap bootstrap = new Bootstrap();

try {

// 3.设置相关参数

bootstrap.group(group)//设置线程组

.channel(NioSocketChannel.class)//设置客户端通道的实现类(反射)

.handler(new ChannelInitializer() {

protected void initChannel(SocketChannel socketChannel) throws Exception {

socketChannel.pipeline().addLast(new NettyClientHandler());//加入自己的处理器

}

});

System.out.println("客户端 ok ...");

// 4.启动客户端连接服务器(ChannelFuture设计到Netty的异步模型)

ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();

// 5.监听关闭通道

channelFuture.channel().closeFuture().sync();

}finally {

group.shutdownGracefully();

}

}

}

3.启动效果(先启动server,在启动client)

e39132fd6a40

image-20201027221621458.png

2.源码之NioEventLoop&&ChannelHandlerContext

1.NioEventLoopGroup含有多少子线程(NioEventLoop)

(1)默认是cpu核心数 * 2

(2)源码解释

e39132fd6a40

(3)断点调试验证

e39132fd6a40

image-20201027225616799.png

(4)修改bossGroup的子线程个数(NioEventLoop) == 1

EventLoopGroup bossGroup = new NioEventLoopGroup(1);

(5)NioEventLoop包含什么重要的信息

e39132fd6a40

image-20201027232612185.png

2.查看Channel和Pipeline的关系以及Ctx(ChannelHandlerContext)

1.Channel和Pipeline的关系: 我中有你,你中有我

2.Ctx(ChannelHandlerContext)包含了丰富的信息

//NettyServerHandler.java

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

System.out.println("服务器读取线程: " + Thread.currentThread().getName());

System.out.println("server ctx: " + ctx);

System.out.println("看看channel和pipeline的关系");

Channel channel = ctx.channel();

ChannelPipeline pipeline = ctx.pipeline();//底层是一个双向链表,出栈入栈

}

e39132fd6a40

image-20201027234240654.png

3.任务队列中的Task有3种典型使用场景

1.用户自定义的普通任务 ------------> taskQueue

1.NioEventLoop(一个线程)其实就是线程池的实现(类似newSingleThreadExecutor),顶层实现了ExecutorService接口

2.任务队列"入队出队"取出任务执行,线程一个只能执行一个任务,其他放在阻塞队列,按顺序执行

e39132fd6a40

image-20201028072506646.png

2.用户自定义的定时任务 ----------> scheduledTaskQueue

(1)定時任务是延迟7s执行,但是前面的任务顺序执行占用了4s,则在过3s,定时任务就会执行

(2)并不是顺序执行到定时任务的时候,对定时任务延迟7s再执行

(3)这里可以理解为定时任务和自定义任务是并行的

// 二.用户自定义的定时任务

ctx.channel().eventLoop().schedule(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(2 * 1000);

ctx.writeAndFlush(Unpooled.copiedBuffer("hello,服务器耗时长的操作完成,喵4...", CharsetUtil.UTF_8));

System.out.println("taskQueue中处理的线程名:" + Thread.currentThread().getName());

} catch (InterruptedException e) {

System.out.println("发生异常: " + e.getMessage());

}

}

},7, TimeUnit.SECONDS);// 延迟5s执行这个任务(前面的任务顺序执行占用了4s,则在过3s,定时任务就会执行)

System.out.println("go on...");

e39132fd6a40

image-20201028081816943.png

3.系统向多用户推送消息

1.非当前Reactor线程调用Channel的方法

2.推送系统可以根据用户表示找到对应的Channel引用,然后调用通道的eventLoop的execute方法提交到任务队列执行

1.可以用一个集合管理SocketChannel

2.推送消息,可以将业务加入到各个channel对应的NioEventLoop的taskQueue或者scheduledTaskQueue

ctx.channel().eventLoop().execute(new Runnable());

ctx.channel().eventLoop().schedule(Runnable);

4.总结

1.Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作

2.Netty模型基础知识

(1)NioEventLoopGroup 下包含多个 NioEventLoop,每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue

(2)NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道(监听多个 NioChannel)每个 NioChannel 只会绑定在唯一的 NioEventLoop 上

2.1 NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责

2.2 处理的操作耗时较长,则会交给任务队列,异步执行

(3)每个 NioChannel 都绑定有一个自己的 ChannelPipeline(互相包含关系)

十、异步模型

1.基本介绍

(1)异步调用之后,调用者不能马上得到结果,实际上是在处理调用方法之后,通过通知来回调方法

(2)Netty 中的 I/O 操作是都是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。

ChannelFuture cf = bootstrap.bind(6668).sync();

(3)Future-Listener机制,用户可以方便的获取IO操作结果

(4)Netty异步模型建立在future和callback之上的

callback容易理解,就是回调

future:举个栗子,调用一个非常耗时的方法fun,等待结果显示不合适,这时先返回一个Future对象,后续用Future对象监控fun方法的执行状态(Future-Listener机制),获取状态作出相应的处理(回调方法去处理)

2.深入理解Future-Listener机制

#1.ChannelFuture源码

The result of an asynchronous {@link Channel} I/O operation.

解释:是一个异步IO操作的结果

#2.ChannelFuture和JDK1.8新加个FutureTask都是继承与JUC的Future接口,异步回调

(1)ChannelFuture 是一个接口 : public interface ChannelFuture extends Future

(2)future可以添加监听器,当监听的事件发生时,就会通知到监听器.

#3.前端的Ajax也是异步回调,加上Future都是观察者模式(监听--通知---回调)

3.链式操作 + handler的异步操作示意图

e39132fd6a40

image-20201028113418370.png

4.ChannelFuture来获取操作执行的状态

e39132fd6a40

image-20201028161156395.png