由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。
一、什么是NIO?
NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标准。是一种多路复用;它是在Java 1.4中被纳入到JDK中的,并具有以下特性:
– NIO是基于块(Block)的,它以块为基本单位处理数据而传统IO是以字节流的形式,因此NIO性能好一些。
– 为所有的原始类型提供(Buffer)缓存支持– 增加通道(Channel)对象(作为新的原始 I/O 抽象)
– 支持锁(文件锁:有些程序运行时产生.log文件,他就是文件锁,表示当前线程在占用文件锁,其他线程要使用该锁时,需要等待阻塞,当使用完锁时后会删除该文件,也就是拿文件系统来实现锁)和内存映射文件的文件访问接口(一个文件在硬盘磁盘中的,把文件映射到内存,把文件读到内存中,比传统把文件中数据一个个读进去快的多)
– 提供了基于Selector的异步网络I/O
详细请看https://blog.csdn.net/gududedabai/article/details/80783994
二、Buffer&Channel
所有的IO/NIO操作(读写)都要经过Buffer,他是NIO核心部分。通道就是一个抽象,他的一端可能是一个文件或socket等,往里写数据。也就是说通过对buffer读写来对实际的NIO目标来读取实现等。
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区:
使用缓冲区有这么两个好处:
1、减少实际的物理读写次数
2、缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数
举个简单的例子,比如A地有1w块砖要搬到B地
由于没有工具(缓冲区),我们一次只能搬一本,那么就要搬1w次(实际读写次数)
如果A,B两地距离很远的话(IO性能消耗),那么性能消耗将会很大
但是要是此时我们有辆大卡车(缓冲区),一次可运5000本,那么2次就够了
相比之前,性能肯定是大大提高了。
Buffer类详细用法:https://www.cnblogs.com/jiduoduo/p/6397454.html
下面是buffer的实现:
1、buffer
buffer简单的使用:
FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//ByteBuffer.allocate()方法分配了一段内存空间,作为缓存
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();//读写转换
(1)通过NIO复制文件
public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //读文件通道
FileChannel writeChannel = fos.getChannel(); //写文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //读入数据缓存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //读入数据
if (len == -1) {
break;
//读取完毕
}
buffer.flip();
writeChannel.write(buffer);
//写入文件
}
readChannel.close();
writeChannel.close();
}
(2)Buffer中有3个重要的参数:位置(position)、容量(capactiy)和上限(limit)
通过案例了解三个参数的意义:
ByteBuffer b=ByteBuffer.allocate(15); //15个字节大小的缓冲区
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<10;i++){ //存入10个字节数据
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<5;i++){
System.out.print(b.get());
}
System.out.println();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
flip转换主要两步操作:把postition置零;把limit前移到10,就是迁移到刚才postition的位置(好处是,下一次操作自然是从postiton的0开始,操作到那一步是有意义的呢,就是limit的位置,换句话说,从postition到limit之间数据是我上一次写入的的数据而且是有意义的数据,从limit到capacity并不是我上次写入的数据,可能是没有意义的数据,因此在此时读缓冲区,就可以读postition到limit之间的数据就可以了,因此flip通常用于读写转换,在filp之前用于写,在filp之后通常用于读,读使因为位置已经置零了,而且此时数据到limit是有意义的数据。)。
该操作会重置position,通常,将buffer从写模式转换为读模式时需要执行此方法flip()操作不仅重置了当前的position为0,还将limit设置到当前position的位置。
public final Buffer rewind()
– 将position置零,并清除标志位(mark)就是把之前读的数据在扫一遍,limit位置是不变的,因此数据是基本一样的
public final Buffer clear()
– 将position置零,同时将limit设置为capacity的大小,并清除了标志mark
public final Buffer flip()
– 先将limit设置到position所在位置,然后将position置零,并清除标志位mark– 通常在读写转换时使用
(3)文件映射到内存
RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel();
//将文件映射到内存中
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();
三、网络编程
多线程网络服务器的一般结构
下面实现上面简单案例:
1.简单案例 EchoServer
ServerSocket echoServer = null;
Socket clientSocket = null;
try {
echoServer = new ServerSocket(8000);
} catch (IOException e) {
System.out.println(e);
}
while (true) {
try {
clientSocket = echoServer.accept();
System.out.println(clientSocket.getRemoteSocketAddress() + " connect!");
tp.execute(new HandleMsg(clientSocket));
} catch (IOException e) {
System.out.println(e);
}
}
}
static class HandleMsg implements Runnable{
省略部分信息
public void run(){
try {
is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
os = new PrintWriter(clientSocket.getOutputStream(), true);
// 从InputStream当中读取客户端所发送的数据
String inputLine = null;
long b=System.currentTimeMillis();//统计时间戳
while ((inputLine = is.readLine()) != null) {
os.println(inputLine);
}
long e=System.currentTimeMillis();
System.out.println("spend:"+(e-b)+"ms");
} catch (IOException e) {
e.printStackTrace();
}finally{
关闭资源
}
}
2.EchoServer的客户端
public static void main(String[] args) throws IOException {
Socket client = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
client = new Socket();//创建socket
client.connect(new InetSocketAddress("localhost", 8000));
writer = new PrintWriter(client.getOutputStream(), true);
writer.println("Hello!");
writer.flush();
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));//读数据,服务器返回的数据从socket中
System.out.println("from server: " + reader.readLine());
} catch
} finally {
//省略资源关闭
}
}
问题:
– 为每一个客户端使用一个线程,如果客户端出现延时等异常,线程可能会被占用很长时间。因为数据的准备和读取都在这个线程中。
– 此时,如果客户端数量众多,可能会消耗大量的系统资源
解决
– 非阻塞的NIO
– 数据准备好了在工作
3.网络编程-模拟低效的客户端
private static ExecutorService tp=Executors.newCachedThreadPool();
private static final int sleep_time=1000*1000*1000;
public static class EchoClient implements Runnable{
public void run(){
try {
client = new Socket();
client.connect(new InetSocketAddress("localhost", 8000));//开辟若干个线程
writer = new PrintWriter(client.getOutputStream(), true);
writer.print("H");
LockSupport.parkNanos(sleep_time);
writer.print("e");
LockSupport.parkNanos(sleep_time);
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("o");
LockSupport.parkNanos(sleep_time);
writer.print("!");
LockSupport.parkNanos(sleep_time);
writer.println();
writer.flush();
服务器输出如下:
spend:6000ms
spend:6000ms
spend:6000ms
spend:6001ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6003ms
spend:6003ms
4.网络编程-NIO
把数据准备好了再通知我
Channel有点类似于流,一个Channel可以和文件或者网络Socket对应
一个线程(也就是selector)可以轮询多个channel,每一个channl下面对应一个socket。
select()与selectNow()用来看那个channel(客户顿)准备好数据,selector用select()当数据没有转备好他会被阻塞.
selectNow()与select()一样,但区别是他是无阻赛的,没有转备好则返回0;
总结:
– NIO会将数据准备好后,再交由应用进行处理(因此数据没有转备好他是不会写入应用中间的,所以节省资源),数据的读取过程依然在应用线程中完成
– 节省数据准备时间(因为Selector可以复用)
5.网络编程 AIO(主要采用异步回调方式,他是异步的IO)
读完了再通知我(就会既不需要读,也不会需要写,就等待就行了)
不会加快IO,只是在读完后进行通知(什么时候拿到通知什么时候处理,在没有拿到通知去做其他事情,节省时间)
使用回调函数,进行业务处理
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
使用server上的accept方法
public abstract void accept(A attachment, CompletionHandlerhandler);
read是从secket中读数据,因为是异步,所以会立即返回个future.,因此不会在等待读完才返回,不然不叫异步,异步是直接返回的。
write也是类似,写后会立即返回个future.
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
final ByteBuffer buffer = ByteBuffer.allocate(1024);
public void completed(AsynchronousSocketChannel result, Object attachment) {
System.out.println(Thread.currentThread().getName());
Future<Integer> writeResult=null;
try {
buffer.clear();
result.read(buffer).get(100, TimeUnit.SECONDS);//这里把异步有变成了类似同步与等待因为这里做了个阻塞。在这里也可以再来个回调函数去做一些处理
buffer.flip();
writeResult=result.write(buffer);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
server.accept(null, this);//用于在做下一个请求操作,不想做完就程序就不在运行了
writeResult.get();
result.close();
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed: " + exc);
}
});
6、BIO、NIO与AIO对比:
1.同步
例:买饭:自己亲自去饭馆买饭,这就是同步(自己处理IO读写)
2.异步
例:买饭:叫外卖送到家,这就是异步(IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(饭名和地址),OS需要支持异步IO操作API)
3.阻塞
例:办理业务:一直排队等待(调用会一直阻塞到读写完成才返回)
4.非阻塞
例:办理业务:抽号后就可以做其他事,如果你等不急,可以去问工作人员到你了没,如果没到你就不能办理业务。(如果不能读写,调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
BIO是同步阻塞的
伪异步IO:通过线程池机制优化了BIO模型
NIO是同步非阻塞的
AIO是异步非阻塞的
由于NIO的读写过程依然在应用线程里完成,所以对于那些读写过程时间长的,NIO就不太适合。
而AIO的读写过程完成后才被通知,所以AIO能够胜任那些重量级,读写过程长的任务。
BIO是服务器实现模式:基于原始的IO(输入输出流)和Socket就可以编写一个最基本的BIO服务器(用于通信,走TCP协议;什么是TCPhttps://www.cnblogs.com/buxiangxin/p/8336022.html;扩展TCP长连接与短连接https://www.cnblogs.com/songjy2116/p/7750039.html);一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,在JDK1.4出来之前使用的BIO
Socket编程就是BIO,一个socket连接一个处理线程(这个线程负责这个Socket连接的一系列数据传输操作)。阻塞的原因在于:操作系统允许的线程数量是有限的,多个socket申请与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接就会阻塞等待或被拒绝。
BIO图解:
伪异步IO
通过线程池机制优化了BIO模型
伪异步IO图解:
NIO模型
同步非阻塞
服务器实现模式为一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO图解:
AIO模型
异步非阻塞
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS(操作系统)先完成了再通知服务器应用去启动线程进行处理,
注:AIO又称为NIO2.0,在JDK7才开始支持。
AIO流程图
BIO、伪异步、NIO和AIO模型的比较
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
Java对BIO、NIO、AIO的支持:
Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式:详细介绍:https://www.cnblogs.com/daoluanxiaozi/p/3274925.html,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO
同步阻塞IO:在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!
同步非阻塞IO:在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
异步阻塞IO:此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!
异步非阻塞IO:在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。
参考:https://blog.csdn.net/baiye_xing/article/details/73123753
https://blog.csdn.net/skiof007/article/details/52873421
https://www.cnblogs.com/ygj0930/p/6543960.html