前言:这是本人在慕课网上学习 一站式学习Java网络编程 全面理解BIO/NIO/AIO 时所做的笔记,供本人复习之用,本文主要讲述NIO的概念,并比较下不同类型IO(阻塞式、非阻塞式等)的速度,不保证一定准确。
目录
第一章 NIO概述
1.1 由来
BIO中的阻塞,由于流的读写是阻塞式调用的,所以多人聊天室为了同时支持多个人互相发送消息,我们就没有办法使用一个线程来处理多个客户端流的输入和输出,因为如果有一个用户长时间无法通过这个流进行读和写,那么这个流就会阻塞整个线程。
ServerSocket.accept()
InputStream.read(),OutputStream.write()
为了避免一个阻塞所有人,我们在BIO中不得不开启多条线程,所以我们就考虑找一个不会阻塞的IO,也就是NIO。
1.2 基本概念
在Nio中不再使用Stream这些类,使用Channel进行替代,Stream是有方向性的,输入流只能写入数据,输出流只能输出数据,可以Channel是双向的,既可以写入数据,也可以读取数据。而且流的读写都是阻塞式的,Channel既提供了阻塞式的,也提供了非阻塞式的读写。
使用Selector来监控多条Channel,每条Channel的读写是非阻塞式的,所以我们需要经常检查Channel的状态,就是看Channel里面有没有数据,然后进行读取或者输出操作,如果自己查询太麻烦了,所以使用Selector来监视其状态。
所以使用NIO的话我们就可以在一个线程里处理多个Channel I/O,如果线程数量超过了处理器的数量,就一定会出现如线程上下文切换的东西,每次切换线程要保存线程的状态,过一段时间再加载线程的状态,这个占用系统资源和花费时间,而且每个线程也占用一定的内存,线程多了也会有一定的负担。
第二章 Buffer
向Channel读或者写数据必须通过Buffer完成,当向Channel中写数据的时候,必须要写到Buffer里面,当要向Channel里读数据的时候,也一定是要从Buffer中读出数据,所以所有对应的操作背后都离不开Buffer。
Buffer也是支持双向操作的,同一个Buffer可以向里面写,也可以向里面读。
2.1 写模式
capacity代表buffer最大的容量,position表示目前所在的位置,limit指向的是和capacity指向的相同的位置。
当写入一部分数据后,会变成这样
2.2 读模式
当写入部分数据后,我们要将数据读取出来,需要调用flip()函数,在flip()函数中,会做如下操作,将position移动会初始位置,将limit指向刚才position的位置,现在postion和limit之间就是刚才写入的数据了,我们也就可以读取数据了,当然最多读到limit。
读取也分两种情况,第一种是全部读完,读完之后我们要继续向其中写入数据了,那我们要调用clear函数,clrear函数会将postion移动到初始位置,然后将limit移动到capacity位置,我们也就正式将读模式切换到了写模式。值得注意是虽然名字叫clear(),但它并没有真的将数据清除,而是仅仅移动了指针。
还有一种情况是没有读完,只读取了一部分,来不及读完,就要写入新的数据了,希望写入后还能读到刚才的数据,这时需要调用compact()函数,compact函数会将还没读取的数据拷贝到 buffer最开始的位置,拷贝完成后将position指针指向刚刚超过未读部分的数据,然后就可以正常的写入数据了,等再反转后就可以再都出来了。
第三章 Channel
Channel不止可以和Buffer进行串数,还可以和Channel进行传输,每个Channel都可以向其它Channel传输或者接收数据。
Channel有以下几种,File是和文件操作相关,ServerSocket和Socket对应于之前的服务端和客户端的Socket。
第四章 多方法实现本地文件拷贝
用四种不同的方法拷贝文件测试效率,分别是:
直接输入输出流拷贝(一个一个字节拷贝,其实不用缓冲也可以每次读1024个字节,这里老师是为了演示不同IO实现)
用带缓存的输入输出流拷贝
用上面说到的nio拷贝,需要buffer
用nio拷贝,不要buffer
package bio.demo;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author ZhangChen
**/
interface FileCopyRunner{
void copyFile(File source,File target);
}
public class FileCopyDemo {
private static final int ROUNDS = 5;
public static void close(Closeable closeable){
if(closeable!=null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void benchmark(FileCopyRunner test,File source,File target){
long elapsed = 0L;
for(int i=0;i<ROUNDS;i++){
long startTime = System.currentTimeMillis();
test.copyFile(source,target);
elapsed+= System.currentTimeMillis()-startTime;
target.delete();
}
System.out.println(test + ": "+elapsed/ROUNDS);
}
public static void main(String[] args) {
FileCopyRunner noBufferStreamCopy =new FileCopyRunner() {
@Override
public void copyFile(File source, File target) {
InputStream fin = null;
OutputStream fout = null;
try {
fin = new FileInputStream(source);
fout = new FileOutputStream(target);
int result ;
while((result=fin.read())!=-1){
fout.write(result);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
@Override
public String toString() {
return "noBufferStreamCopy";
}
};
FileCopyRunner bufferStreamCopy;
bufferStreamCopy = new FileCopyRunner() {
@Override
public void copyFile(File source, File target) {
InputStream fin = null;
OutputStream fout = null;
try {
fin = new BufferedInputStream(new FileInputStream(source));
fout = new BufferedOutputStream(new FileOutputStream(target));
//将内容读入缓冲区
byte[] buffer = new byte[1024];
int result ;
while((result=fin.read(buffer))!=-1){
fout.write(buffer,0,result);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
@Override
public String toString() {
return "bufferStreamCopy";
}
};
FileCopyRunner nioBufferCopy;
nioBufferCopy = new FileCopyRunner() {
@Override
public void copyFile(File source, File target) {
FileChannel fin = null;
FileChannel fout = null;
try {
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(source).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(fin.read(buffer)!=-1){
buffer.flip();
//不一定会把数据都读取出来write进去,要判断是否有剩余,如果有则一直写
while(buffer.hasRemaining()){
fout.write(buffer);
}
buffer.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
@Override
public String toString() {
return "nioBufferCopy";
}
};
FileCopyRunner nioTransferCopy;
nioTransferCopy = new FileCopyRunner() {
@Override
public void copyFile(File source, File target) {
FileChannel fin = null;
FileChannel fout = null;
try {
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(source).getChannel();
//同样,tranfer也不能保证一次全部写完,要做字节统计
long transferred = 0;
long size = fin.size();
while(transferred!=size){
transferred+=fin.transferTo(0,size,fout);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
@Override
public String toString() {
return "nioTransferCopy";
}
};
File smallFile = new File("C:\\Users\\61037\\Desktop\\test.jpg");
File smallFileCopy = new File("C:\\Users\\61037\\Desktop\\new.jpg");
System.out.println("---copy small file---");
benchmark(noBufferStreamCopy,smallFile,smallFileCopy);
benchmark(bufferStreamCopy,smallFile,smallFileCopy);
benchmark(nioBufferCopy,smallFile,smallFileCopy);
benchmark(nioTransferCopy,smallFile,smallFileCopy);
}
}
最后的效率问题我就没在本地测试,借用一下视频老师的测试结果
400KB
10或11个 (单位没听清,感觉是MB)
500个
总结:可以看出来速度差异不大,这是因为Java后面用nio或者更有效率的实现方法去优化了之前的这些IO操作,所以速度提升了很多,所以看不到传统IO和NIO的太大的区别。
第五章 Selector
5.1 channel的状态
我们可以选择Channel进行非阻塞性的读写,但是却要不停的访问Channel看看有没有数据,能不能进行读写,自己实现比较繁琐,所以Java提供了Selector,帮我们监控多个通道的状态。
首先我们要把通道注册在Selector上面,注册后我们只需询问selector就可以得到答案了。
每个通道在不同的时间会处于不同的状态,这个状态不是一成不变的,各种外部事件的发生都会导致通道的状态发生变化。
客户端的SocketChannel和远端建立了连接,这就使SocketChannel处于了connect状态,对应的在服务器端ServerSocketChannel接受了一个客户端的连接,它就处于accept状态。
channel中有可读取的数据后,channel处于read状态,当channel处于可以向其中写入数据的状态,这就是write状态。
channel也可以不处于任何状态,没什么操作可以操作channel。
5.2 基本操作
当把channel注册在selector上后,我么们会得到一个SelectionKey对象,SelectionKey类似于一个ID,每一个在selector上注册的channel,都对应于一个SelectionKey。
通过SelectionKey,我们可以调用interestOps(),获取我们注册在selecor上的状态。我们注册时候可以选择我们感兴趣的状态,如只选择read状态,也就是只有channel可读的时候才提醒我们。
通过SelectionKey,我们可以调用readyOps(),返回的是对于这个SelectionKey,它有哪些监听的状态是准备好的,即处于哪些可操作的状态下。
通过SelectionKey,我们可以调用channel,即注册到selector上的channel。
通过SelectionKey,我们可以调用selector,即channel注册的是哪个selector。
通过SelectionKey,我们可以调用attachment,对于每一个注册在selector上的channel对象,我们可以再attach上一个对象,这个对象可以是任何你认为有意义的对象,任何对于channel的操作会带来帮助或者是必须的对象。
如下图所示,我们再selector上注册了3个Channel,当没有一个处于我们感兴趣的状态时调用select返回0。
当有一个channel处于我们感兴趣状态时,再调用select状态时,select就会返回1,我们就可以调用其它的selector函数得到这个channel的SelectionKey,我们可以通过这个SelectionKey得到channel对象等等。
当操作完channel后,selector不会将channel置为不可操作状态,需要手动把channel重置为不可操作状态,如果后面有两个channel进入状态,那么我们调用select就会返回2,然后可以通过SelectionKey获取channel对象再进行其它操作。