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

Java体系之IO设计演进

作者头像
keithl
发布2020-04-07 10:33:21
4060
发布2020-04-07 10:33:21
举报
文章被收录于专栏:疾风先生疾风先生

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

BIO与多线程设计

BIO 概述

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

服务端单线程BIO模型

  • 单线程图解
  • 代码演示
代码语言:javascript
复制
// 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
复制
// 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
复制
// 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
复制
// 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
复制
// 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 删除。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档