前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Java体系之IO设计演进

Java体系之IO设计演进

作者头像
keithl
发布于 2020-04-07 02:33:21
发布于 2020-04-07 02:33:21
43000
代码可运行
举报
文章被收录于专栏:疾风先生疾风先生
运行总次数:0
代码可运行

在先前的文章《Unix之IO模型》已经讲述到5种IO模型以及对应的同步异步和阻塞非阻塞相关核心概念,接下来看下Java的IO模型在服务端的网络编程中是如何演进,注意这里用启动Java程序表示一个JVM进程,而JVM进程中以多线程方式进行协作,这里讲述以线程为主展开.

BIO与多线程设计

BIO 概述

  • 在先前文章中讲述到阻塞式IO是应用进程等待内核系统接收到数据报并将数据报复制到内核再返回的处理过程
  • 在Java中的阻塞式IO模型(Blocking IO)网络编程中,服务端accept & read 都需要等待客户端建立连接和发起请求才能够进行让服务端程序进行响应,也就是上述的方法在服务端的编程中会让服务端的主线程产生阻塞,当其他客户端与Java服务端尝试建立连接和发请求的时候会被阻塞,等待前面一个客户端处理完之后才会处理下一个客户端的连接和请求,以上是Java的BIO体现

服务端单线程BIO模型

  • 单线程图解
  • 代码演示
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// server.java
// 仅写部分服务端核心代码
ServerSocket server = new ServerSocket(ip, port);
while(true){
	Socket socket = server.accept();	// 接收客户端的连接,会阻塞
	out.put("收到新连接:" + socket.toString());
	// client have connected
	// start read
	BufferedReader br = new BuufferedReader(new InputstreamReader(socket.getInputStream));
	String line = br.readLine();		// 	等待客户端发起请求进行读取操作,会阻塞
	// decode 	..
	// process  ..
	// encode   ..
	// send     ..
}
  • 运行结果(启动两个客户端)
  • 分析

1) 上述代码accept方法以及read方法需要等待客户端发送数据过来,服务端才能从操作系统的底层网卡获取数据,在没有获取数据之前将处于阻塞状态

2) 其次,可以看到上述的服务端程序只能处理一个客户端的连接和请求操作,只有当前的客户端连接以及请求执行完之后才能接收下一个客户端的连接以及请求处理操作

3) 不足: 上述代码压根无法满足服务端处理多客户端的连接和请求,同时造成CPU空闲,尤其是在接收客户端读取的时候,如果此时客户端一直没有发起请求操作,那么其他客户端建立的连接请求根本无法处理,因此对上述进行改进为多线程处理方式

基于1:1的多线程BIO模型

  • 根据上述的BIO模型,现优化为主线程接收accept以及通过创建多线程方式处理IO的读写操作
  • 一个客户端的请求处理交由服务端新创建的一个线程进行处理,主线程仍然处理接收客户端连接的操作
  • 如下图
  • 代码演示
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// thread-task.java
public class IOTask implements Runnable{
	private Socket client;
	
	public IOTask(Socket client){
		this.client = client;
	}

	run(){
		while(!Thread.isInterrupt()){
			// read from socket inputstream
			// encode reading text
			// process
			// decode sent text
			// send
		}
	}
}

// server.java
ServerSocket server = new ServerSocket(ip, port);
while(true){
	Socket client = server.accept();
	out.put(“收到新连接:+ client.toString());
	new Thread(new IOTask(client)),start();
}
  • 运行效果(客户端启动服务端就接收到客户端的连接)
  • 分析

1) 通过多线程的方式将Accept与IO读写操作的阻塞方式进行分离,主线程处理accept接收客户端的连接,新开线程接收客户端的请求进行请求处理操作

2) 上述的方式仅仅是将阻塞的方式进行分离,但是如果处理的客户端数量越来越多的时候,上述服务器创建线程会越来越多,容易造成机器CPU出现100%情况,那么我们可以如何控制线程的方式,在并发编程中,一般通过管理并发处理任务的多线程技术是采用线程池的方式,于是就有了以下的M:N的多线程网络编程的BIO模型

基于M:N的线程池实现的BIO模式

  • M:N的线程池实现的图解如下
  • 示例代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// server.java
ExecutorService executors = Executros.newFixedThreadPool(MAX_THREAD_NUM);
ServerSocket server = new ServerSocket(ip,port);
while(true){
	Socket client = server.accept();
	out.put(“收到新连接:+ client.toString());
	threadPool.submit(new IOTask(ckient));
}
  • 分析

1) 上述运行结果与1:1的线程模型是一致的,但是相比1:1创建线程的方式,充分利用池化技术重复利用线程资源,有助于降低CPU占用的资源

2) 其次,上述的BIO都是属于阻塞式IO处理,每一次的accept操作以及read操作都需要等待客户端的操作才能给予响应,如果客户端不发生操作,那么新创建的线程将一直处于阻塞状态,将占用资源迟迟没有释放,也容易造成CPU发生瓶颈,于是我们想到能否等到客户端有发起相应的操作的时候线程才进行处理呢,在客户端还没有发生请求操作的时候,服务端线程资源是否可以优先处理其他任务,提升CPU利用率呢,这也就是接下来的非阻塞式IO,即Non-Blocking IO

NIO设计

  • 《Unix的IO模型》中的NIO模型有非阻塞式IO,IO复用模型以及信号驱动的IO模型,在Java中的NIO模型主要是以非阻塞式IO以及IO复用模型为主.
  • 从上述的BIO可知,服务端会在accept方法以及read方法调用中导致当前线程处于阻塞状态,结合Unix中的非阻塞式IO可知,NIO本质上是将上述的方法设置为非阻塞,然后通过轮询的方式来检查当前的状态是否就绪,如果是Accept就处理客户端连接事件,如果是READ就处理客户端的请求事件.
  • Java实现NIO的方式注意依赖于以下三个核心组件

1) Channel通道:服务端与客户端建立连接以及进行数据传输的通道,分为ServerSocketChannel(接收客户端的TCP连接通道)以及SocketChannel(建立与服务端的连接通道)

2) Buffer缓存区: 客户端与服务端在channel中建立一个连续数组的内存空间,用于在channel中接收和发送数据数据实现两端的数据通信

3) Selector选择注册器,对比IO复用模型,Selector中包含select函数,用于向系统内核注册网络编程中的Aceept,Read以及Write等事件,相对于从Java而言,是指channel(不论是服务端还是客户端通道)可以向注册器selector发起注册事件,底层交由select()向操作系统进行事件注册

  • 简要的NIO模型图

基于单线程通道轮询的NIO模式(NIO模型)

  • 这类IO模型与unix下的NIO模型是一致的,就是服务端不断地检查当前的连接状态信息,如果状态信息就绪那么就开始执行相应的处理逻辑
  • NIO图解模型如下
  • 在NIO模型图中,accept不断polling客户端是否有建立连接,如果有客户端连接到服务端,这个时候就会将其转发进行IO操作
  • 部分java示例伪代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// server.java
ServerSocketChannel server = ServerSocketChannel.open();
// 设置所有的socket默认伪阻塞,必须设置服务端的通道为非阻塞模式
server.configureBlocking(false);
// 绑定端口以及最大可接收的连接数backlog
server.socket().bind(new InetSocketAddress(PORT), MAX_BACKLOG_NUM);
while(true){
	SocketChannel client = server.accept();
	// 非阻塞获取,所以client可能为null
	if(null != client){
		// 设置客户端的通道为非阻塞
		client.configureBlocking(false);
		// 进行IO操作
		// read
		ByteBuffer req = ByteBuffer.allocate(MAX_SIZE);
		while(client.isOpen() &/& client.read(req)!= -1){
			// BufferCoding是自己封装的一个解码工具类,结合ByteBuffer与Charset使用,这里不演示代码实现
			// decode
			byte[] data = BufferCoding.decode(req);
			if(data != null){
				break;
			}
		}
		// prepared data to send
		sentData = process(data);
		// encode
		ByteBuffer sent = BufferCoding.encode(sentData);
		// write
		client.writeAndFlush(sent);
	}
}
  • 分析

1) 上述的代码与BIO的设计基本无差,只是在原有的基础上设置为非阻塞的操作,然后通过不断轮询的方式不断监控连接和读取操作,与BIO的多线程设计差别不大,只是BIO是多线程方式实现,这里是单线程实现

2) 小结:上述代码使用BIO的API方式,也就是说不断polling的过程都是调用阻塞的API去检查是否就绪的状态,结合先前的Unix的IO模型,非阻塞可以继续改进为给予select的方式来实现,而select不是属于调用阻塞式API而是通过事件轮询的方式等待套接字中的描述符变为就绪状态再进行业务处理操作

基于单线程的select事件轮询IO模式(IO多路复用模型)

  • IO复用模型是通过调用select函数不断轮询获取当前socket的描述符是否就绪,是基于事件的方式实现非阻塞
  • 客户端与服务端都需要注册到selector上,告诉selector当前对哪个描述符感兴趣,再由selector将感兴趣的描述符注册到系统内核中,内核收到一份描述符的数组表,根据网络传输过来的事件告知selector当前对应的描述符的状态信息
  • 其简要的示例图如下
  • 从上述模型可以看出

1) 服务端启动的时候,首先需要创建channel并注册到selector上才能够监听到客户端建立的连接

2) 其次客户端要与服务端建立通信,也需要在客户端自己创建channel并注册到selector上

3) 当selector监听到客户端的连接就会转发给服务端的Accept事件进行处理

4) 当selector监听到客户端发起请求的操作,就会转发给READ事件进行处理,并且如果需要将数据通知客户端,需要在指定的事件上添加写操作

5) 此时selector监听到写操作的时候,就会转发给处理WRITE事件进行处理,并且当前在进行写操作之后取消写操作的事件

  • java实现的伪代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// server.java
ServerSocketChannel server = ServerSocketChannel,open();
server.configureBlocking(false);

Selector selector = Selector.open();
// 服务端只注册ACCEPT,作为接入客户端的连接
// DataWrap封装读写缓存ByteBuffer
server.register(selector, SelectionKey.OP_ACCEPT, server);
server.socket().bind(new InetSocketAddress(PORT), MAX_BACKLOG_NUM);

while(true){
	int key = selector.select();
	if(key == 0) continue;
	// 获取注册到selecor所有感兴趣的事件
	Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> it = keys.iterator();
    while(it.hasNext()){
		SelectionKey key = it.next();
		it.remove();
		if(key.isAcceptable()){
			// 	接收accept事件
			ServerSocketChannel serverChannel = (ServerSocketChannel)key.attachment();
			SocketChannel client = server.accept();
			client.configureBlocking(false);
			// 客户端已经获取到连接,告诉客户端的channel可以开始进行读写操作
			client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, new DataWrap(256, 256));
		}
		
		// read
		if(key.isReadable()){
			//...
			
			// 在事件中添加写操作
			key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
		}
		
		if(key.isWriteable()){
			// ...
			//  成功完成写操作,这个时候取消写操作
          	key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE));
		}
	}
}
  • 分析

1) 上述代码相比第一种方案的实现,主要是采用select函数调用获取注册的事件,阻塞于select方法的调用

2) 另外上述的代码相比第一种稍微更为复杂,操作也更为繁琐

3) 通过上述代码展示,我们也看到NIO的实现方式稍微比BIO复杂一些,都是基于连接线程架构的web服务实现方式,比较依赖于线程,而线程资源更容易造成cpu的瓶颈,因而就有了基于事件驱动设计的web服务,对于IO事件驱动设计主要是基于Reactor模式实现,面向连接与事件的编程方式,此时的线程主要处理IO就绪事件并响应IO事件.

可伸缩性IO设计

单Reactor模式

  • 在一个web体系设计中,处理一个客户端发起的请求需要经过主要包含以下5个核心步骤,即读取请求数据,对请求数据进行协议解析,处理业务逻辑并返回处理结果,将处理的结果封装在协议包中,最后将协议包的数据响应给客户端的请求,通过分散各个步骤,有助于针对每个步骤进行管理和优化,有助于提升程序的伸缩性,相比上述一个IO连接请求对应一个线程处理上述5个步骤的方式,无法分别独立处理.
  • Reactor模式是基于事件驱动设计架构的IO实现技术,通过监听客户端的连接事件来响应对应的IO操作,也就是上述的5个步骤.在Reactor模式将采用分发策略,通过监听的连接就绪事件就将对应的连接分发给每个handler处理器来处理上述的5个IO操作,也就是说每个handler此时处理的IO事件都是就绪的连接事件,这个时候每个连接面向的不是一个线程而是一个IO就绪事件的发生.
  • 单Reactor模式简要图如下

单Reactor模式 + 多线程

  • 如果上述的单Reactor要处理的业务十分耗时,那么使用单线程会导致其他业务处理逻辑一直处于CPU就绪队列无法被执行,这个时候我们可以使用多线程的方式来增加业务的处理能力,提升程序的并发处理能力
  • 在已有的BIO多线程使用经验中,这里的多线程并发技术使用线程池的方式,一来是可以管理和分配线程,二来可以对线程进行重复资源利用,减少上下文切换产生的性能开销,三来当连接十分繁多的时候可以借助线程池的阻塞队列缓冲存储从而避免更多的线程创建销毁开销
  • 单Reactor模式与多线程图解如下

多Reactor模式 + 多线程

  • 相比单个Reactor模式,多Reactor模式主要包含Main Reactor以及Sub Reactor,Main Reaactor主要通过事件轮询的方式监听客户端的连接以及请求,对于新连接的建立将会分发到Acceptor为新建立的客户端连接注册一个监听事件并转发监听请求事件到Sub Reactor中,而Sub Reactor在监听到连接请求的就绪事件时将响应IO事件,开始执行读取,通过多线程的方式提交并处理业务逻辑,最后在Sub Reactor获取业务处理最终结果之后将数据输出到请求的客户端中,在此过程中Sub Reactor是真正响应IO事件,而Main Reacotor主要是接收新的连接并进行注册绑定事件监听,最后分发到下游组件去真正响应IO事件
  • 多Reactor模式与多线程模式图解如下

最后,这里仅展示Web服务的IO设计思路演进,对于Reactor模式设计后面会详细阐述,另外可以思考下,BIO与NIO之间的区别在哪!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疾风先生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
BIO、NIO、IO多路复用模型的演进&Java NIO 网络编程
上文介绍了网络编程的基础知识,并基于 Java 编写了 BIO 的网络编程。我们知道 BIO 模型是存在巨大问题的,比如 C10K 问题,其本质就是因其阻塞原因,导致如果想要承受更多的请求就必须有足够多的线程,但是足够多的线程会带来内存占用问题、CPU上下文切换带来的性能问题,从而造成服务端崩溃的现象。怎么解决这一问题呢?优化呗,所以后面就有了NIO、AIO、IO多路复用。本文将对这几个模型详细说明并基于 Java 编写 NIO。
王二蛋
2024/01/18
7750
『互联网架构』软件架构-io与nio线程模型reactor模型(上)(53)
PS:NIO不需要的代码里面根本没有多线程,实际上nio只有一个工作线程,一个线程可以为多个客人服务。
IT架构圈
2019/05/30
5410
3万字加50张图,带你深度解析 Netty 架构与原理(上)
接下来我们会学习一个 Netty 系列教程,Netty 系列由「架构与原理」,「源码」,「架构」三部分组成,今天我们先来看看第一部分:Netty 架构与原理初探,大纲如下:
烂猪皮
2021/01/28
1K0
3万字加50张图,带你深度解析 Netty 架构与原理(上)
Java网络编程——NIO的阻塞IO模式、非阻塞IO模式、IO多路复用模式的使用
NIO虽然称为Non-Blocking IO(非阻塞IO),但它支持阻塞IO、非阻塞IO和IO多路复用模式这几种方式的使用。
DannyHoo
2022/08/07
5190
Java网络编程——NIO的阻塞IO模式、非阻塞IO模式、IO多路复用模式的使用
深度长文:从bio到nio到aio,再到响应式编程
要问计算机系统里,有哪些概念比较折腾人,nio绝对能算上一个。配合着多是异的网络编程,nio加上多线程一般能够完成双杀。
xjjdog
2021/08/25
7820
高性能IO编程设计
? 首先,在讲述高性能IO编程设计的时候,我们先思考一下何为“高性能”呢,如果自己来设计一个web体系服务,选择BIO还是NIO的编程方式呢?其次,我们可以了解下构建一个web体系服务中,为了能够支撑
keithl
2020/04/07
1.2K0
45 张图深度解析 Netty 架构与原理
接下来我们会学习一个 Netty 系列教程,Netty 系列由「架构与原理」,「源码」,「架构」三部分组成,今天我们先来看看第一部分:Netty 架构与原理初探,大纲如下:
kunge
2020/11/27
25.5K4
Netty序章之BIO NIO AIO演变
Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种NIO框架。而BIO,NIO,AIO更是笔试中要考,面试中要问的技术。也是一个很好的加分项,加分就是加工资,你还在等什么?本章带你细细品味三者的不同! 流程图:
用户1212940
2022/04/13
5360
Netty序章之BIO NIO AIO演变
java的IO模型
本文主要是重新梳理了Java的IO模型,基于之前NIO的文章进行补充,为学习Netty做准备。
贪挽懒月
2020/07/14
7310
Linux IO 模型
先抛出一个问题,基于此问题引出文章的主题:1999 年 Dan Kegel 在其个人站点提出了 C10K问题,首字母 C 是 Client 的缩写,C10K 即单机同时处理 1 万个连接的问题。C10K 表示处理 10000 个并发连接,注意这里的并发连接和每秒请求数不同,虽然它们是相似的,每秒处理许多请求需要很高的吞吐量(快速处理它们),但是更大数量的并发连接需要高效的连接调度,即 I/O 模型的问题。
政采云前端团队
2023/11/14
2800
Linux IO 模型
Java 的 NIO 是如何工作的?
在这个数据爆炸的时代,有大量的数据在系统中流动,一个应用系统的瓶颈往往都是 IO 瓶颈。传统的 javaIO 模型是 BIO,也就是同步阻塞 IO,数据在写入 OutputStream 或者从 InputStream 读取时,如果没有数据没有读到或写完,线程都会被阻塞,处于等待状态,直到数据读取完成或写入完成。而在网络编程中,每一个客户端连接发出后,服务端都会有一个对应线程来处理请求,服务器线程与并发数成 1:1 关系,然而一个服务器所能处理的线程是有限的,处理高并发时就会有问题。
水货程序员
2018/11/13
1.7K0
Redis线程模型的前世今生
众所周知,Redis是一个高性能的数据存储框架,在高并发的系统设计中,Redis也是一个比较关键的组件,是我们提升系统性能的一大利器。深入去理解Redis高性能的原理显得越发重要,当然Redis的高性能设计是一个系统性的工程,涉及到很多内容,本文重点关注Redis的IO模型,以及基于IO模型的线程模型。
2020labs小助手
2021/11/30
3590
1.Java-IO演进之路
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。
云扬四海
2022/05/10
2900
1.Java-IO演进之路
IO事件驱动设计实现
在web体系中,相比线程连接架构设计而言,事件驱动设计更满足我们实现一个高性能IO的web服务,这点在高性能IO编程一文已经有讲述.对此,我们接下来将要展开如何去设计一个基于IO事件驱动架构的web服务,目前有Reactor同步多路复用模式以及Proactor异步多路复用模式两种方案,通过后面文章的分析,我们可以了解到这两种方案的设计思路,具体实现原理以及这两种模式各自的优势以及不足.
keithl
2020/04/21
1.1K0
IO事件驱动设计实现
浅析 Java NIO
现在使用 NIO 的场景越来越多,很多网上的技术框架或多或少的使用 NIO 技术,譬如 Tomcat、Jetty、Netty,学习和掌握 NIO 技术已经不是一个 Java 攻城狮的加分技能,而是一个必备技能。
Java极客技术
2023/09/02
3830
浅析 Java NIO
Java NIO
通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。传统的 Server/Client模式如下图所示:
conanma
2021/12/06
4520
Netty之旅:你想要的NIO知识点,这里都有!
这段时间也一直在学习Netty相关知识,因为涉及知识点比较多,也走了不少弯路。目前网上关于Netty学习资料玲琅满目,不知如何下手,其实大家都是一样的,学习方法和技巧都是总结出来的,我们在没有找到很好的方法之前不如按部就班先从基础开始,一般从总分总的渐进方式,既观森林,又见草木。
一枝花算不算浪漫
2020/08/10
5450
一文了解Java的IO模型
我们都知道在 Java 当中有许许多多的使用上的问题,比如 Java 的锁,Java 的安全性,以及 Java 的IO操作,Java 中各种设计模式的使用,今天我们就来说说关于这个 Java 的IO。
Java极客技术
2024/01/31
6880
一文了解Java的IO模型
Java IO: BIO, NIO, AIO
BIO, NIO, AIO,本身的描述都是在Java语言的基础上的。 而描述IO,我们需要从三个层面:
九州暮云
2019/08/21
6930
深入探索Java BIO与NIO输入输出模型:基于文件复制和socket通信
Java BIO是一种同步阻塞的I/O模型,它是Java最早提供的I/O模型。在进行读写操作的时候,若使用BIO进行通信,则操作不再受到操作系统的控制,而是由应用程序自己控制。在BIO中,数据的读取写入必须阻塞在一个线程内等待其完成。
公众号:码到三十五
2024/03/19
1720
深入探索Java BIO与NIO输入输出模型:基于文件复制和socket通信
相关推荐
BIO、NIO、IO多路复用模型的演进&Java NIO 网络编程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验