在先前的文章《Unix之IO模型》已经讲述到5种IO模型以及对应的同步异步和阻塞非阻塞相关核心概念,接下来看下Java的IO模型在服务端的网络编程中是如何演进,注意这里用启动Java程序表示一个JVM进程,而JVM进程中以多线程方式进行协作,这里讲述以线程为主展开.
BIO与多线程设计
BIO 概述
-
在先前文章中讲述到阻塞式IO是应用进程等待内核系统接收到数据报并将数据报复制到内核再返回的处理过程 -
在Java中的阻塞式IO模型(Blocking IO)网络编程中,服务端 accept
&read
都需要等待客户端建立连接和发起请求才能够进行让服务端程序进行响应,也就是上述的方法在服务端的编程中会让服务端的主线程产生阻塞,当其他客户端与Java服务端尝试建立连接和发请求的时候会被阻塞,等待前面一个客户端处理完之后才会处理下一个客户端的连接和请求,以上是Java的BIO体现
服务端单线程BIO模型
-
单线程图解
-
代码演示
// 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:1的多线程BIO模型
-
根据上述的BIO模型,现优化为主线程接收accept以及通过创建多线程方式处理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();
}
-
运行效果(客户端启动服务端就接收到客户端的连接)
-
分析
基于M:N的线程池实现的BIO模式
-
M:N的线程池实现的图解如下
-
示例代码
// 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));
}
-
分析
NIO设计
-
在《Unix的IO模型》中的NIO模型有非阻塞式IO,IO复用模型以及信号驱动的IO模型,在Java中的NIO模型主要是以非阻塞式IO以及IO复用模型为主. -
从上述的BIO可知,服务端会在accept方法以及read方法调用中导致当前线程处于阻塞状态,结合Unix中的非阻塞式IO可知,NIO本质上是将上述的方法设置为非阻塞,然后通过轮询的方式来检查当前的状态是否就绪,如果是Accept就处理客户端连接事件,如果是READ就处理客户端的请求事件. -
Java实现NIO的方式注意依赖于以下三个核心组件 -
简要的NIO模型图
select()
向操作系统进行事件注册基于单线程通道轮询的NIO模式(NIO模型)
-
这类IO模型与unix下的NIO模型是一致的,就是服务端不断地检查当前的连接状态信息,如果状态信息就绪那么就开始执行相应的处理逻辑
-
NIO图解模型如下
-
在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是自己封装的一个解码工具类,结合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当前对应的描述符的状态信息 -
其简要的示例图如下
-
从上述模型可以看出 -
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));
}
}
}
-
分析
可伸缩性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模式与多线程模式图解如下
老铁们关注走一走,不迷路
原文始发于微信公众号(疾风先生):Java体系之IO设计演进
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25608.html