Java BIO总结
目录
1. 分类
1.1 按数据流的方向分为输入流、输出流
数据流向 | 含义 |
---|---|
输入流 | 从别的地方(本地文件,网络上的资源等)获取资源 输入到 我们的程序中 |
输出流 | 从我们的程序中 输出到 别的地方(本地文件), 将一个字符串保存到本地文件中,就需要使用输出流 |
1.2 按处理数据单位不同分为字节流、字符流
1字符 = 2字节 、 1字节(byte) = 8位(bit) 、 一个汉字占两个字节长度
数据处理单位 | 含义 | 对应类 |
---|---|---|
字节流 | 每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码 | 输入 InputStream 输出 OutputStream |
字符流 | 每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文 | 输入 Reader 输出 Writer |
1.3 按功能不同分为介质流、装饰流、功能流
功能分类 | 含义 |
---|---|
介质流 | 以从或向一个特定的地方(节点)读写数据。如FileInputStream |
装饰流 | 是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如FilterInputStream。装饰流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装 |
功能流 | 特定功能来处理流对象,如SequenceInputStream可合并流、BufferedInputStream可为输入流提供缓冲功能 |
2. 字节流与字符流
2.1 使用场景
IO流类型 | 使用场景 |
---|---|
字节流 | 一般用来处理图像,视频,以及PPT,Word类型的文件 字节流可以用来处理纯文本文件,但是仅限于英文、数字等,由于底层基于字节数组byte[]实现,存储范围有限,无法处理复杂编码的文本如中文等 |
字符流 | 一般用于处理纯文本类型的文件,如TXT文件等 字节流可以用来处理纯文本文件,但是字符流不能用于处理图像视频等非文本类型的文件 |
2.2 转换
转换流的作用,文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。
转换流 | 作用 |
---|---|
InputStreamReader | 字节到字符的桥梁,将字节流以字符流输入 |
OutputStreamWriter | 字符到字节的桥梁,将字节流以字符流输出 |
3. java.io包结构
java.io包中核心类结构如下:
3.1 File
File(文件特征与管理):File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹。 File类保存文件或目录的各种元数据信息,包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名,判断指定文件是否存在、获得当前目录中的文件列表,创建、删除文件和目录等方法。
3.2 InputStream
InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
3.2.1 ByteArrayInputStream
字节数组输入流,该类的功能就是从字节数组(byte[])中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去,我们拿也是从这个字节数组中拿。
如上图,ByteArrayOutputStream内部是一个字节数组byte[] buff作为缓冲区进行实现的,它的存储值范围是Byte的取值范围-128~127,因此能存储的格式有限,支持数字、英文字符,但是不支持中文,因此该字节流无法处理中文等复杂字符集,如下摘取了部分byte值与字符的映射表
byte值 | 字符 |
---|---|
48 | 0 |
49 | 1 |
50 | 2 |
51 | 3 |
52 | 4 |
… | … |
97 | a |
98 | b |
99 | c |
… | … |
使用示例如下
/**
* created by guanjian on 2021/1/6 15:08
*/
public class ByteArrayStreamTest {
public static void main(String[] args) throws IOException {
String text = "abc";
ByteArrayOutputStream bOutput = new ByteArrayOutputStream(text.length());
bOutput.write(text.getBytes("UTF-8"));
byte b[] = bOutput.toByteArray();
for (int x = 0; x < b.length; x++) {
// 打印字符
System.out.format((char) b[x] + "\n");
}
System.out.println("========转大写=========");
int c;
ByteArrayInputStream bInput = new ByteArrayInputStream(b);
while ((c = bInput.read()) != -1) {
System.out.println(Character.toUpperCase((char) c));
}
}
}
【控制台输出】
a
b
c
========转大写=========
A
B
C
3.2.2 PipedInputStream
管道字节输入流,它和PipedOutputStream一起使用,能实现多线程间的管道通信。
如上图,PipedInputStream的底层也是基于字节数组byte[] buff进行实现的,它提供了对应的读写线程ReadThread、WriteThread对字节数组进行写入和读取操作,使用Object.wait()、Object.notifyAll() 方法来协调读写线程,当字节数组中数据读取完毕时则阻塞读线程,自旋等待新字节写入后唤醒读线程重新进行读取。
使用示例如下:
/**
* created by guanjian on 2021/1/6 16:26
*/
public class PipedStreamTest {
public static void main(String[] args) throws InterruptedException {
Sender sender = new Sender();
Receiver receiver = new Receiver();
PipedOutputStream outStream = sender.getOutStream();
PipedInputStream inStream = receiver.getInStream();
try {
inStream.connect(outStream); // 与下一句一样
//outStream.connect(inStream);
} catch (Exception e) {
e.printStackTrace();
}
sender.start();
receiver.start();
}
static class Sender extends Thread {
private PipedOutputStream outStream = new PipedOutputStream();
public PipedOutputStream getOutStream() {
return outStream;
}
@Override
public void run() {
IntStream.rangeClosed(0, 10).forEach(x -> {
String info = "hello, receiver";
try {
outStream.write(info.getBytes());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
});
try {
outStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class Receiver extends Thread {
private PipedInputStream inStream = new PipedInputStream();
public PipedInputStream getInStream() {
return inStream;
}
@Override
public void run() {
byte[] buf = new byte[1024];
try {
int len;
while ((len = inStream.read(buf)) != -1) {
System.out.format("receive message from sender : %s \n" , new String(buf, 0, len));
}
inStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
【控制台输出】
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
receive message from sender : hello, receiver
3.2.3 SequenceInputStream
如上图,SequenceInputStream相当于合并了输入流,支持所有实现InputStream的输入流子类,通过实现了Enumeration接口的容器类进行存储遍历合并。
使用示例如下:
/**
* created by guanjian on 2021/1/6 17:42
*/
public class SequenceStreamTest {
public static void main(String[] args) throws IOException {
Vector<ByteArrayInputStream> inputStreams = new Vector<>();
inputStreams.addElement(new ByteArrayInputStream("abc".getBytes()));
inputStreams.addElement(new ByteArrayInputStream("def".getBytes()));
inputStreams.addElement(new ByteArrayInputStream("ghi".getBytes()));
SequenceInputStream sis = new SequenceInputStream(inputStreams.elements());
int c = 0;
while ((c = sis.read()) != -1) {
System.out.print((char)c);
}
}
}
【控制台输出】
abcdefghi
3.2.4 FileInputStream
提供系统文件的输入流方式,底层依赖系统实现。
3.2.5 FilterInputStream
FilterInputStream在装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类。
FilterInputStream 的作用是用来封装其它的输入流,并为它们提供额外的功能。它主要包括BufferedInputStream、DataInputStream。
- BufferedInputStream 为输入流提供缓冲功能,减少底层IO交互,预先批量读取。
- DataInputStream 允许应用程序以与机器无关方式从底层输入流中读写基本 Java 数据类型。
3.2.5.1 BufferedInputStream
如上图,读取字节流时不是直接从处理流(这里用ByteArrayInputStream来作为实际的介质流)获取,而是从BufferedInputStream装饰流中的缓冲区获取
普通的介质流是单个顺序读取的,每次获取一个容量单位的字节,我们知道的是在操作系统中文件是也是按照字节进行存储的,读取系统文件每次读一次就进行一次操作系统和应用之间的IO交互是非常耗时的,而BufferedInputStream的作用就是增加一层缓冲区域,它的处理逻辑不是一个个读取,而是每次读多个先缓存到缓冲区,将多次IO降低,外部不需要和底层直接交互IO,这是典型的空间换时间的思想,增加BufferedInputStream的缓冲区空间提前批量读取来避免频繁底层IO提高性能。
3.2.6 ObjectInputStream
ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流,只不过他继承InputStream而不是FilterInputStream。它的作用是写入对象信息与读取对象信息。 对象信息一旦写到文件上那么对象的信息就可以做到持久化,进行持久化的类必须要实现Serializable接口,这是Java提供的序列化方式。
- 底层依然使用字节数组byte[] 进行存储
- 定义了序列化的格式,classDesc、class、handler等
- 写入(序列化) 是按照顺序存放的,因此写出(反序列化) 也要按照顺序读取
- 被 transient 关键字修饰的类字段不会参与序列化,会被忽略
使用示例如下:
/**
* created by guanjian on 2021/1/6 18:18
*/
public class ObjectStreamTest {
private static final String TMP_FILE = "person.tmp";
public static void main(String[] args) {
testWrite();
testRead();
}
private static void testWrite() {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(TMP_FILE));
//1、基本数据类型
out.writeBoolean(true);
out.writeByte((byte) 65);
out.writeChar('a');
out.writeInt(20131015);
out.writeFloat(3.14F);
out.writeDouble(1.414D);
out.writeUTF("我是字符串");
//2、复杂数据结构
Map<String, String> map = new HashMap<String, String>();
map.put("one", "red");
map.put("two", "green");
map.put("three", "blue");
out.writeObject(map);
//3、写入自定义的Person对象,实现了Serializable接口
Person person = new Person("zhangsan", 30, true, 'm');
out.writeObject(person);
out.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* ObjectInputStream 测试函数
*/
private static void testRead() {
try {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(TMP_FILE));
//1、基本数据类型
System.out.printf("boolean:%b\n", in.readBoolean());
System.out.printf("byte:%d\n", (in.readByte() & 0xff));
System.out.printf("char:%c\n", in.readChar());
System.out.printf("int:%d\n", in.readInt());
System.out.printf("float:%f\n", in.readFloat());
System.out.printf("double:%f\n", in.readDouble());
System.out.printf("String:%s\n", in.readUTF());
//2、复杂数据结构
HashMap map = (HashMap) in.readObject();
Iterator<?> iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
System.out.printf("%-6s -- %s\n", entry.getKey(), entry.getValue());
}
//3、读取Person对象,实现了Serializable接口
Person person = (Person) in.readObject();
System.out.println("Person: " + person);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
static class Person implements Serializable {
private static final long serialVersionUID = 5512460159096320796L;
private String name;
private int age;
private boolean isAlive;
private char gender;
public Person(String name, int age, boolean isAlive, char gender) {
this.name = name;
this.age = age;
this.isAlive = isAlive;
this.gender = gender;
}
public Person() {
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", isAlive=" + isAlive +
", gender=" + gender +
'}';
}
}
}
3.3 OutputStream
OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
关于OutputStream的子类都是对应以上InputStream的子类,这里不再赘述。
3.4 Reader
Reader(文件格式操作):抽象类,基于字符的输入操作。
3.4.1 StringReader
如上图,StringReader中使用String来进行存储,读取时候会将String转换为char字符读取
3.4.2 CharArrayReader
如上图,CharArrayReader的底层是采用字符数组char[],char类型在内存中占2个字节-16位,其值的范围在0-65535之间
3.4.3 BufferedReader
参考BufferedInputStream部分,原理类似
3.4.4 PipedReader
参考PipedInputStream部分,原理类似
3.5 Writer
Writer(文件格式操作):抽象类,基于字符的输出操作。关于Writer的子类都是对应以上Reader的子类,这里不再赘述。
3.6 RandomAccessFile
RandomAccessFile(随机文件操作):一个独立的类,直接继承至Object.它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
类 | 限制 | 流向 | 读写 |
---|---|---|---|
InputStream | 字节流(字符流可转字节流) | 游标顺序 | 读 |
OutputStream | 字节流(字符流可转字节流) | 游标顺序 | 写 |
Reader | 字符流(字节流可转字符流) | 游标顺序 | 读 |
Writer | 字符流(字节流可转字符流) | 游标顺序 | 写 |
RandomAccessFile | 文件 | 随机、指定位置 | 读、写 |
4. 实例
4.1 Socket连接
单线程服务端Socket实现
服务端
/**
* 每次只能处理一个连接请求,其他连接请求会被阻塞
* created by guanjian on 2021/1/12 9:09
*/
public class SingleThreadBIOSocketServer {
private static byte[] bytes = new byte[1024];
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket socketServer = new ServerSocket();
socketServer.bind(new InetSocketAddress(9090));
System.out.println("Server starting ...");
while (true) {
System.out.println("Server waiting connect ...");
//accept方法会阻塞
Socket socket = socketServer.accept();
long start = System.currentTimeMillis();
System.out.println("Server begin receive data ...");
//read方法会阻塞
socket.getInputStream().read(bytes);
long end = System.currentTimeMillis();
System.out.format("Server finish receive data: %s , cost: %s \n", new String(bytes), end - start);
}
}
}
客户端
/**
* 单独一个socket请求
* created by guanjian on 2021/1/12 9:09
*/
public class SingleThreadBIOSocketClient {
public static void main(String[] args) throws IOException {
Socket socketClient = new Socket();
socketClient.connect(new InetSocketAddress("127.0.0.1", 9090));
System.out.println("Client connected ...");
//会阻塞socket等待输入
Scanner scanner = new Scanner(System.in);
String input = scanner.next();
socketClient.getOutputStream().write(input.getBytes());
System.out.format("Client sending content:%s \n", input);
}
}
多线程服务端Socket实现
服务端
/**
* 开启多线程来接收处理Socket请求
* 不会被阻塞socket请求,但是会耗费大量线程资源
*
* created by guanjian on 2021/1/12 9:09
*/
public class MultiThreadBIOSocketServer {
private static byte[] bytes = new byte[1024];
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket socketServer = new ServerSocket();
socketServer.bind(new InetSocketAddress(9090));
System.out.println("Server starting ...");
while (true) {
System.out.println("Server waiting connect ...");
//accept方法会阻塞
Socket socket = socketServer.accept();
//接收请求后开启线程来处理,避免accept阻塞
if (null != socket) {
new SockectServerHandler(socket).start();
}
}
}
static class SockectServerHandler extends Thread {
private Socket socket;
public SockectServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
long start = System.currentTimeMillis();
System.out.println("Server begin receive data ...");
//read方法会阻塞
socket.getInputStream().read(bytes);
long end = System.currentTimeMillis();
System.out.format("Server finish receive data: %s , cost: %s \n", new String(bytes), end - start);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
/**
* 模拟多线程并发socket请求
* created by guanjian on 2021/1/12 9:09
*/
public class MultiThreadBIOSocketClient {
private static CyclicBarrier cb = new CyclicBarrier(10, () -> {
System.out.println("===== Client Multi Threads Begin Request =====");
});
public static void main(String[] args) throws IOException {
IntStream.range(0, 10).forEach(t -> {
new SockectClient(t).start();
});
}
static class SockectClient extends Thread {
private int seq;
public SockectClient(int seq) {
this.seq = seq;
}
@Override
public void run() {
Socket socketClient = new Socket();
try {
cb.await(5, TimeUnit.SECONDS);
socketClient.connect(new InetSocketAddress("127.0.0.1", 9090));
System.out.println("Client connected.");
String sendMsg = "你好 " + seq;
System.out.format("Client sending content:%s \n", sendMsg);
socketClient.getOutputStream().write(sendMsg.getBytes());
socketClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
总结
通过BIO进行Socket通信会产生阻塞,阻塞点如下:
- socket.accept() 会阻塞
- socket.getInputStream().read(bytes) 会阻塞
4.2 File读取
5. 参考
https://blog.csdn.net/sinat_37064286/article/details/86537354
https://blog.csdn.net/ro_wsy/article/details/8207527
https://blog.csdn.net/weixin_30239339/article/details/99439679
https://www.cnblogs.com/myseries/p/10757033.html
https://blog.csdn.net/qq_40208605/article/details/80647803