BIO NIO NIO2(AIO(Asynchronous))
计算机用户空间(进程使用的空间)
应用程序运行在用户空间
计算机内核空间
操作系统和驱动程序运行在内核空间
BIO(blocking I/O)
在java进程(用户空间)向内核空间发起读取请求之后
1.系统内核和硬盘之间的复制过程(内核将硬盘中data copy到内存中)
2.内核空间把数据copy给用户空间的过程
1和2的过程 java进程一直是阻塞状态
BIO加多线程(tomcat)
这种模式具备一定的并发处理能力
BIO+多线程
NIO(nonblocking I/O)
NIO中进程的线程不等待 不工作就去处理其他事情
组成
1.Channel(数据传输双向通道(可读可写)IO多路复用器)
serversocketchannel(服务端管道)和socketchannel(堵塞状态(Accecp),可读或者可写状态)
2.Selector(多路复用器,一个map注册客户端和监听客户端)
3.Buffer(内核与用户空间传输data(可读可写))
capacity(容量), position(起始位置), limit(终止位置)
服务端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server implements Runnable{
//1 多路复用器(管理所有的通道)
private Selector seletor;
//2 建立缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port){
try {
//1 打开路复用器
this.seletor = Selector.open();
//2 打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必须要让多路复用器开始监听
this.seletor.select();
//2 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 进行遍历
while(keys.hasNext()){
//4 获取一个选择的元素
SelectionKey key = keys.next();
//5 直接从容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid()){
//7 如果为阻塞状态
if(key.isAcceptable()){
this.accept(key);
}
//8 如果为可读状态
if(key.isReadable()){
this.read(key);
}
//9 写数据
if(key.isWritable()){
//this.write(key); //ssc
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key){
//ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//ssc.register(this.seletor, SelectionKey.OP_WRITE);
}
private void read(SelectionKey key) {
try {
//1 清空缓冲区旧的数据
this.readBuf.clear();
//2 获取之前注册的socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
//3 读取数据
int count = sc.read(this.readBuf);
//4 如果没有数据
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
this.readBuf.flip();
//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收缓冲区数据
this.readBuf.get(bytes);
//8 打印结果
String body = new String(bytes).trim();
System.out.println("Server : " + body);
// 9..可以写回给客户端数据
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式
sc.configureBlocking(false);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8765)).start();;
}
}
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
//需要一个Selector
public static void main(String[] args) {
//创建连接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
//声明连接通道
SocketChannel sc = null;
//建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
//打开通道
sc = SocketChannel.open();
//进行连接
sc.connect(address);
while(true){
//定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
//把数据放到缓冲区中
buf.put(bytes);
//对缓冲区进行复位
buf.flip();
//写出数据
sc.write(buf);
//清空缓冲区数据
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
区别
同步阻塞、同步非阻塞和异步非阻塞又代表什么意思
阻塞和非阻塞
区别在于IO操作的第一阶段,调用blockingIO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel(内核)还准备数据的情况下会立刻返回。
同步和异步
将数据从kernel拷贝到buf中的过程
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。
BIO
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO的优点
1.客户端发起的连接请求是异步的,可以通过直接在多路复用器注册OP_CONNECT操作等待后续操作,不需要同步阻塞等待结果可用。(key.isValid())
2.SocketChannel的读写都是异步(该异步指的是在用户空间内的读写)的,如果没有可读写的数据它不会同步等待,直接返回,这样IO通信线程可以处理其他链路,不要同步等待这个链路可用。
3.线程模型的优化:由于JDK的Selector在Linux等主流OS中通过epoll实现,没有连接句柄数的限制,这意味着Selector线程可以同时处理成千上万的客户端连接,而且性能不会随着客户端的增加而线性下降,因此非常适合做高性能、高并发的网络服务器。
AIO的优点
它不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写
对比
类型 | BIO | BIO+多线程 | NIO | AIO |
---|---|---|---|---|
客户端个数:IO线程数 | 1:1 | M:N(一般M大于N) | M:1 | M:0(不需要额外启动线程,被动回调) |
阻塞/非阻塞 | 阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
同步/异步 | 同步 | 同步 | 同步 | 异步 |
适用场景
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
NIO2(AIO)方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
延伸
Tomcat Connector三种运行模式(BIO, NIO, APR)的比较和优化
BIO(早期版本采用的BIO和多线程技术)
bio是阻塞式IO操作,使用java io技术,即每一个请求都要创建一个线程来进行处理。缺点:并发量高时,线程数较多,占资源
NIO
使用java nio技术,能够通过少量的线程处理大量的请求
nio是基于java中非阻塞IO操作的API实现,比传统的i/o处理方式有更高的并发运行性能(service.xml文件中修改配置即可)
APR
(Apache Portable Runtime/Apache可移植运行时库)
apr是从操作系统级别解决异步IO问题(JNI技术),大幅度提高服务器的并发处理性能,也是Tomcat生产环境运行的首选方式(修改server.xml还要安装apr和native)
三种运行模式每种都会有各自适用的场合,不能说哪个好那个不好,就像 tomcat 内存方面的配置,如果内存设置的过大,gc 垃圾回收机制就会变慢;如果内存设置的过小,tomcat又会出现内存溢出的情况
在Springboot中内嵌的Tomcat默认启动开启的是NIO模式
可以开启apr模式
实现
nio netty
aio smart-socket