Java IO 基本概念
Java IO:即 Java 输入 / 输出系统。
区分 Java 的输入和输出:把自己当成程序, 当你从外边读数据到自己这里就用输入(InputStream/Reader), 向外边写数据就用输出(OutputStream/Writer)。
Stream:Java 中将数据的输入输出抽象为流,流是一组有顺序的,单向的,有起点和终点的数据集合,就像水流。按照流中的最小数据单元又分为字节流和字符流。
- 字节流:以 8 位(即 1 byte,8 bit)作为一个数据单元,数据流中最小的数据单元是字节。
- 字符流:以 16 位(即 1 char,2 byte,16 bit)作为一个数据单元,数据流中最小的数据单元是字符, Java 中的字符是 Unicode 编码,一个字符占用两个字节。
IO 的分类0
流式部分和非流式部分
Java 的 IO 主要包含两个部分:
1.流式部分
是 IO 的主体部分,也是本文介绍的重点, 流式部分根据流向分为输入流(InputStream/Reader)和输出流(OutputStream/Writer), 根据数据不同的操作单元,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),依据字节流和字符流,Java 定义了用来操作数据的抽象基类InputStream/OutputStream 和 Reader/Writer,再根据不同应用场景(或功能),在这两种抽象基类上基于数据载体或功能派上出很多子类,用来满足文件,网络,管道等不同场景的 IO 需求,从而形成了 Java 的基本 IO 体系。
下面是 Java IO 体系中常用的流类:
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutPutStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushBackReader | ||
特殊流 | DataInputStream | DataOutputStream |
2.非流式部分
主要包含一些辅助流式部分的类,如: SerializablePermission 类、File 类、RandomAccessFile 类和 FileDescriptor 等;
3.InputStream
总结:
- InputStream 是所有的输入字节流的父类,它是一个抽象类。
- PushbackInputStream、DataInputStream 和 BufferedInput Stream都是处理流,他们的的父类是 FilterInputStream。
- ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从 Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream 是从与其它线程共用的管道中读取数据。
InputStream 中的三个基本的读方法
- abstract int read() :读取一个字节数据,并返回读到的数据,如果返回 -1,表示读到了输入流的末尾。
- int read(byte[] b) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,表示读到了输入流的末尾。
- int read(byte[] b, int off, int len) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回 -1,表示读到了输入流的末尾。off 指定在数组 b 中存放数据的起始偏移位置;len 指定读取的最大字节数。
4.OutputStream
总结:
- OutputStream 是所有的输出字节流的父类,它是一个抽象类。
- ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向 Byte 数组、和本地文件中写入数据。
- PipedOutputStream 是向与其它线程共用的管道中写入数据。
- BufferedOutputStream、DataOutputStream 和 PrintStream 都是处理流,他们的的父类是 FilterOutputStream。
outputStream中的三个基本的写方法
- abstract void write(int b):往输出流中写入一个字节。
- void write(byte[] b) :往输出流中写入数组b中的所有字节。
- void write(byte[] b, int?off, int?len) :往输出流中写入数组 b 中从偏移量 off 开始的 len 个字节的数据。
5.Reader
Reader 是所有的输入字符流的父类,它是一个抽象类。
- CharReader、StringReader 是两种基本的介质流,它们分别将 Char 数组、String 中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
- BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它 Reader 对象。
- FilterReader 是所有自定义具体装饰流的父类,其子类 PushbackReader 对 Reader 对象进行装饰,会增加一个行号。
- InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。
Reader 基本的三个读方法(和字节流对应):
- public int read() throws IOException; 读取一个字符,返回值为读取的字符。
- public int read(char cbuf[]) throws IOException; 读取一系列字符到数组 cbuf[]中,返回值为实际读取的字符的数量。
- public abstract int read(char cbuf[],int off,int len) throws IOException; 读取 len 个字符,从数组 cbuf[] 的下标 off 处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现。
6.Writer
Writer 是所有的输出字符流的父类,它是一个抽象类。
- CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向 Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据。
- BufferedWriter 是一个装饰器为 Writer 提供缓冲功能。
- PrintWriter 和 PrintStream 极其类似,功能和使用也非常相似。
- OutputStreamWriter 是 OutputStream 到 Writer 转换的桥梁,它的子类 FileWriter 其实就是一个实现此功能的具体类。
writer 的主要写方法:
- public void write(int c) throws IOException; //写单个字符
- public void write(char cbuf[]) throws IOException; //将字符数组 cbuf[] 写到输出流 。
- public abstract void write(char cbuf[],int off,int len) throws IOException; //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 。
- public void write(String str) throws IOException; //将字符串str中的字符写入输出流 。
- public void write(String str,int off,int len) throws IOException; //将字符串 str 中从索引 off 开始处的 len 个字符写入输出流 。
7.实例
字节流
public class TestStream {
/**
* 文件流
* @throws Exception
*/
@Test
public void testWrite() throws Exception {
File file = new File("D:\\work\\学习\\study.txt");
String str = "test write";
//1.建立通道
FileOutputStream fos = new FileOutputStream(file);
//2.写数据
fos.write(str.getBytes());
//3.关闭通道
fos.close();
}
/**
* FileOutputStream 字节输出流
* @throws Exception
*/
@Test
public void testWrite2() throws Exception {
File file = new File("D:\\work\\学习\\study.txt");
//追加文件
FileOutputStream fos = new FileOutputStream(file, true);
String str = "\r\n中国人";
//2.写数据
fos.write(str.getBytes());
//3.关闭通道
fos.close();
}
/**
* FileOutputStream 字节输出流
* @throws Exception
*/
@Test
public void testRead() throws Exception {
File file = new File("D:\\work\\学习\\study.txt");
//1建立管道
FileInputStream is = new FileInputStream(file);
while(is.read() != -1) {
System.out.print((char)is.read());
}
//3.关闭通道
is.close();
}
/**
* 测试copy文件
* @throws Exception
*/
@Test
public void copyfile() throws Exception {
//建立读取视频的管道
FileInputStream fis = new FileInputStream("D:\\work\\学习\\io\\from\\test.wmv");
BufferedInputStream bis = new BufferedInputStream(fis); //包装成字符流
//创建用于接收视频的文件
File file = new File("D:\\work\\学习\\io\\to\\test.wmv");
if(!file.exists()) {
boolean flag = file.createNewFile();
if(flag) {
System.out.println("创建文件成功");
}else {
System.out.println("创建文件失败");
}
}
//建立写入视频的管道
FileOutputStream fos = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fos); //包装成字符流
//读取数据 设置缓存区大小
byte[] buf = new byte[1024];
int len = 0;
while((len = bis.read(buf)) != -1){
bos.write(buf, 0, len);
}
//关闭流 先关外部管道 再关内部管道
bis.close();
fis.close();
fos.close();
bos.close();
}
}
字符流:
/**
* @author meikb
* @date 2018年1月8日
*/
public class TestFileder {
public static void main(String[] args) {
//readFile();
writeFile();
}
/**
* 读取文件内容
*/
public static void readFile() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("E:\\txt\\mkb.txt"), "UTF-8"));
String line = null;
// 每次读取一行内容,循环读取,读到文件末尾结束
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 关闭I/O流
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 读取文件内容
*/
public static void writeFile() {
try {
FileOutputStream fos = new FileOutputStream("E:\\txt\\mkb.txt", true);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write("小红帽");
BufferedWriter br = new BufferedWriter(osw);
br.write("白雪公主");
PrintWriter pw = new PrintWriter(fos);
pw.println("总一条蜿蜒在童话镇里七彩的河");
pw.flush();
br.close(); //字符流采用缓冲区 必须关闭才能输出
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
BIO
server
public class BioServer {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(8181);
while (true){
//等待接受连接,这个方法是阻塞的
System.out.println("等待连接");
Socket socket = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
byte[] bufArr = new byte[2048];
while (true){
int length = inputStream.read(bufArr);
// 如果不判断流结束,上面的read()读不到数据会一直堵塞
if(length == -1){
break;
}
System.out.println("接受到消息" + new String(bufArr));
}
}catch (Exception e){
}
}
}).start();
}
}
}
client
public class BioClient {
public static void main(String[] args) throws Exception{
Socket serverSocket = new Socket("localhost", 8181);
OutputStream outputStream = serverSocket.getOutputStream();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
while (scanner.hasNextLine()){
String msg = scanner.nextLine();
outputStream.write(msg.getBytes());
}
}
}
BIO: accept 和 read方法都是阻塞方法,通过 new thread 可以解决问题,但是不可能所有的socket都新建一个线程去执行。
Java NIO
I/O介绍
网络I/O:本质是socket读取
磁盘I/O:每次I/O,都经由两个阶段:
第一步:将数据先从磁盘文件先加载至内核内存空间(缓冲区),等待数据准备完成时间较长。
第二步:将数据从内核缓冲区复制到用户空间进程的内存中,时间较短
用户空间与内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
文件描述符 FD
文件描述符是一个用于表述指向文件的引用的抽象化概念。在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
在linux中一切皆文件,当客户端发起连接请求的时候,会在指定的目录下生产,一个文件,来记录这个进程。
select
1、最大并发数限制,因为一个进程所打开的 FD(文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,因此Select模型的最大并发数就被相应限制了。
2、效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是,都超时了。
3、内核 / 用户空间内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。
总结:1、连接数受限 2、查找配对速度慢 3、数据由内核拷贝到用户态消耗时间
epoll
1、Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。
2、效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, Epoll的效率就会远远高于select和poll。
3、内存共享, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。
总结:把描述符列表的管理交由内核负责,一旦有某种事件发生,内核把发生事件的描述符列表通知给进程,避免轮询减少系统开销
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的文件描述符集合,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的文件描述符集合就行了。当描述符多的时候也只是会占用较多的内存而已,而不会造成占用大量cpu时间
Buffer
buffer 本身就是一块内存,实际上就是一个数组,数据的读写都是通过 buffer 来实现的。
java 中 8 种基本数据类型都有各自对应的 buffer 类型(除 boolean 外),如 IntBuffer、CharBuffer、ByteBuffer、ShortBuffer、LongBuffer 等。
capacity:最大容量,永远不可能是负数,并且是不会变化的
limit:限制,永远不可能是负数,并且不会大于 capacity
position:写一个读或者写的位置,永远不可能是负数,并且不会大于 limit
- buffer.put():往此 buffer 中放置元素(往数组中写)
- buffer.get():往此 buffer 中取出元素(往数组中读)
Channel
所有数据读写都是通过 buffer 来进行的,永远不会出现直接在 channel 中直接写入、读取数据,与 stream 不同的是,channel 是双向的,而 stream 可能是 inputSteram 或 outputStream,而 channel 打开后可以读又可以写
- channel.read(buffer):从通道中读取数据写到 buffer 中,对于 buffer 来说是写操作
- channel.write(buffer):从 buffer 中读取数据写到到 channel 中,对于 buffer 来说是读操作
什么是阻塞、什么是同步?
阻塞:用户程序向操作系统提出IO请求,操作系统对于用户程序的请求是否立即返回,如果立即返回就是非阻塞的,如果不是立即返回的就是阻塞的;
同步:用户程序的读写操作是否暂停用户程序,如果用户程序停下手头的工作去忙活读写操作就是同步的,如果不用停下用户程序,操作系统就可以帮忙读写那就是异步的;
阻塞 IO 和非阻塞 IO 的区别就在于:应用程序的调用是否立即返回!
同步 IO 和异步 IO 的区别就在于:数据拷贝的时候进程是否阻塞!
IO的几种类型
1.同步阻塞IO(BIO):
用户程序向操作系统发送请求后,操作系统一直不返回所以一直堵塞,当操作系统将数据准备好以后返回,然后用户程序将数据写入socket空间或者将数据读出socket空间;
2,同步非阻塞IO(NIO)//NIO有两种,还有一种是IO多路复用
用户程序向操作系统发送请求后,操作体统立即返回(非阻塞),返回之后用户程序可以继续进行自己的事,用户线程要定时轮询检查数据是否就绪,当数据就绪后,用户线程将数据从用户空间写入socket空间,或从socket空间读取数据到用户空间(同步)。
3.IO多路复用
用户程序向操作系统发送请求后,操作系统立即返回(非阻塞),将socket连接及关注事件注册到selector(多路复用器,os级别线程)上,selector循环遍历socket连接,看是否有关注数据就绪,如果连接有数据就绪后,就通知应用程序,建立线程进行数据读写。同BIO对比,NIO中线程处理的都是有效连接(数据就绪),且一个线程可以分管处理多个连接上的就绪数据,节省线程资源开销。
上一个NIO轮询检查数据是否就绪,多路复用就是将数据是否就绪的问题交给了操作系统,当数据准备就绪后通知用户程序建立连接进行想要的IO操作;
3.异步非阻塞(AIO):
用户程序向操作系统发送请求后,操作系统立即返回(非阻塞),当操作系统将数据的读写操作完成后才会通知用户程序,这里面的读写操作也都是OS进行的不会停止用户程序(异步);
**
NIO
server
public class NioServer {
static SelectorProvider provider = SelectorProvider.provider();
static Selector selector = null;
static ServerSocketChannel server = null;
private static void accept() throws IOException {
SocketChannel channel = null;
try {
channel = server.accept(); // 接受连接
channel.configureBlocking(false); // 非阻塞模式
channel.register(selector, SelectionKey.OP_READ, null); // 监听读就绪
} catch (IOException e) {
if (channel != null)
channel.close();
}
}
private static int read(SocketChannel channel) throws IOException {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配HeapByteBuffer
int len = channel.read(buffer); // 直到没有数据 || buffer满
if (len > 0)
System.out.println(new String(buffer.array(), 0, len, Charset.forName("UTF-8"))); // buffer.array():取HeapByteBuffer中的原始byte[]
return len;
} catch (IOException e) {
if (channel != null)
channel.close();
return -1;
}
}
private static void write(SocketChannel channel, String msg) throws IOException {
try {
byte[] bytes = msg.getBytes(Charset.forName("UTF-8"));
ByteBuffer buffer = ByteBuffer.allocate(bytes.length); // 分配HeapByteBuffer
buffer.put(bytes);
buffer.flip(); // 切换为读模式
channel.write(buffer);
} catch (IOException e) {
if (channel != null)
channel.close();
}
}
public static void main(String[] args) throws IOException {
try {
selector = provider.openSelector();
server = provider.openServerSocketChannel();
server.configureBlocking(false); // 非阻塞模式
SelectionKey key = server.register(selector, 0, null); // 注册
if (server.bind(new InetSocketAddress(8888)).socket().isBound()) // 绑定成功
key.interestOps(SelectionKey.OP_ACCEPT); // 监听连接请求
while (true) {
selector.select(); // 监听就绪事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
key = it.next();
it.remove(); // 从已选择键集中移除key
if (key.isAcceptable()) { // 连接请求到来
System.out.println("accept...");
accept();
} else {
SocketChannel channel = (SocketChannel) key.channel();
if (key.isWritable()) { // 写就绪
System.out.println("write...");
write(channel, "Hello NioClient!");
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写就绪,否则会一直触发写就绪(写就绪为代码触发)
key.channel().close(); // 关闭channel(key将失效)
}
if (key.isValid() && key.isReadable()) { // key有效(避免在写就绪时关闭了channel或者取消了key) && 读就绪
System.out.println("read...");
int len = read(channel);
if (len >= 0)
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // 写就绪,准备写数据
else if (len < 0) // 客户端已关闭socket
channel.close(); // 关闭channel(key将失效)
}
}
}
}
} finally {
if (server != null)
server.close();
if (selector != null)
selector.close();
}
}
}
client
public class NioClient {
static SelectorProvider provider = SelectorProvider.provider();
static Selector selector = null;
static SocketChannel client = null;
static boolean close = false;
private static void write(String msg) throws IOException {
byte[] bytes = msg.getBytes(Charset.forName("UTF-8"));
ByteBuffer buffer = ByteBuffer.allocate(bytes.length); // 建立HeapByteBuffer(DirectByteBuffer以后有机会再讨论)
buffer.put(bytes);
buffer.flip(); // 切换为读模式
client.write(buffer);
}
private static int read() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配HeapByteBuffer
int len = client.read(buffer); // 直到没有数据 || buffer满
if (len > 0)
System.out.println(new String(buffer.array(), 0, len, Charset.forName("UTF-8"))); // buffer.array():取HeapByteBuffer中的原始byte[]
return len;
}
public static void main(String[] args) throws IOException {
try {
selector = provider.openSelector();
client = provider.openSocketChannel();
client.configureBlocking(false); // 非阻塞模式
SelectionKey key = client.register(selector, 0, null); // 注册
if (client.connect(new InetSocketAddress("127.0.0.1", 8888))) { // 连接成功(很难)
System.out.println("connected...");
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 监听读就绪和写就绪(准备写数据)
} else // 连接失败(正常情况下)
key.interestOps(SelectionKey.OP_CONNECT); // 监听连接就绪
while (!close) {
selector.select(); // 监听就绪事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
key = it.next();
it.remove(); // 从已选择键集移除key
if (key.isConnectable()) { // 连接就绪
client.finishConnect(); // 完成连接
System.out.println("connected...");
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT); // 取消监听连接就绪(否则selector会不断提醒连接就绪)
key.interestOps(key.interestOps() | SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 监听读就绪和写就绪
} else {
if (key.isWritable()) { // 写就绪
System.out.println("write...");
write("Hello NioServer!");
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写就绪,否则会一直触发写就绪(写就绪为代码触发)
}
if (key.isValid() && key.isReadable()) { // key有效(避免在写就绪时关闭了channel或者取消了key) && 读就绪
System.out.println("read...");
if (read() < 0) // 服务器已关闭socket
close = true; // 退出循环
}
}
}
}
} finally {
if (client != null)
client.close();
if (selector != null)
selector.close();
}
}
}