Java IO系统及IO模型详细笔记
前言
学习Java IO系统,重点是学会IO模型,了解了各种IO模型之后就可以更好的理解Java IO。
java中IO系统可以分为Bio,Nio,Aio三种io模型
关于Bio,我们需要知道什么是同步阻塞IO模型,Bio操作的对象:流,以及如何使用Bio进行网络编程,使用Bio进行网络编程的问题
关于Nio,我们需要知道什么是同步非阻塞IO模型,什么是多路复用Io模型,以及Nio中的Buffer,Channel,Selector的概念,以及如何使用Nio进行网络编程
关于Aio,我们需要知道什么是异步非阻塞IO模型,Aio可以使用几种方式实现异步操作,以及如何使用Aio进行网络编程
一、File类
File封装的对象仅仅是一个路径名,这个路径可以是存在的,也可以是不存在的。当我们通过 new FIle(“文件/文件夹/绝对路径/相对路径”) 创建对象定位文件,可以创建、删除、获取文件信息等。但是不能读写文件内容。
绝对路径和相对路径是什么样的?
(1)绝对路径是带盘符的。
例如:File file1 = new File(“D:\itheima\a.txt”);
(2)相对路径是不带盘符的,默认到当前工程下寻找文件。
例如:File file3 = new File(“模块名\a.txt”);
二、BIO(同步阻塞IO)
BIO是同步阻塞IO,JDK1.4之前只有这一个IO模型,BIO操作的对象是流,一个线程只能处理一个流的IO请求,如果想要同时处理多个流就需要使用多线程。
流包括字符流和字节流,流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流。
1.BIO模型
2.IO流
BIO中操作的流主要有两大类,字节流和字符流。字节流主要用来处理字节或二进制对象,字符流用来处理字符文本或字符串。
按照类型和输入输出方向可分为:
输入字节流:InputStream
输出字节流:OutputStream
输入字符流:Reader
输出字符流:Writer
列举的这些都是抽象类,我们实例化的都是他们的子类,每一个子类都有自己的作用范围。
2.1.字节流
(1)字节输入流 FileInputStream
InputStream is = new FileInputStream("test.txt");
int size = is.available();
for (int i = 0; i < size; i++) {
System.out.print((char) is.read() + " ");
}
is.close();
InputStream常用API:
public int read() //读取一个字节:性能较慢,中文字符输出乱码问题。
public int read(byte[] buffer) //读取一个字节数组:中文字符输出可能乱码问题。
(2)字节输出流 FileOutputStream
byte bWrite[] = { 11, 21, 3, 40, 5 };
OutputStream os = new FileOutputStream("test.txt");
for (int x = 0; x < bWrite.length; x++) {
os.write(bWrite[x]); // writes the bytes
}
os.close();
OutputStream常用API:
public void write(int a) //写一个字节
public void write(byte[] buffer) //写一个字节数组
2.2.字符流
(1)字符输入流 FileReader
Reader reader = new FileReader(new File("D:/a.txt"));
char[] bytes = new char[(int) file.length()];
reader.read(bytes);
reader.close();
Reader常用API:
public int read() //每次读取一个字符:性能较慢,中文字符会出现乱码。
public int read(char[] buffer) //每次读取一个字符数组:中文字符可能会出现乱码。
(2)字符输出流 FileWriter
String var = "hai this is a test";
Writer writer = new FileWriter(new File("D:/b.txt"));
writer.write(var);
writer.close();
Writer常用API:
public void write(int a) //写一个字符
public void write(byte[] buffer) //写一个字符数组
2.3.字节字符缓冲流
字节缓冲流性能优化原理:字节缓冲输入流自带了8KB缓冲池,读数据的时候会一次读取一块数据放到缓冲区里,当缓冲区里的数据被读完之后,输入流会再次填充数据缓冲区,直到输入流被读完。
有了缓冲区就能够提高很多io速度以后我们直接从缓冲池读取或写入数据,所以性能较好。
推荐使用哪种方式提高字节流读写数据的性能?建议使用字节缓冲输入流、字节缓冲输出流,结合字节数组的方式,目前来看是性能最优的组合。
(1)字节缓冲输入流 BufferedInputStream
/**
* inputStream 输入流
* 1024 内部缓冲区大小为1024byte
*/
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);
(2)字节缓冲输出流 BufferedOutputStream
/**
* outputStream 输出流
* 1024 内部缓冲区大小为1024byte
*/
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream,1024);
2.4.字符缓冲流
(1)字符缓冲输入流 BufferedReader
BufferedReader bufferedReader = new BufferedReader(reader,1024);
(2)字符缓冲输出流 BufferedInputStream
BufferedWriter bufferedWriter = new BufferedWriter(writer,1024);
2.5.字符转换流
使用字符转换流,解决乱码问题。
File f = new File("a.txt");
FileOutputStream fop = new FileOutputStream(f);
// 构建FileOutputStream对象,文件不存在会自动新建
OutputStreamWriter writer = new OutputStreamWriter(fop, "UTF-8");
// 构建OutputStreamWriter对象,参数可以指定编码,默认为操作系统默认编码,windows上是gbk
writer.append("中文输入");
// 写入到缓冲区
writer.append("\r\n");
// 换行
writer.append("English");
// 刷新缓存冲,写入到文件,如果下面已经没有写入的内容了,直接close也会写入
writer.close();
// 关闭写入流,同时会把缓冲区内容写入文件,所以上面的注释掉
fop.close();
// 关闭输出流,释放系统资源
FileInputStream fip = new FileInputStream(f);
// 构建FileInputStream对象
InputStreamReader reader = new InputStreamReader(fip, "UTF-8");
// 构建InputStreamReader对象,编码与写入相同
StringBuffer sb = new StringBuffer();
while (reader.ready()) {
sb.append((char) reader.read());
// 转成char加到StringBuffer对象中
}
System.out.println(sb.toString());
reader.close();
// 关闭读取流
fip.close();
// 关闭输入流,释放系统资源
3. BIO模型 网络编程
二、NIO(同步非阻塞IO)
JDK 1.4版本以来,JDK发布了全新的I/O类库,简称NIO,是一种同步非阻塞IO模型。
1.BIO模型
2.NIO的核心概念
2.1.Buffer(缓冲区)
Buffer是一个对象,它包含一些要写入或者读出的数据,在NIO中所有数据都是用缓存区处理的,在读数据的时候要从缓冲区中读,写数据的时候会先写到缓冲区中,缓冲区本质上是一块可以写入数据,然后可以从中读取数据的一个数组,提供了对数据的结构化访问以及在内部维护了读写位置等信息。了解更多Buffer
//创建一个容量为1024个byte的缓冲区
ByteBuffer buffer=ByteBuffer.allocate(1024);
如何使用Buffer:
(1)写入数据到Buffer
(2)调用flip()方法将Buffer从写模式切换到读模式
(3)从Buffer中读取数据
(4)调用clear()方法或者compact()方法清空缓冲区,让它可以再次被写入
2.2.Channel(通道)
Channel(通道)数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中,Channel只负责运输数据,而操作数据是Buffer。了解更多Channel
InputStream is = new FileInputStream(new File("doc/input/nio/mysql-8.0.25-linux-glibc2.12-i686.tar.xz"));
OutputStream os = new FileOutputStream(new File("doc/output/nio/mysql-8.0.25-linux-glibc2.12-i686.tar.xz"));
//获取输入输出channel
FileChannel inChannel = ((FileInputStream) is).getChannel();
FileChannel osChannel = ((FileOutputStream) os).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
//从inChannel里读数据
int len=0;
if((len = inChannel.read(buffer)) == -1){
break;
}
//将buffer从写模式切换到读模式
buffer.flip();
//开始往osChannel写数据
osChannel.write(buffer);
//清空buffer
buffer.clear();
}
inChannel.close();
osChannel.close();
is.close();
os.close();
2.3.Selector(多路复用选择器)
Selector是NIO编程的基础,主要作用就是将多个Channel注册到Selector上,如果Channel上发生读或写事件,Channel就处于就绪状态,就会被Selector轮询出来,然后通过SelectionKey就可以获取到已经就绪的Channel集合,进行IO操作了。了解更多Selector
3.NIO模型 网络编程
JDK中NIO使用多路复用的IO模型,通过把多个IO阻塞复用到一个select的阻塞上,实现系统在单线程中可以同时处理多个客户端请求,节省系统开销,在JDK1.4和1.5 update10版本之前,JDK的Selector基于select/poll模型实现,在JDK 1.5 update10以上的版本,底层使用epoll代替了select/poll。
epoll较select/poll的优点在于:
(1) epoll支持打开的文件描述符数量不在受限制,select/poll可以打开的文件描述符数量有限
(2)select/poll使用轮询方式遍历整个文件描述符的集合,epoll基于每个文件描述符的callback函数回调
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写。
NIO提供了两套不同的套接字通道实现网络编程,服务端:ServerSocketChannel和客户端SocketChannel,两种通道都支持阻塞和非阻塞模式。
3.1.服务端代码
服务端接受客户端发送的消息输出,并给客户端发送一个消息。
//创建多路复用选择器Selector
Selector selector=Selector.open();
//创建一个通道对象Channel,监听9001端口
ServerSocketChannel channel = ServerSocketChannel.open().bind(new InetSocketAddress(9001));
//设置channel为非阻塞
channel.configureBlocking(false);
//
/**
* 1.SelectionKey.OP_CONNECT:连接事件
* 2.SelectionKey.OP_ACCEPT:接收事件
* 3.SelectionKey.OP_READ:读事件
* 4.SelectionKey.OP_WRITE:写事件
*
* 将channel绑定到selector上并注册OP_ACCEPT事件
*/
channel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
//只有当OP_ACCEPT事件到达时,selector.select()会返回(一个key),如果该事件没到达会一直阻塞
selector.select();
//当有事件到达了,select()不在阻塞,然后selector.selectedKeys()会取到已经到达事件的SelectionKey集合
Set keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = (SelectionKey) iterator.next();
//删除这个SelectionKey,防止下次select方法返回已处理过的通道
iterator.remove();
//根据SelectionKey状态判断
if (key.isConnectable()){
//连接成功
} else if (key.isAcceptable()){
/**
* 接受客户端请求
*
* 因为我们只注册了OP_ACCEPT事件,所以有客户端链接上,只会走到这
* 我们要做的就是去读取客户端的数据,所以我们需要根据SelectionKey获取到serverChannel
* 根据serverChannel获取到客户端Channel,然后为其再注册一个OP_READ事件
*/
// 1,获取到ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 2,因为已经确定有事件到达,所以accept()方法不会阻塞
SocketChannel clientChannel = serverChannel.accept();
// 3,设置channel为非阻塞
clientChannel.configureBlocking(false);
// 4,注册OP_READ事件
clientChannel.register(key.selector(),SelectionKey.OP_READ);
} else if (key.isReadable()){
// 通道可以读数据
/**
* 因为客户端连上服务器之后,注册了一个OP_READ事件发送了一些数据
* 所以首先还是需要先获取到clientChannel
* 然后通过Buffer读取clientChannel的数据
*/
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead>0){
byteBuffer.flip();
System.out.println("client data :"+new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
/**
* 我们服务端收到信息之后,我们再给客户端发送一个数据
*/
byteBuffer.clear();
byteBuffer.put("客户端你好,我是服务端,你看这NIO多难".getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
} else if (key.isWritable() && key.isValid()){
//通道可以写数据
}
}
}
3.2.客户端代码
客户端连接上服务端后,先给服务端发送一个消息,并接受服务端发送的消息。
Selector selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
//将channel设置为非阻塞
clientChannel.configureBlocking(false);
//连接服务器
clientChannel.connect(new InetSocketAddress(9001));
//注册OP_CONNECT事件
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
//如果事件没到达就一直阻塞着
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()){
/**
* 连接服务器端成功
*
* 首先获取到clientChannel,然后通过Buffer写入数据,然后为clientChannel注册OP_READ时间
*/
clientChannel = (SocketChannel) key.channel();
if (clientChannel.isConnectionPending()){
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
byteBuffer.put("服务端你好,我是客户端,你看这NIO难吗".getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
clientChannel.register(key.selector(),SelectionKey.OP_READ);
} else if (key.isReadable()){
//通道可以读数据
clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead>0){
byteBuffer.flip();
System.out.println("server data :"+new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
} else if (key.isWritable() && key.isValid()){
//通道可以写数据
}
}
}
使用原生NIO类库十分复杂,NIO的类库和Api繁杂,使用麻烦,需要对网络编程十分熟悉,才能编写出高质量的NIO程序,所以并不建议直接使用原生NIO进行网络编程,而是使用一些成熟的框架,比如Netty。
三、AIO(异步非阻塞IO)
JDK1.7升级了Nio类库,成为Nio2.0,最主要的是提供了异步文件的IO操作,以及事件驱动IO,AIO的异步套接字通道是真正的异步非阻塞IO。
1.AIO模型
2.AIO模型 网络编程
2.1.异步操作
aio不需要通过多路复用器对注册的通道进行轮询操作就可以实现异步读写,从而简化了NIO的编程模型。
aio通过异步通道实现异步操作,异步通道提供了两种方式获取操作结果:
(1)通过Future类来获取异步操作的结果,不过要注意的是future.get()是阻塞方法,会阻塞线程
(2)通过回调的方式进行异步,通过传入一个CompletionHandler的实现类进行回调,CompletionHandler定义了两个方法,completed和failed两方法分别对应成功和失败
Aio中的Channel都支持以上两种方式。
AIO提供了对应的异步套接字通道实现网络编程,服务端:AsynchronousServerSocketChannel和客户端AsynchronousSocketChannel。
2.2.服务端
服务端向客户端发送消息,并接受客户端发送的消息。
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9001));
//异步接受请求
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
//成功时
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("我是服务端,客户端你好".getBytes());
buffer.flip();
result.write(buffer, null, new CompletionHandler<Integer, Void>(){
@Override
public void completed(Integer result, Void attachment) {
System.out.println("服务端发送消息成功");
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("发送失败");
}
});
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
result.read(readBuffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println(new String(readBuffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("读取失败");
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//失败时
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
//防止线程执行完
TimeUnit.SECONDS.sleep(1000L);
2.3.客户端
客户端向服务端发送消息,并接受服务端发送的消息。
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9001));
//阻塞,获取连接
future.get();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读数据
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println(new String(buffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("客户端接收消息失败");
}
});
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("我是客户端,服务端你好".getBytes());
writeBuffer.flip();
//阻塞方法
Future<Integer> write = client.write(writeBuffer);
Integer r = write.get();
if(r>0){
System.out.println("客户端消息发送成功");
}
//休眠线程
TimeUnit.SECONDS.sleep(1000L);
总结
各IO模型对比: