一、为什么使用带缓冲的读写?
在 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