IO 到底是什么?
IO 就是 Input/Output------输入输出,要清楚概念,关键在于要知道所谓的输入输出是以程序内存(代码中的变量、对象)为主体的。
程序内存就是内部,程序内存之外的则是外部,比如本地文件 、网络连接、其他进程等。
所以:
- 输入指的是把数据从外部(如文件)读到内存中。
- 输出指的是把数据从内存写到外部(如文件)。
传统 IO(java.io)
传统 IO 操作都是阻塞的,它的设计核心是流(Stream)。
形象点来说,流就像一条管道,数据会流动于管道中,我们通过管道来存取数据。
字节流
字节流有着 InputStream 和 OutputStream,它们内部流动的是字节,我们通常用于处理原始的二进制数据,比如图片、音频。
java
/**
* 字节流示例
*/
private static void IOStreamDemo() {
// 往文件写入数据
File file = new File("./Text");
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
outputStream.write("hello world!\r\n你好啊,世界!".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close(); // 关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 从文件读取数据
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int len; // 读取的字节数
while ((len = inputStream.read(buffer)) != -1) {
String dataChunk = new String(buffer, 0, len, StandardCharsets.UTF_8);
System.out.print(dataChunk);
}
} catch (IOException e) {
e.printStackTrace();
}
}
当文件被打开后,会在内存中保存文件的相关信息(如文件描述符或句柄)。我们需要手动关闭文件,具体是在 finally 块中调用流的 close() 函数。如果不关闭,会导致资源泄露。当打开的文件过多时,程序将无法再打开新文件,最终导致崩溃。
上面的代码展示了两种关闭流的写法:第一种是标准写法。第二种写法利用了 Java7 新增的 try-with-resources 特性,它能够自动关闭所用到的资源。
字符流
字符流有着 Reader 和 Writer,它们内部流动的是字符,我们专门用它们来处理文本数据。
字节只是计算机物理存储的最小单位,有 8 个比特位。而字符是人类语言的抽象概念,大小由编码决定。
java
/**
* 字符流示例
*/
private static void ReaderWriterDemo() {
// 往文件写入数据
File file = new File("./Text.txt");
try (OutputStream outputStream = new FileOutputStream(file);
Writer writer = new OutputStreamWriter(outputStream)) {
writer.write("hello world!");
} catch (IOException e) {
e.printStackTrace();
}
// 从文件读取数据
try (InputStream inputStream = new FileInputStream(file);
Reader reader = new InputStreamReader(inputStream)) {
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
for (int i = 0; i < len; i++) {
char ch = buffer[i];
System.out.print(ch);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
缓冲流
字节流是连接到文件的管道;字符流包着字节流,会将流过的字节按照指定编码(默认是 UTF-8)转换为字符;
虽然我们在字节流和字符流的示例中,手动创建了字节数组来分块读取,但这和 BufferedStream 缓冲流的缓冲机制是不同的。
缓冲流包着字符流,它内部自带了一个缓冲区(默认大小为 8192),它能自动预读数据尽可能填满缓冲区,从而减少与外部(如磁盘)IO 的次数,极大提升性能。同时,它还提供了 readLine 等便捷函数。
java
/**
* 缓冲流示例
*/
private static void BufferedStreamDemo() {
// 写入数据到文件
File file = new File("./Text.txt");
try (OutputStream outputStream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(writer);) {
bufferedWriter.write("hello io!");
bufferedWriter.flush(); // 刷新缓冲区
} catch (IOException e) {
e.printStackTrace();
}
// 从文件中读取数据
try (InputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader);
) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
使用缓冲输出流时,数据会先写入到内存中,并没有写入到文件。调用 flush() 是为了让缓冲区中的数据能够及立即写入文件。当缓冲区满了或者输出流被关闭时,缓冲区会自动刷新,也就是自动调用一次 flush()。
文件复制
要完成文件复制,我们可以调用 android.os.FileUtils.copy() 或者 kotlin.io.File.copyTo() 扩展函数。
如果要自己完成,也不难,只需复制一个文件的所有字节到另一个文件即可。
java
public class Main {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("Text.txt");
OutputStream outputStream = new FileOutputStream("NewText.txt")) {
long progress = myFileCopy(inputStream, outputStream);
System.out.println("File Copied, Total Size: " + progress + " bytes");
} catch (IOException e) {
System.err.println("Error: An IO exception occurred during file copying");
e.printStackTrace();
}
}
public static long myFileCopy(InputStream src, OutputStream dest) throws IOException {
if (src == null) {
throw new NullPointerException("Src stream is null");
}
if (dest == null) {
throw new NullPointerException("Dest stream is null");
}
byte[] buffer = new byte[8192];
long progress = 0;
int len;
while ((len = src.read(buffer)) != -1) {
dest.write(buffer, 0, len);
progress += len;
}
return progress;
}
}
Socket
在与网络进行交互时,使用的也是 IO 流,比如说:
客户端:
java
// SocketClientDemo.java
void main() {
SocketClientDemo();
}
private static final int PORT = 8080;
private static final String HOST = "127.0.0.1";
public static void SocketClientDemo() {
IO.println("Client: Connecting to " + HOST + ":" + PORT + "...");
try (Socket socket = new Socket(HOST, PORT);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in))) {
IO.println("Client: Connected. Please enter a message (enter 'exit'):");
String userInput;
while ((userInput = consoleReader.readLine()) != null) {
// 将用户输入发送到服务器
writer.write(userInput);
writer.newLine(); // 添加换行符
writer.flush();
// 退出
if ("exit".equals(userInput)) {
break;
}
// 读取服务器的回显
String line = reader.readLine();
IO.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
IO.println("Client: The connection is closed.");
}
服务端:
java
// SocketServerDemo.java
void main() {
SocketServerDemo();
}
private static final int PORT = 8080;
public static void SocketServerDemo() {
IO.println("Server: Starting, listening on port " + PORT + "...");
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
try (Socket socket = serverSocket.accept();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
IO.println("Server: Client connected: " + socket.getRemoteSocketAddress());
String clientMessage;
while ((clientMessage = reader.readLine()) != null) {
// 退出
if ("exit".equalsIgnoreCase(clientMessage)) {
break;
}
// 回显消息
String echoMessage = "Echo: " + clientMessage;
// 将消息发回客户端
writer.write(echoMessage);
writer.newLine();
writer.flush();
}
}
IO.println("Server: The client connection is closed.");
} catch (IOException e) {
e.printStackTrace();
}
IO.println("Server: The server is down.");
}
在客户端控制台输入消息,服务端会回显对应消息:
vbnet
Client: Connecting to 127.0.0.1:8080...
Client: Connected. Please enter a message (enter 'exit'):
你好!
Echo: 你好!
今天天气不错呢?
Echo: 今天天气不错呢?
NIO (java.nio)
NIO(New IO)和传统 IO 不同的是:
- 它使用的是
Channel,这个通道是双向的(可读可写)。 Buffer缓冲区被强制使用。- 具有
Selector选择器,可实现非阻塞网络 IO 操作。
我们先来了解一下它的 Buffer,缓冲区有三个指针:position, limit, capacity。分别表示了:当前的读写位置,读写的上限(默认等于容量),缓冲区的容量。
所以一个简单的读数据操作,需要这样:
java
/**
* NIO示例
*/
private static void NIODemo() {
// 从文件中读取数据
try (RandomAccessFile file = new RandomAccessFile("Text.txt", "r");) {
FileChannel channel = file.getChannel();
// 分配字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) { // 读取数据
// 修改读取的范围,这两行代码等价于 buffer.flip();
buffer.limit(buffer.position());
buffer.position(0);
// 解码打印
System.out.print(StandardCharsets.UTF_8.decode(buffer));
// 清空缓冲区,以便下次使用
buffer.clear();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
不仅非常麻烦,还要手动管理指针位置。
非阻塞式
其实 NIO 并不是提供给客户端使用的。它的非阻塞式网络 IO,配合上 Selector,能够让单个线程监视成百上千个网络连接事件。这在服务端非常有用,避免了为每一个连接都开启一个线程。
下面这段代码只需粗略看看即可,我们无需了解这么多,你也可以将 NIO 给彻底忘了。
java
private static final int PORT = 8080;
private static final int BUFFER_SIZE = 1024;
/**
* 使用 NIO 和 Selector 实现的 Socket 服务器
*/
public static void SocketServerNioDemo() {
IO.println("NIO Server: Starting, listening on port " + PORT + "...");
try (// 打开 Selector
Selector selector = Selector.open();
// 打开 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open()
) {
// 绑定端口并设置为非阻塞
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
// 将 ServerChannel 注册到 Selector,监听连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
while (true) {
// 阻塞等待事件发生
selector.select();
// 遍历所有已就绪的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
// 根据事件类型分别处理
if (key.isAcceptable()) {
// 有新的客户端连接
handleAccept(key, selector);
}
if (key.isReadable()) {
// 有客户端发送数据
handleRead(key, buffer);
}
} catch (IOException e) {
// 客户端异常断开
System.err.println("Server: Client connection error: " + e.getMessage());
closeClientConnection(key);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 处理新的客户端连接
*/
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 非阻塞接受连接
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false); // 必须设置客户端 Channel 也为非阻塞
// 将新的客户端 Channel 注册到 Selector,监听 "READ" 事件
clientChannel.register(selector, SelectionKey.OP_READ);
IO.println("Server: Client connected: " + clientChannel.getRemoteAddress());
}
/**
* 处理来自客户端的数据读取
*/
private static void handleRead(SelectionKey key, ByteBuffer buffer) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 清空缓冲区,准备写入
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端正常关闭了连接
IO.println("Server: Client disconnected cleanly.");
closeClientConnection(key);
return;
}
if (bytesRead > 0) {
// 切换到读模式
buffer.flip();
// 解码
String clientMessage = StandardCharsets.UTF_8.decode(buffer).toString().trim();
IO.println("Server: Received: "" + clientMessage + """);
if ("exit".equalsIgnoreCase(clientMessage)) {
// 客户端请求退出
closeClientConnection(key);
return;
}
// 回显消息
String echoMessage = "Echo: " + clientMessage;
// 加上换行符
ByteBuffer echoBuffer = StandardCharsets.UTF_8.encode(echoMessage + "\n");
// 将消息写回客户端
while (echoBuffer.hasRemaining()) {
clientChannel.write(echoBuffer);
}
}
}
/**
* 关闭客户端连接
*/
private static void closeClientConnection(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
IO.println("Server: Closing connection for: " + clientChannel.getRemoteAddress());
// 从 Selector 中取消注册
key.cancel();
// 关闭 Channel
clientChannel.close();
}
OkIO
OkIO 只是传统 IO 的封装,它提供了两个核心接口:
Source:数据的来源,对应InputStream。Sink:数据的聚集地,对应OutputStream。
OkIO 也支持 Buffer,它既是 Source 又是 Sink。
首先引入依赖:
kotlin
implementation("com.squareup.okio:okio:3.16.2")
然后来简单读取文件:
java
public static void OkIODemo() {
File file = new File("file.txt");
if (!file.exists()) {
try {
boolean fileCreated = file.createNewFile();
if (!fileCreated) {
throw new FileNotFoundException("File creation failed");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try (Source source = Okio.source(file);
Buffer buffer = new Buffer()) {
// 只要 Source 还有数据,就往 buffer 里读
while (source.read(buffer, 8192) != -1) {
// 处理 buffer 中所有完整的行
String line;
while ((line = buffer.readUtf8Line()) != null) {
System.out.println(line);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}