Java BIO详解
一、简介
1.1 BIO概述
BIO(Blocking I/O),即同步阻塞IO(传统IO)。
BIO 全称是 Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型,就是传统的 java.io 包下面的代码实现。
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:
在 BIO 模型下,应用程序会在进行 I/O 操作时阻塞当前线程,直到 I/O 操作完成。例如,执行一个读取操作时,线程会等待,直到数据从磁盘或网络中完全读取完成。在这个过程中,线程不能做其他任务,必须等待 I/O 操作的结果。
BIO 模型的特点
- 同步阻塞:
- 当线程进行 I/O 操作时,它会被阻塞,直到操作完成。
- 阻塞操作通常会导致 CPU 的浪费,因为线程在等待 I/O 时并没有进行其他有用的工作。
- 一个连接一个线程:
- 每个客户端请求都会创建一个新的线程,每个线程对应一个 I/O 操作。
- 当并发连接数很多时,系统可能会因为线程数过多而导致性能瓶颈。
- 适用于连接数较少的场景:BIO 更适用于连接数较少、请求不频繁的应用场景,如一些小型应用或传统的阻塞式通信。
- 容易实现:相对于其他 I/O 模型(如 NIO 和 AIO),BIO 的实现比较简单,应用开发人员只需要关心 I/O 操作,不需要处理复杂的事件驱动机制。
BIO 的缺点:
- 线程资源浪费:每个连接都会对应一个独立的线程,当有大量并发连接时,会导致系统开销巨大,因为操作系统会为每个线程分配资源(如内存、栈空间等)。如果并发请求量很大,线程上下文切换开销也会非常高。
- 不适合高并发:BIO 模型非常依赖操作系统线程,线程数过多时容易造成系统性能下降。特别是在高并发的情况下,线程的创建和销毁频繁,容易耗尽系统资源。
- 效率低:在 I/O 操作过程中,线程被阻塞,无法处理其他请求,导致 CPU 的浪费。即使没有数据可读或可写,线程依然会等待,直到 I/O 完成。
1.2 IO流概述
IO流是基于流的概念,它将数据的输入和输出看作是一个连续的流。数据从一个地方流向另一个地方,流的方向可以是输入(读取数据)或输出(写入数据)。
IO流的原理是通过流的管道将数据从源头传输到目标地。源头可以是文件、网络连接、内存等,而目标地可以是文件、数据库、网络等。IO流提供了一组丰富的类和方法来实现不同类型的输入和输出操作。
IO流主要用于处理输入和输出操作,适用于以下场景:
- 文件读写:通过IO流可以读取和写入文件中的数据,如读取配置文件、写入日志等。
- 网络通信:通过IO流可以进行网络数据的传输和接收,如Socket通信、HTTP请求等。
- 数据库操作:通过IO流可以将数据读取到内存中,或将内存中的数据写入到数据库中。
- 文本处理:通过IO流可以读取和写入文本文件,进行文本处理和操作。
1.2.1 IO流分类
Java中的IO流可以按照数据的类型和流的方向进行分类:
- 按数据类型分类
- 字节流(Byte Stream):以字节为单位读写数据,适用于处理二进制数据,如图像、音频、视频等。常见的字节流类有InputStream和OutputStream。
- 字符流(Character Stream):以字符为单位读写数据,适用于处理文本数据。字符流会自动进行字符编码和解码,可以处理多国语言字符。常见的字符流类有Reader和Writer。
- 按流的方向分类
- 输入流(Input Stream):用于读取数据。输入流从数据源读取数据,如文件、网络连接等。常见的输入流类有FileInputStream、ByteArrayInputStream、SocketInputStream等。
- 输出流(Output Stream):用于写入数据。输出流将数据写入到目标地,如文件、数据库、网络等。常见的输出流类有FileOutputStream、ByteArrayOutputStream、SocketOutputStream等。
1.2.1.1 字符流
- 只用来处理文本数据 ;
- 数据最常见的表现形式是文件,字符流用来操作文件的子类一般是 FileReader 和 FileWriter 。
字符流读写文件注意事项:
- 写入文件必须要用 flush() 刷新 ;
- 用完流记得要关闭流 ;
- 使用流对象要抛出IO异常 ;
- 定义文件路径时,可以用"/"或者"\" ;
- 在创建一个文件时,如果目录下有同名文件将被覆盖 ;
- 在读取文件时,必须保证该文件已存在,否则抛出异常 ;
- 字符流的缓冲区 ;
- 缓冲区的出现是为了提高流的操作效率而出现的 ;
- 需要被提高效率的流作为参数传递给缓冲区的构造函数 ;
- 在缓冲区中封装了一个数组,存入数据后一次取出 。
1.2.1.2 字节流
- 用来处理媒体数据 。
字节流读写文件注意事项:
- 字节流和字符流的基本操作是相同的,但是想要操作媒体流就需要用到字节流 ;
- 字节流因为操作的是字节,所以可以用来操作媒体文件(媒体文件也是以字节存储的);
- 输入流(InputStream)、输出流(OutputStream);
- 字节流操作可以不用刷新流操作 ;
- InputStream特有方法:int available()(返回文件中的字节数);
- 字节流的缓冲区 ;
- 字节流缓冲区跟字符流缓冲区一样,也是为了提高效率 。
1.2.2 常用IO流
1.2.2.1 字节输入流类
- InputStream:用于从输入源读取字节数据的抽象类。
- FileInputStream:从文件中读取字节数据的类。
- ByteArrayInputStream:从字节数组中读取字节数据的类。
- BufferedInputStream:提供缓冲功能的字节输入流类。
- DataInputStream:读取基本数据类型的字节输入流类。
try (InputStream is = new FileInputStream("input.txt");
BufferedInputStream bis = new BufferedInputStream(is)) {
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
1.2.2.2 字符输入流类
- Reader:用于从输入源读取字符数据的抽象类。
- FileReader:从文件中读取字符数据的类。
- BufferedReader:提供缓冲功能的字符输入流类。
- InputStreamReader:将字节流转换为字符流的类。
try (Reader reader = new FileReader("input.txt");
BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
1.2.2.3 字节输出流类
- OutputStream:用于向输出目标写入字节数据的抽象类。
- FileOutputStream:将字节数据写入文件的类。
- ByteArrayOutputStream:将字节数据写入字节数组的类。
- BufferedOutputStream:提供缓冲功能的字节输出流类。
- DataOutputStream:将基本数据类型写入输出流的类。
try (OutputStream os = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(os)) {
String data = "Hello, World!";
bos.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
1.2.2.4 字符输出流类
- Writer:用于向输出目标写入字符数据的抽象类。
- FileWriter:将字符数据写入文件的类。
- BufferedWriter:提供缓冲功能的字符输出流类。
- OutputStreamWriter:将字节流转换为字符流的类。
try (Writer writer = new FileWriter("output.txt");
BufferedWriter bw = new BufferedWriter(writer)) {
String data = "Hello, World!";
bw.write(data);
} catch (IOException e) {
e.printStackTrace();
}
1.2.3 Java Scanner类
Java 5添加了 java.util.Scanner 类,这是一个用于扫描输入文本的新的实用程序。
- 关于 nextInt()、next()、nextLine() 的理解 :
- nextInt() :只能读取数值,若是格式不对,会抛出 java.util.InputMismatchException 异常 ;
- next() :遇见第一个有效字符(非空格,非换行符)时,开始扫描,当遇见第一个分隔符或结束符(空格或换行符)时,结束扫描,获取扫描到的内容 ;
- nextLine() :可以扫描到一行内容并作为字符串而被捕获到 。
- 关于 hasNext()、hasNextLine()、hasNextxxx() 的理解 :就是为了判断输入行中是否还存在xxx的意思。
- 与 delimiter() 有关的方法的理解 :应该是输入内容的分隔符设置,
二、BIO工作机制
客户端:
- 通过Socket对象请求与服务端建立连接。
- 从Socket中得到字节输入或者字节输出流进行数据读写操作。
服务端:
- 通过ServerSocket注册端口。
- 服务端通过调用accept方法用于监听客户端的Socket请求。
- 从Socket中得到字节输入或者字节输出流进行数据读写操作。
三、简单编码实现
3.1 服务端
public class BioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
System.out.println("Server is listening on port 8080...");
while (true) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress());
// 为每个客户端请求创建一个新线程进行处理
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(serverSocket != null){
serverSocket.close();
}
} catch (IOException e){
e.printStackTrace();
}
}
}
}
class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedInputStream bufferedInputStream = null;
BufferedOutputStream bufferedOutputStream = null;
try {
bufferedInputStream = new BufferedInputStream(socket.getInputStream());;
bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
System.out.println("收到来自客户端的消息:");
byte[] bytes = new byte[1024]; //创建字节数组,存储临时读取的数据
int len ; //记录数据读取的长度
if ((len = bufferedInputStream.read(bytes)) > -1) { //长度为-1则读取完毕
String result = new String(bytes,0,len);
System.out.println(result);
}
String outString = "服务端收到,这里是线程" + Thread.currentThread().getName();
System.out.println("回复信息给客户端: " + outString);
bufferedOutputStream.write(outString.getBytes());
bufferedOutputStream.flush();
System.out.println("回复完成========");
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭数据流========");
try {
if (bufferedInputStream != null) {
bufferedInputStream.close();
}
if (bufferedOutputStream != null) {
bufferedOutputStream.close();
}
} catch (IOException e){
e.printStackTrace();
}
}
}
}
工作过程:
- 服务器通过 ServerSocket 对象监听 8080 端口,等待客户端连接。
- 每当有一个客户端连接到服务器时,serverSocket.accept() 会阻塞当前线程,直到有客户端连接进来。
- 然后,服务器会为每个客户端连接创建一个新线程来处理该客户端的请求。
- 线程通过 BufferedInputStream 读取客户端发送的数据,并通过 BufferedOutputStream 回复客户端。
3.2 客户端
public class BioClient {
public static void start() throws IOException {
Socket socket = new Socket("127.0.0.1", 8080);
String msg = "Hi,This is the BioClient";
byte[] msgBytes = msg.getBytes();
//1.拿到输出流
//2.对输出流进行处理
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
//3.输出msg
bufferedOutputStream.write(msgBytes);
bufferedOutputStream.flush();
System.out.println("客户端发送消息完毕: " + msg);
System.out.println("客户端开始接收到消息==========");
//4.对输入流进行处理
BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream());
System.out.println("客户端接收到的消息:");
byte[] inBytes = new byte[1024];
int len;
//5.读取输入流
if ((len = bufferedInputStream.read(inBytes)) != -1) {
String result = new String(inBytes, 0, len);
System.out.println(result);
}
//6.关闭输入输出流
bufferedOutputStream.close();
bufferedInputStream.close();
socket.close();
}
public static void main(String[] args) throws IOException {
start();
}
}
工作过程:
- 与服务端建立连接
- 发送消息给服务端
- 接收服务端返回的消息