1. BIO和NIO

我们平常使用的IO是BIO(Blocking-IO),即阻塞IO、而NIO(No-blocking-IO)则是非阻塞IO,二者有什么区别呢?

预先知识准备

  • 同步:发起调用后,调用者一直处理任务至结束后才返回结果,期间不能执行其他任务
  • 异步:发起调用后,调用者立即返回结果的标记(当结果出来后用回调等机制通知),期间可以执行其他任务
  • 阻塞:发起请求后,发起者一直等待结果返回,期间不能执行其他任务
  • 非阻塞:发起请求后,发起者不用一直等待结果,期间可以执行其他任务
  • IO模式有五种(BIO、NIO、信号驱动IO、多路复用IO、异步IO)这里介绍同步阻塞和同步非阻塞IO,而剩下的后面回来填坑

NIO主要体现在网络IO中,所以下面就围绕网络IO来说明,这里会涉及到传统的BIO、网络编程、反应器设计模式,如果不了解的童鞋这里有各自的传送门 BIO ,[未完善]

二者区别

BIO NIO
类型 同步阻塞 同步非阻塞
面向 面向流 面向缓冲区
组件 选择器

若没有了解过NIO,那么列出的区别只需有个印象即可,后面会逐步说明

2.BIO

2.1 传统BIO

传统的IO其读写操作都阻塞在同一个线程之中,即在读写期间不能再接收其他请求

那么我们就来看看传统BIO是怎么实现的,后面都以网络编程的Socket为例,因其与后面的NIO有关

public class BIO {

	public static void main(String[] args) throws IOException {

		// 开个线程运行服务器端套接字
new Thread( () -> {
try {
// 建立服务器端套接字
ServerSocket serverSocket = new ServerSocket(8080);
// 该方法阻塞至有请求过来
Socket socket = serverSocket.accept(); // 获取输入流
int length = 0;
byte[] bytes = new byte[1024];
InputStream in = socket.getInputStream(); // 客户端关闭输出流这里才会读取到-1,shutdownOutput或者close
while( (length = in.read(bytes)) != -1){
System.out.println(new String(bytes,0,length));
} System.out.println("这里服务器端处理任务花费了10秒");
Thread.sleep(10000); // 获取输出流
OutputStream out = socket.getOutputStream();
out.write( ("这里是服务器端发送给客户端的消息: " + new Date()).getBytes() ); // 关闭资源
in.close();
out.close();
socket.close();
serverSocket.close();
} catch (Exception e) {
}
}).start(); // 开个线程运行客户端套接字
new Thread( () -> {
try {
// 建立客户端套接字
Socket socket = new Socket("127.0.0.1",8080); // 获取输出流
OutputStream out = socket.getOutputStream();
out.write( ("这里是客户端发送给服务器端的消息:" + new Date()).getBytes() );
// 关闭输出流,让服务器知道数据已经发送完毕,剩下接收数据了
socket.shutdownOutput(); // 获取输入流
int length = 0;
byte[] bytes = new byte[1024];
InputStream in = socket.getInputStream();
while( (length = in.read(bytes)) != -1){
System.out.println(new String(bytes,0,length));
} // 关闭资源,若没有关闭则会保持连接至超时,单线程服务器端就不能接收后来的连接请求
out.close();
in.close();
socket.close();
} catch (Exception e) {
}
}).start(); }
}
这里是客户端发送给服务器端的消息:Sat Feb 08 15:14:55 GMT+08:00 2020
这里服务器端处理任务花费了10秒
这里是服务器端发送给客户端的消息: Sat Feb 08 15:15:05 GMT+08:00 2020
  • 从输出可以看出,客户端会一直等待阻塞直至服务器端返回内容

  • 服务器端的accept()方法会阻塞当前线程,直至有请求发送过来才会继续accept()方法下面的代码

  • 服务器端接收到一个请求后且该请求还没处理完,后又再有一个请求过来,则后来的请求会被阻塞需排队等待

  • 客户端打开输出流若没关闭,则服务器端是不知道客户端数据已经发送完,会一直等待至超时 ,关闭方法:

    • 客户端socket.close(),整个连接也关闭了
    • 客户端socket.shutdownOutput(),单方面关闭输出流,不关闭连接
    • 客户端的outputStream.close(),会造成socket被关闭

2.2 伪异步BIO

传统的BIO是单线程的,一次只能处理一个请求,而我们可以改进为多线程,即服务器端每接收到一个请求就为该请求单独创建一个线程,而主线程还是继续监听是否有请求过来,伪异步是因为accept方法到底还是同步的

public class SocketTest {

	// 定义线程接口
class MyRunnable implements Runnable{ @Override
public void run(){
try {
Socket socket = new Socket("127.0.0.1",8080); // 获取输出流
OutputStream out = socket.getOutputStream();
out.write( ("这里是客户端发送给服务器端的消息:" + new Date()).getBytes() );
// 关闭输出流,让服务器知道数据已经发送完毕,剩下接收数据了
socket.shutdownOutput(); // 获取输入流
int length = 0;
byte[] bytes = new byte[1024];
InputStream in = socket.getInputStream();
while( (length = in.read(bytes)) != -1){
System.out.println(new String(bytes,0,length));
} // 关闭资源
out.close();
in.close();
socket.close();
} catch (Exception e) {
}
}
} public static void main(String[] args) throws IOException, InterruptedException { // 开个线程运行服务器端套接字
new Thread( () -> {
try {
// 建立服务器端套接字
ServerSocket serverSocket = new ServerSocket(8080); // 循环接收请求
while(true){
Socket socket = serverSocket.accept(); // 为每个请求单独开线程,这里就不那么复杂使用线程池了
new Thread( () -> {
try {
// 获取输入流
int length = 0;
byte[] bytes = new byte[1024];
InputStream in = socket.getInputStream();
while( (length = in.read(bytes)) != -1){
System.out.println(new String(bytes,0,length));
} // 获取输出流
OutputStream out = socket.getOutputStream();
out.write( ("这里是服务器端发送给客户端的消息: " + new Date()).getBytes() ); // 关闭资源
in.close();
out.close();
socket.close();
} catch (Exception e) {
// TODO: handle exception
}
}).start();
}
} catch (Exception e) {
}
}).start(); // 创建多线程,调用类中接口(为了偷懒写成这样了。。。)
// 关注点在于伪异步,像线程计数器,,线程池,Lambda表达式也尽量少用,使代码易懂
SocketTest socketTest = new SocketTest();
MyRunnable myRunnable = socketTest.new MyRunnable();
new Thread(myRunnable).start();
new Thread(myRunnable).start();
new Thread(myRunnable).start(); }
}
这里是客户端发送给服务器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
这里是服务器端发送给客户端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
这里是客户端发送给服务器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
这里是客户端发送给服务器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
这里是服务器端发送给客户端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
这里是服务器端发送给客户端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
  • 服务器端每来一个请求就为之单独创建线程来处理任务,使主线程可以继续循环接收请求
  • 客户端的请求之间就互不干扰了,不用等待上一个请求处理完才处理下一个
  • 其本质还是同步,使用了多线程才实现异步功能
  • 使用多线程,若在多高并发情况下,会大量创建线程而导致内存溢出(可以使用线程池优化,但有界限池终究不是办法)

3. NIO

  • 看了上面那么多铺垫,终于到我们的正题了。NIO主要使用在网络IO中,当然文件IO也有使用,NIO在高并发的网络IO中有极大的优势,其在JDK1.4中引入,以我们传统再传统的开发环境--1.7中可以使用了
  • 在单线程中,NIO在写读数据的时候可以同时执行其他任务,不必等数据完全读写而导致阻塞(后面有地方说明)

NIO的组成

  • Buffer(缓冲区)
  • Channel(通道)
  • Selector(选择器)

那么我们就来看看NIO的三个组成把

3.1 Buffer

NIO是面向缓冲区的,一次处理一个区的数据,在NIO中我们都是使用缓冲区来处理数据,即数据的读入或写出都要经过缓冲区

缓冲区的类型有:

  • ByteBuffer、

  • ShortBuffer、

  • IntBuffer、

  • LongBuffer、

  • FloatBuffer、

  • DoubleBuffer、

  • CharBuffer

最常用是ByteBuffer,记住后面要用到,可使用静态方法获取缓冲区:ByteBuffer.allocate(1024)

Buffer类中主要的方法:

返回类型 函数 解释
XXXBuffer allocate(int capacity) 返回指定容量的缓冲区
ByteBuffer put(byte[] src) 向缓冲区添加字节数组
ByteBuffer get(byte[] dst) 向缓冲区获取字节数组
XXXBuffer flip() 切换成读模式
XXXBuffer clear() 清除此缓冲区

其内部维护了几个变量

// Invariants: mark <= position <= limit <= capacity
private int mark = -1; // 标记这里不讲解
private int position = 0; //位置
private int limit; // 限制
private int capacity; // 容量大小

变量的变化:

初始化时:position为0,limit和capacity为容量大小,且capacity不变化,后面省略

put数据时:position为put进去数据大小(如放进5字节数据,则position=5),其余不变,正常默认为写模式

切换读模式:limit赋值为position的当前值,而position赋值为0

get数据时:读取多少个数据,position就前进几个位置

清空:调用clear(),变量变为初始化状态,即position为0,limit为容量大小

3.2 Channel

通道主要是传输数据的,不进行数据操作,并且与流不同可以前后移动,而且通道是双向的读写的,最重要的是Channel只能与Buffer交互,所以要使用NIO就要用Channel和Buffer来配合

其类型包括:

  • FileChannel
  • DatagramChannel
  • SocketChannel:
  • ServerSocketChannel

可以看出NIO主要支持网络IO及文件IO,可通过静态方法获取:ServerSocketChannel.open(),然后通过ServerSocketChannel.socket()获取对应的套接字,套接字的获取通道方法前提是已经绑定了通道才行,不然空指针

通道的主要方法:

类型 函数名 解释
ServerSocketChannel open 返回对应的通道
int read(ByteBuffer dst) 从该通道读取到给定缓冲区的字节序列
int write(ByteBuffer src) 从给定的缓冲区向该通道写入一个字节序列
ServerSocketChannel bind(SocketAddress local) 将通道的套接字绑定到本地,设为监听连接
SelectableChannel configureBlocking(Boolean bool) 设置通道的阻塞模式
SelectionKey register(Selector sel, int ops) 将通道注册到选择器

配合Channel和Buffer来简单实现数据流通

int length = 0;
while( (length = inChannel.read()) != -1 ){
buffer.flip(); //切换读模式
outChannel.write(buffer); // 数据写入通道
buffer.clear(); // 清空缓冲区,实现可再写入
}

3.3 Selector

NIO特有的组件(选择器容器),注意只有在网络IO中才具有非阻塞性,网络IO中的套接字的通道才有非阻塞的配置。使用单线程通过Selector来轮询监听多个Channel,在IO事件还没到达时不会陷入阻塞态等待。划重点:传统BIO在事件还没到达时该线程会被阻塞而等待,一次只能处理一个请求(可以使用多线程来提高处理能力)。而NIO在事件还没到达是非阻塞轮询监听的,一次可以处理多个事件。使用一个线程来处理多个事件明显比一个线程处理一个事件更优秀,获取选择器:Selector.open()

选择器主要方法:

类型 方法名 解释
void close 关闭此选择器
Selector open 打开选择器
int select 选择一组准备好的IO键
Set selectedKeys 返回选择器的键集

这里补充一下注册通道时返回的键的方法

XXXChannel channel 返回键对应的通道,类似于句柄
boolean isAcceptable 键对应的通道是否准备好了
boolean isReadable 键对应的通道是否可读
boolean isWritable 键对应的通道是否可写

3.4 使用事例

综合上面BIO的 2.1和 2.2的代码,客户端基本不用改动,使用多线程来模拟多次请求,而重点改造在于服务器端

这里的服务器端用单线程来处理请求,即一对多使用了多路复用。若是BIO单线程则会阻塞,即一请求一应答

public class NIOTest {0.

	// 定义线程接口
class MyRunnable implements Runnable{ @Override
public void run(){
try {
Socket socket = new Socket("127.0.0.1",8080); // 获取输出流
OutputStream out = socket.getOutputStream();
out.write( ("这里是客户端发送给服务器端的消息:" + new Date()).getBytes() );
// 关闭输出流,让服务器知道数据已经发送完毕,剩下接收数据了
socket.shutdownOutput(); // 获取输入流
int length = 0;
byte[] bytes = new byte[1024];
InputStream in = socket.getInputStream();
while( (length = in.read(bytes)) != -1){
System.out.println(new String(bytes,0,length));
} // 这里故意不关闭资源,保持连接
// 如果是BIO单线程,没有断开连接,则会阻塞后面的请求
// 而NIO则不会阻塞,因为是多路复用 } catch (Exception e) {
}
}
} public static void main(String[] args) throws UnknownHostException, IOException { // 开个线程运行服务器端套接字
new Thread( () -> { try {
// 静态方法获取选择器
// 开启选择器的线程会被选择器阻塞,所以要另开一个线程执行
Selector selector = Selector.open(); // 获取服务器端通道并配置非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
serverSocketChannel.bind(inetSocketAddress); // Selector管理Channel,则需将对应的channel注册上去,且指定类型
// 将服务器通道注册到选择器上,注册为accept
// 可频道为:一看能看出来不解释了
/**
* SelectionKey.OP_CONNECT
* SelectionKey.OP_ACCEPT
* SelectionKey.OP_READ
* SelectionKey.OP_WRITE
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while(true){
// 轮询监听是否有准备好的连接
if(selector.select() > 0){
// 获取键集
Set<SelectionKey> set = selector.selectedKeys();
Iterator iterator = set.iterator();
// 迭代器迭代
while(iterator.hasNext()){
SelectionKey selectionKey = (SelectionKey) iterator.next(); // 接收连接事件
if(selectionKey.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept(); // 设置客户端通道为非阻塞,不然选择器会被阻塞,其存在没有意义了
socketChannel.configureBlocking(false); // 将客户端通道注册到选择器上,使选择器可以统一管理
socketChannel.register(selector, SelectionKey.OP_READ); // 处理可读事件
}else if(selectionKey.isReadable()){
// 通过key来获取通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); // 配合缓冲区
ByteBuffer bytebuffer = ByteBuffer.allocate(1024); int length = 0;
byte[] bytes = new byte[1024];
while( (length = socketChannel.read(bytebuffer)) != -1){
bytebuffer.flip(); // 将缓冲区数据放入字节数组,并输出
bytebuffer.get(bytes, 0, length);
System.out.println(new String(bytes,0,length)); bytebuffer.clear();
}
}
// 取消选择键,因为有些已经处理了
iterator.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start(); // 调用类中接口,创建多线程(为了偷懒写成这样了。。。)
NIOTest NIOTest = new NIOTest();
MyRunnable myRunnable = NIOTest.new MyRunnable();
new Thread(myRunnable).start();
new Thread(myRunnable).start();
new Thread(myRunnable).start(); }
}
  • 上面客户端故意不关闭连接,未超时情况下也能处理多请求,则说明NIO是非阻塞的,最大好处就在于这里

总结

挖坑:AIO异步的IO,基于网络编程的Netty框架,越来越多的坑要填了 —_—!

BIO、NIO的更多相关文章

  1. Java BIO、NIO、AIO 学习(转)

    转自 http://stevex.blog.51cto.com/4300375/1284437 先来个例子理解一下概念,以银行取款为例: 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Ja ...

  2. BIO、NIO与NIO.2的区别与联系

    BIO.NIO.NIO.2之间的区别主要是通过同步/异步.阻塞/非阻塞来进行区分的 同步: 程序与操作系统进行交互的时候采取的是问答的形式 异步: 程序与操作系统取得连接后,操作系统会主动通知程序消息 ...

  3. Java BIO、NIO、AIO-------转载

    先来个例子理解一下概念,以银行取款为例: 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写). 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Ja ...

  4. JAVA中IO技术:BIO、NIO、AIO

    1.同步异步.阻塞非阻塞概念        同步和异步是针对应用程序和内核的交互而言的. 阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作 ...

  5. Java IO 之 BIO、NIO、AIO

    1.BIO.NIO.AIO解释 Java BIO : 同步并阻塞 (Blocking IO) 一个连接一个线程 即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不 ...

  6. Tomcat在Linux服务器上的BIO、NIO、APR模式设置

    一.BIO.NIO.AIO 先了解四个概念: 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写). 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时, ...

  7. tomcat并发优化之三种接收处理请求方式(BIO、NIO、APR)介绍

    原文链接:http://blog.csdn.net/xyang81/article/details/51502766 Tomcat支持三种接收请求的处理方式:BIO.NIO.APR 1>.BIO ...

  8. Java提高班(五)深入理解BIO、NIO、AIO

    导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别:BIO.NIO.AIO 的区别:理解和实现 NIO 操作 Socket 时的多路复用:同时掌握 IO 最底层最核心的操作技巧. BIO.NIO ...

  9. Java网络通信方面,BIO、NIO、AIO、Netty

    码云项目源码地址:https://gitee.com/ZhangShunHai/echo 教学视频地址:链接: https://pan.baidu.com/s/1knVlW7O8hZc8XgXm1dC ...

  10. 以Java的视角来聊聊BIO、NIO与AIO的区别?

    转: 以Java的视角来聊聊BIO.NIO与AIO的区别? 飞丫玲丫 17-07-2623:10 题目:说一下BIO/AIO/NIO 有什么区别?及异步模式的用途和意义? BIO(Blocking I ...

随机推荐

  1. 【他山之石】IntelliJ Idea 内存设置

    最近一次使用idea,删掉target目录内容,准备让项目重新编译的时候,整个mac系统崩溃然后黑屏重启了.紧接着就是重启后自动恢复原先打开的程序,结果再次黑屏重启.最开始以为是系统问题,还怀疑过最近 ...

  2. 使用SparkSql进行表的分析与统计

    # 背景 ​ 我们的数据挖掘平台对数据统计有比较迫切的需求,而Spark本身对数据统计已经做了一些工作,希望梳理一下Spark已经支持的数据统计功能,后期再进行扩展. # 准备数据 在参考文献6中下载 ...

  3. @程序员,你们还在用网上乱找的方法导入导出Excel么,我们给你造了个轮子!!!!!

    程序员的显著特点 有一天跟一位同事跟我闲聊,讨论起过去若干年软件行业的感受,他问了个问题:你觉得一个好的软件工程师最显著的特点是什么? 我想了一会,说:大概是坐得住吧. 某种意义上来说,在互联网技术飞 ...

  4. 【题解】PKUWC2018简要题解

    [题解]PKUWC2018简要题解 Minimax 定义结点x的权值为: 1.若x没有子结点,那么它的权值会在输入里给出,保证这类点中每个结点的权值互不相同. 2.若x有子结点,那么它的权值有p的概率 ...

  5. 【题解】AcWing 110. 防晒(普及题)

    [题解]AcWing 110. 防晒(普及题) AcWing 110. 防晒 你没有用过的全新OJ 嘿嘿水水题. 题目就是一维坐标轴上给定多个线段,给定多个点,点在线段上造成贡献,点可以重复,问最大贡 ...

  6. $loj530\ [LibreOJ\ \beta\ Round \#5]$ 最小倍数 数论

    正解:数论 解题报告: 传送门$QwQ$! 不想做题,来水点儿简单点的$QwQ$. 一个显然的点在于可以直接对不同质因子分别算$n_{min}$最后取$max$. 这个正确性还是蛮显然的?因为只要有$ ...

  7. 「Luogu P3183」[HAOI2016]食物链 解题报告

    身为一个蒟蒻,由于刷不过[NOI2001]食物链 于是出门左转写了道另一道假的食物链 戳这里 这里的食物链个条数其实就是有向图的路径数(应该是这么说吧,我弱) 思路: 拓扑(Topulogy)(一本正 ...

  8. 小小知识点(三十)集中式大规模和无小区大规模MIMO

    集中式大规模MIMO 同一小区的所有接入点( access point,AP) 布置在同一个基站( base station,BS) 中,并且 AP 之间的间距非常小,这种布置方式称为集中式大规模MI ...

  9. MongoDB 上手开发实践(入门上手开发这一篇就够了)

    前言 MongoDB是一个介于 关系数据库 和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的.它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较复 ...

  10. java中一些常考知识

    一.static的作用 static是修饰符,用于修饰成员变量(静态变量/类变量). static修饰的成员被所有对象共享. static优先于对象存在. static修饰的成员可以用类名.静态成员来 ...