Java体系之IO设计演进


点击上方疾风先生可以订阅哦

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



BIO与多线程设计

BIO 概述

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

服务端单线程BIO模型

  • 单线程图解

Java体系之IO设计演进

  • 代码演示
// 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 ..
}
  • 运行结果(启动两个客户端) 

Java体系之IO设计演进

Java体系之IO设计演进

Java体系之IO设计演进

  • 分析
  • 1) 上述代码accept方法以及read方法需要等待客户端发送数据过来,服务端才能从操作系统的底层网卡获取数据,在没有获取数据之前将处于阻塞状态
    2) 其次,可以看到上述的服务端程序只能处理一个客户端的连接和请求操作,只有当前的客户端连接以及请求执行完之后才能接收下一个客户端的连接以及请求处理操作
    3) 不足: 上述代码压根无法满足服务端处理多客户端的连接和请求,同时造成CPU空闲,尤其是在接收客户端读取的时候,如果此时客户端一直没有发起请求操作,那么其他客户端建立的连接请求根本无法处理,因此对上述进行改进为多线程处理方式

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

  • 根据上述的BIO模型,现优化为主线程接收accept以及通过创建多线程方式处理IO的读写操作

  • 一个客户端的请求处理交由服务端新创建的一个线程进行处理,主线程仍然处理接收客户端连接的操作

  • 如下图 

Java体系之IO设计演进

  • 代码演示
// 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();
}
  • 运行效果(客户端启动服务端就接收到客户端的连接) 

Java体系之IO设计演进

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

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

  • M:N的线程池实现的图解如下 

Java体系之IO设计演进

  • 示例代码
// 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模型图 

Java体系之IO设计演进

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

  • 这类IO模型与unix下的NIO模型是一致的,就是服务端不断地检查当前的连接状态信息,如果状态信息就绪那么就开始执行相应的处理逻辑

  • NIO图解模型如下 

Java体系之IO设计演进

  • 在NIO模型图中,accept不断polling客户端是否有建立连接,如果有客户端连接到服务端,这个时候就会将其转发进行IO操作

  • 部分java示例伪代码

// 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是自己封装的一个解码工具类,结合ByteBufferCharset使用,这里不演示代码实现
// 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当前对应的描述符的状态信息
  • 其简要的示例图如下 

Java体系之IO设计演进

  • 从上述模型可以看出
  • 1) 服务端启动的时候,首先需要创建channel并注册到selector上才能够监听到客户端建立的连接
    2) 其次客户端要与服务端建立通信,也需要在客户端自己创建channel并注册到selector上
    3) 当selector监听到客户端的连接就会转发给服务端的Accept事件进行处理
    4) 当selector监听到客户端发起请求的操作,就会转发给READ事件进行处理,并且如果需要将数据通知客户端,需要在指定的事件上添加写操作
    5) 此时selector监听到写操作的时候,就会转发给处理WRITE事件进行处理,并且当前在进行写操作之后取消写操作的事件
  • java实现的伪代码
// 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模式简要图如下

Java体系之IO设计演进

单Reactor模式 + 多线程

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

Java体系之IO设计演进

多Reactor模式 + 多线程

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

Java体系之IO设计演进


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



Java体系之IO设计演进
你好,我是疾风先生,先后从事外企和互联网大厂的java和python工作, 记录并分享个人技术栈,欢迎关注我的公众号,致力于做一个有深度,有广度,有故事的工程师,欢迎成长的路上有你陪伴,关注后回复greek可添加私人微信,欢迎技术互动和交流,谢谢!

老铁们关注走一走,不迷路



      有用就点个好看  

原文始发于微信公众号(疾风先生):Java体系之IO设计演进

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25608.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!