一、为什么使用带缓冲的读写?
在 Rust 的异步编程中,AsyncReadExt
和 AsyncWriteExt
提供了方便的读写方法。然而,每次调用这些方法,都会向操作系统发起系统调用。如果处理大量数据时,每次只读或写少量字节,就会频繁地切换上下文,造成 CPU 时间浪费,降低 IO 效率。
缓冲读写的好处
- 减少系统调用次数:数据会先写入缓冲区,当缓冲满了或条件满足时才向操作系统发起系统调用。
- 按行读取:缓冲读操作可以识别换行符并按行读取数据,这比按字节读取更方便。
- 高效的数据处理:减少 CPU 资源浪费,提高 IO 性能。
二、使用 BufReader
和 BufWriter
实现缓冲读写
tokio::io
模块提供了 BufReader
和 BufWriter
,用于为 Reader 和 Writer 添加缓冲功能。
BufReader 读取文件按行打印
use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}, runtime};
fn main() {
let rt = runtime::Runtime::new().unwrap();
rt.block_on(async {
let f = File::open("a.log").await.unwrap();
let mut lines = BufReader::new(f).lines();
while let Some(line) = lines.next_line().await.unwrap() {
println!("read line: {}", line);
}
});
}
解释:
BufReader
读取文件时,会将内容缓存在内部缓冲区中。- 按行读取:
lines()
方法将内容按行分割,next_line()
异步获取下一行。
BufWriter 写文件
use tokio::{fs::File, io::{AsyncWriteExt, BufWriter}, runtime};
fn main() {
let rt = runtime::Runtime::new().unwrap();
rt.block_on(async {
let f = File::create("output.txt").await.unwrap();
let mut writer = BufWriter::new(f);
writer.write_all(b"Hello, world!\n").await.unwrap();
writer.flush().await.unwrap(); // 确保缓冲区数据写入文件
});
}
解释:
BufWriter
将数据写入缓冲区,当缓冲区满或调用flush()
时,才会将数据写入文件。- **
write_all()
**:写入字节数据。
三、按自定义分隔符读取数据:split()
split()
可以将读取的内容按指定的字节分隔符分割成多个片段。
示例:按换行符分割文件
use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}, runtime};
fn main() {
let rt = runtime::Runtime::new().unwrap();
rt.block_on(async {
let f = File::open("a.log").await.unwrap();
let mut segments = BufReader::new(f).split(b'\n');
while let Some(segment) = segments.next_segment().await.unwrap() {
println!("segment: {}", String::from_utf8(segment).unwrap());
}
});
}
解释:
- **
split(b'\n')
**:按换行符分割文件内容。 - **每次调用 **
next_segment()
获取一个片段,返回Vec<u8>
。
四、随机读写文件:Seek
tokio::io::AsyncSeekExt
提供了 随机读写 功能,可以设置文件的偏移位置,实现非顺序的读写。
示例:设置偏移位置读取文件
use std::io::SeekFrom;
use tokio::{fs::File, io::{AsyncReadExt, AsyncSeekExt}, runtime};
fn main() {
let rt = runtime::Runtime::new().unwrap();
rt.block_on(async {
let mut f = File::open("a.log").await.unwrap();
f.seek(SeekFrom::Start(5)).await.unwrap(); // 从第5个字节开始读取
let mut content = String::new();
f.read_to_string(&mut content).await.unwrap();
println!("Data: {}", content);
f.rewind().await.unwrap(); // 将偏移指针重置到文件开头
});
}
解释:
- **
seek()
**:将偏移指针移动到指定位置。 - **
rewind()
**:重置偏移指针到文件开头。
五、标准输入输出
tokio::io
提供了 stdin()
和 **stdout()
**,用于异步读取标准输入和输出。
示例:读取用户输入并回显
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, runtime};
fn main() {
let rt = runtime::Runtime::new().unwrap();
rt.block_on(async {
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let mut buffer = vec![0; 1024];
loop {
stdout.write(b"Enter something: ").await.unwrap();
stdout.flush().await.unwrap();
let n = stdin.read(&mut buffer).await.unwrap();
if n == 0 {
break; // 用户输入结束
}
stdout.write(&buffer[..n]).await.unwrap();
stdout.flush().await.unwrap();
}
});
}
解释:
- 读取用户输入:
stdin.read()
从标准输入读取数据。 - 输出到终端:
stdout.write()
将数据回显。
六、全双工通信:DuplexStream
tokio::io::duplex()
提供了类似套接字的全双工管道,支持读写同时进行。
示例:模拟客户端和服务端的通信
use tokio::{io::{self, AsyncReadExt, AsyncWriteExt}, runtime, time};
#[tokio::main]
async fn main() {
let (mut client, mut server) = io::duplex(64); // 创建双向管道
// 启动一个任务:服务端发送消息
tokio::spawn(async move {
server.write_all(b"Hello from server").await.unwrap();
});
// 客户端读取来自服务端的消息
let mut buf = vec![0; 64];
let n = client.read(&mut buf).await.unwrap();
println!("Client received: {}", String::from_utf8_lossy(&buf[..n]));
}
解释:
- **
duplex()
**:创建全双工读写管道。 - 客户端和服务端通信:服务端写入数据,客户端读取数据。
七、拆分 Reader 和 Writer:split()
split()
方法可以将一个可读写的目标(如 TcpStream
)拆分为 读半部分 和 写半部分。
示例:拆分 DuplexStream
use tokio::{io::{self, AsyncReadExt, AsyncWriteExt}, runtime};
#[tokio::main]
async fn main() {
let (client, server) = io::duplex(64);
let (mut reader, writer) = io::split(client); // 拆分为读和写部分
// 关闭不使用的写部分
drop(writer);
let mut buf = vec![0; 64];
let n = reader.read(&mut buf).await.unwrap();
println!("Read from server: {}", String::from_utf8_lossy(&buf[..n]));
}
八、总结
**在这一部分,教程介绍了如何使用 **Tokio 实现异步 IO,包括:
- **
BufReader
和BufWriter
**:用于高效的缓冲读写。 - w按行读取和自定义分隔符:方便处理文本数据。
- 随机读写文件:通过设置偏移指针实现非顺序读写。
- 标准输入输出:异步处理用户输入和终端输出。
- 全双工通信:通过
DuplexStream
实现双向数据传输。 - 拆分 Reader 和 Writer:提高代码的灵活性。w