Netty(1)线程模型【常识】

一、Netty简介

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
在这里插入图片描述

二、线程模型

当前存在的线程模型:

  • 传统阻塞IO模型
  • Reactor模型

根据 Reactor模型中的 Reactor 数量以及处理资源线程池的大小,分3种:

  • 单Reactor 单线程
  • 单Reactor 多线程
  • 主从Reactor 多线程

Netty 主要基于 主从Reactor 多线程模型做出了改进,主从Reactor多线程模型有多个Reactor。

2.1 传统IO

在这里插入图片描述传统IO 问题:

  • 一个请求一个线程,并发上不去
  • 每个线程都会阻塞在read

解决办法:

  • 不能来一个请求创建一个线程,可以搞线程池复用线程。一个线程处理多个连接
  • 不能阻塞在read死等,而是要 IO复用。也就是,多个线程复用同一个阻塞对象,应用只在这一个阻塞对象等待,无需等待所有链接。某链接新来数据可以处理时,OS通知应用,线程从阻塞状态返回处理业务。

2.2 Reactor

这就是 Reactor 的线程模型:
这里的 ServiceHandler 就是 Reactor ,可以比作 电话接线员;
这里的 EventHandlers 就像是 接线员转接到的、真正处理任务的人员。

Reactor 的翻译,五花八门,比如:
a. 反应者模式
b. Dispatcher模式( 服务端将传递进来的多个请求,分发到多个 线程处理)
c. Notifier 模式,其实都是一个意思
在这里插入图片描述

2.3 单Reactor单线程

这个图在看的时候,要注意 线程和对象的区别(右下方说明)。
这种模型, 只有一个Reactor 线程,也就是说, select 、dispatch、accept、read … 这些操作都要靠Reactor 来搞定,实战根本没法用。

在这里插入图片描述

  • 这里的select 是 IO复用模型中的 标准API,可以实现程序通过一个阻塞对象监听多路连接请求
  • Reactor对象通过select 监控客户端请求事件,收到事件后通过dispatch分发
  • select 的请求中,一种是 建立连接的,由acceptor 处理,然后创建一个handler对象处理连接完成后的后续业务处理。
  • 如果不是建立连接的,Reactor 分发调用 handler 对象处理连接完成后的业务

2.4 单Reactor 多线程模型

在这里插入图片描述
这里比较重要的是:

  • Handler 只负责响应事件,不做具体业务处理, Handler 通过 read读取数据后,会分发给后面的 worker 线程池的某个线程处理
  • worker线程池的线程处理完业务后,会将结果返回给 handler。【这点要十分注意!否则 处理结果怎么成为 响应返回给客户端呢? 如此,逻辑就断层了】

这种模型的性能瓶颈很可能出现在 reactor 主线程的处理能力上,高并发情况下,分发速度可能跟不上。

2.5 主从Reactor多线程

在这里插入图片描述
上文提到,一个reactor 单线程可能成为 性能瓶颈,那主从Reactor多线程的示意图中, SubReactor 只有一个线程,会不会也成为瓶颈。
这里需要解释下,实际上, Reactor 子线程是可以有多个的,这里仅仅是篇幅限制所以从简画。

有几个关键点:

  • main reactor 、 subreactor 数据是单向流动的,前者分发请求后, subreactor完成后续的所有处理工作,subreactor 无需返回数据给 main reactor

使用场景:

  • Nginx主从reactor 多进程模型
  • Memcache 主从 Reactor 多线程
  • Netty 主从Reactor 多线程模型

2.6 小结一下

在这里插入图片描述

  • 扩展性怎么理解?
    说白了,就是subreactor 可以有很多个,而不是图示中的一个。
  • 响应快 ,不为单个同步时间阻塞, 怎么理解?
    说白了,多个subcreator 中其中一个处理请求即可,一个在阻塞,另一个线程来处理即可

三、Netty线程模型【重点内容】

在这里插入图片描述
这幅图十分重要 ,它描述了Netty 的线程模型整体轮廓。按图示,BossGroup 、WorkGroup 只是个通俗说法,本质就是NioEventLoopGroup对象,用大白话说,就是 装了一定数量 NioEventLoop 对象的 桶。NioEventLoop 就是真正干活的对象。

  • BossGroup 中的每个 NioEventLoop:

    • 1、轮询accept事件
    • 2、处理accept事件 ,与 client建立连接,生成NioSocketChannel ,并将其注册到 某个worker group中的NioEventLoop 的selector
    • 3、处理任务队列的任务 ,即 runAllTasks
  • WorkerGroup中的每个 NioEventLoop

    • 1、轮询 read/write
    • 2、处理IO事件 (read/write 事件),在对应 NioSocketChannel上处理
    • 3、处理队列任务,即 runAllTasks

四、Netty 任务队列

先说下场景是什么呢?

如果我们在server handler 中需要做过长时间的操作,会阻塞当前线程,造成 对客户端响应时间增长。抽象成代码就是:

public class EchoServerHandler extends ChannelInboundHandlerAdapter {


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

		System.out.println("channelRead thread:" + Thread.currentThread().getName());
		try {
			//模拟长时间操作:阻塞5S
			TimeUnit.SECONDS.sleep(5);
			ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}

}

这段代码里的 sleep 会阻塞住当前线程,因此要考虑异步化 【使用任务队列】

下面来说下 任务队列 的常见三种使用场景

1、用户自定义的普通任务 (任务队列是 字段taskQueue)
2、用户自定义的定时任务 (任务队列是 字段 scheduleQueue)
3、非当前reactor 线程调用 channel
【第三种在推送系统中经常使用到:根据 用户的ID,找到对应的channel引用,然后调用 write 方法向该用户推送消息。最终的write 会提交到任务队列后 被异步消费

注意这里说的阻塞,都是针对 ChannelRead() 方法会不会 被阻塞住的!!!
A: 按方式一改造后的代码:


public class EchoServerHandler extends ChannelInboundHandlerAdapter {


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

		System.out.println("channelRead thread:" + Thread.currentThread().getName());
		Runnable runnable = () -> {
			//模拟长时间操作:阻塞5S
			try {
				TimeUnit.SECONDS.sleep(5);

				System.out.println("write-and-flush thread :" + Thread.currentThread().getName());

				ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));

			} catch (Exception e) {
				e.printStackTrace();
			}
		};
		
		// 其实这是个伪异步, 由于执行 这个channelRead 、以及两次 runnable 的都是同一个线程,事实上这种方式
		// 还是会阻塞 这个 channel 对应的线程。 不过,这种做法可以让 ChannelRead 先执行完,然后再执行 Runnable 
		
		// 提交第一次任务
		ctx.channel().eventLoop().execute(runnable);
		
		// 提交第二次任务
		ctx.channel().eventLoop().execute(runnable);
	}


	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}

}

B : 按方式二改造代码 【同上,这里channelRead 不会会阻塞住了, schedule 中的任务会在 channelRead 被执行完之后才会被执行


public class EchoServerHandler extends ChannelInboundHandlerAdapter {


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

		System.out.println("channelRead thread:" + Thread.currentThread().getName() + " now it is: " + CommonUtil
				.toCurrentTimeStr());
		Runnable runnable = () -> {
			//模拟长时间操作:阻塞5S
			try {

				System.out.println(
						"write-and-flush thread :" + Thread.currentThread().getName() + " now it is: " + CommonUtil
								.toCurrentTimeStr());

				ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));

			} catch (Exception e) {
				e.printStackTrace();
			}
		};

		// 其实这是个伪异步, 由于执行 这个channelRead 、以及两次 runnable 的都是同一个线程,事实上这种方式
		// 还是会阻塞 这个 channel 对应的线程

		// 提交第一次任务
		ctx.channel().eventLoop().schedule(runnable, 10, TimeUnit.SECONDS);

		// 提交第二次任务
		ctx.channel().eventLoop().schedule(runnable, 5, TimeUnit.SECONDS);
	}


	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}

}

C. 方式 三 代码略
其中有个比较简单的做法是在下面代码中获取到 SocketChannel 的引用,然后用Map 缓存起来


.childHandler(new ChannelInitializer<SocketChannel>() {
						protected void initChannel(SocketChannel ch) throws Exception {
							ch.pipeline().addLast(new EchoServerHandler());
						}
					})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值