18. Input/Output — 输入输出
Rust 的 I/O 系统围绕三个核心 trait 组织:Read(字节输入)、Write(字节输出)和 BufRead(缓冲输入)。这些 trait 为文件、网络连接、内存缓冲区、管道、终端等提供了统一接口——一旦理解了这个模型,所有 I/O 操作都遵循相同的模式。本章从 trait 设计出发,覆盖文件操作、路径处理、目录操作和网络初探。
核心 Trait 体系
Rust I/O 的类型关系如下:
Read (字节读取)
├── File, TcpStream, Stdin, Cursor<&[u8]>
├── BufReader<R> (包装任意 Read 实现者为 BufRead)
├── &[u8] (内存字节数组)
│
BufRead (缓冲读取,继承 Read)
├── BufReader<R>
├── StdinLock
├── Cursor<&[u8]>
│
Write (字节写入)
├── File, TcpStream, Stdout, Stderr
├── Vec<u8> (数据追加到尾部)
├── BufWriter<W> (缓冲写入包装器)
├── Cursor<&mut [u8]>
└── Sink (丢弃所有写入)导入 I/O 相关 trait 的惯用方式:
use std::io::prelude::*;
use std::io::{self, Read, Write, ErrorKind};Read Trait
Read 定义了从字节源读取数据的方法:
pub trait Read {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
// 提供默认实现的方法
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> { ... }
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> { ... }
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { ... }
// 适配器方法
fn bytes(self) -> Bytes<Self> { ... }
fn chain<R: Read>(self, next: R) -> Chain<Self, R> { ... }
fn take(self, limit: u64) -> Take<Self> { ... }
}read:最底层方法
read 尝试读取一些字节到缓冲区,返回实际读取的字节数。返回 Ok(0) 表示输入结束:
use std::io::Read;
fn read_exact_data(mut reader: impl Read) -> io::Result<Vec<u8>> {
let mut buf = [0u8; 1024];
let mut data = Vec::new();
loop {
match reader.read(&mut buf) {
Ok(0) => break, // EOF
Ok(n) => data.extend_from_slice(&buf[..n]),
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(data)
}关键行为:
read可能读取少于请求的字节数——依赖返回值来确定实际读取量ErrorKind::Interrupted需要特殊处理——表示系统调用被信号中断,应重试- 返回
Ok(0)是唯一的 EOF 信号
read_to_end 和 read_to_string
一次性读取所有内容:
use std::io::Read;
// 读取全部字节到 Vec<u8>
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
// 读取全部文本到 String(要求有效 UTF-8)
let mut text = String::new();
reader.read_to_string(&mut text)?;
// 安全注意:对不受信任的数据源使用 .take() 限制读取量
let safe_read = reader.take(10 * 1024 * 1024); // 限制 10MBread_exact
读取恰好填满缓冲区——与 read 不同,它保证要么读满要么以 UnexpectedEof 错误返回:
let mut header = [0u8; 16];
reader.read_exact(&mut header)?; // 不到 16 字节则出错适配器方法
// bytes()——返回逐字节迭代器
for byte_result in reader.bytes() {
let byte = byte_result?;
println!("{byte:#04x}");
}
// chain()——连接两个 reader
let combined = file1.chain(file2);
// take()——限制读取量
let limited = reader.take(1024); // 最多读 1024 字节BufRead Trait
BufRead 继承 Read,为缓冲 reader 添加按行读取等高级操作:
pub trait BufRead: Read {
fn read_line(&mut self, buf: &mut String) -> io::Result<usize>;
fn lines(self) -> Lines<Self> { ... }
fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> io::Result<usize>;
fn split(self, byte: u8) -> Split<Self> { ... }
fn fill_buf(&mut self) -> io::Result<&[u8]> { ... }
fn consume(&mut self, amt: usize);
}关键注意:File 没有实现 BufRead——必须用 BufReader::new(file) 包装才能按行读取。
read_line 和 lines
use std::io::{BufRead, BufReader};
use std::fs::File;
let file = File::open("data.txt")?;
let reader = BufReader::new(file);
// read_line——读取一行(包含换行符)
let mut line = String::new();
reader.read_line(&mut line)?;
println!("第一行:{line}"); // 包含 '\n'
// lines()——迭代器,每行一个 String(不包含换行符)
for line_result in reader.lines() {
let line = line_result?;
println!("{line}");
}lines() 是大多数文本处理任务的推荐方式:
// grep 工具的简化实现
fn grep<R: BufRead>(target: &str, reader: R) -> io::Result<()> {
for line_result in reader.lines() {
let line = line_result?;
if line.contains(target) {
println!("{line}");
}
}
Ok(())
}
// 从文件搜索
let file = File::open("log.txt")?;
grep("ERROR", BufReader::new(file))?;
// 从标准输入搜索
let stdin = io::stdin();
grep("ERROR", stdin.lock())?;收集 lines 到 Vec
直接 collect() 会失败——需要利用 Result 的 FromIterator 实现:
let file = File::open("data.txt")?;
let reader = BufReader::new(file);
// 这是错误的——类型不匹配
// let lines: Vec<String> = reader.lines().collect(); // 编译错误!
// 正确做法——利用 Result 的 FromIterator
let lines: Vec<String> = reader
.lines()
.collect::<io::Result<Vec<String>>>()?;
// 等价写法
let lines: Vec<String> = reader
.lines()
.filter_map(|r| r.ok())
.collect();Write Trait
Write 定义了向字节接收器写入数据的方法:
pub trait Write {
fn write(&mut self, buf: &[u8]) -> io::Result<usize>;
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { ... }
fn flush(&mut self) -> io::Result<()>;
// write_fmt——被 write! 宏调用
fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> { ... }
}write, write_all 和 flush
use std::io::Write;
let mut file = File::create("output.txt")?;
// write——可能只写入部分数据
let bytes_written = file.write(b"Hello, ")?;
// write_all——确保全部写入
file.write_all(b"World!")?;
// flush——确保缓冲的数据到达目标
file.flush()?;write! 和 writeln! 宏
与 print!/println! 不同的是,write!/writeln! 将输出定向到任意 writer(而非仅 stdout),并返回 io::Result:
use std::io::Write;
let mut file = File::create("report.txt")?;
writeln!(file, "Report Generated: {}", chrono::Local::now())?;
writeln!(file, "Name: {name}, Score: {score}", name = "Alice", score = 95)?;
// println! 在写入失败时会 panic
// println!("写入屏幕");
// writeln! 让你自己处理错误
// writeln!(io::stdout(), "写入 stdout")?;BufWriter
为任意 writer 添加缓冲,减少系统调用:
use std::io::{BufWriter, Write};
let file = File::create("large_output.txt")?;
let mut writer = BufWriter::new(file);
// 多次 write! 调用被缓冲
for i in 0..100_000 {
writeln!(writer, "Line {i}")?;
}
// 重要:丢弃 BufWriter 时如果 flush 失败,错误会被静默吞掉
// 显式 flush 确保错误被处理
writer.flush()?;BufWriter 默认缓冲区大小为 8 KB,可用 BufWriter::with_capacity(size, writer) 自定义。
Vec<u8> 作为 Writer
use std::io::Write;
let mut buf = Vec::new();
write!(&mut buf, "Hello, {}!", "World")?;
writeln!(&mut buf)?;
assert_eq!(String::from_utf8(buf).unwrap(), "Hello, World!\n");
// 注意:String 没有实现 Write
// 需要先写入 Vec<u8> 再转换,或使用 fmt::Write 的 write!std::io::copy
io::copy 是将数据从 reader 传输到 writer 的最高效方式——使用 8 KB 内部缓冲区:
use std::io;
fn copy_file(src: &str, dst: &str) -> io::Result<u64> {
let mut src_file = File::open(src)?;
let mut dst_file = File::create(dst)?;
// 返回复制的字节数
io::copy(&mut src_file, &mut dst_file)
}注意:io::copy 在 reader 返回 Ok(0) 之前不断循环,没有内置的大小限制——对不受信任的数据源应包装 .take(n)。
File 类型
File 类型位于 std::fs 模块中,表示打开的文件:
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
// 打开已存在的文件(只读)
let mut file = File::open("input.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// 创建新文件(写入,存在则截断)
let mut file = File::create("output.txt")?;
file.write_all(b"Hello, World!")?;
// OpenOptions——精细控制打开模式
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true) // 不存在则创建
.append(true) // 追加模式
.open("log.txt")?;
let file = OpenOptions::new()
.write(true)
.create_new(true) // 存在则返回错误
.open("new_file.txt")?;File 实现了 Read、Write 和 Seek。文件在 Drop 时自动关闭。
Seek Trait
Seek trait 支持在文件中重定位读写位置:
use std::io::{Seek, SeekFrom};
let mut file = File::open("data.bin")?;
// 跳到文件开头
file.seek(SeekFrom::Start(0))?;
// 跳到第 100 字节
file.seek(SeekFrom::Start(100))?;
// 从末尾向前 8 字节
file.seek(SeekFrom::End(-8))?;
// 从当前位置偏移
file.seek(SeekFrom::Current(16))?;
// 获取当前位置
let pos = file.stream_position()?;标准输入、输出和错误
stdin、stdout 和 stderr 都持有互斥锁——每次读写都要获取锁:
use std::io::{self, Read, Write, BufRead};
// 读取标准输入的一行
let mut line = String::new();
io::stdin().read_line(&mut line)?;
// 写入标准输出
io::stdout().write_all(b"Hello, World!\n")?;
// 写入标准错误
io::stderr().write_all(b"Error occurred!\n")?;
// 使用 lock() 获取独占锁——高效批量操作
let stdin = io::stdin();
let handle = stdin.lock(); // StdinLock 实现了 BufRead
for line_result in handle.lines() {
let line = line_result?;
println!("读取:{line}");
}
// lock 在 handle 离开作用域时自动释放stdin() 返回的类型是 Stdin,不是 StdinLock。直接在 stdin() 上调用 .read_line() 隐式获取/释放锁——频繁调用效率低。lock() 获取一次锁然后可重复使用。
Path 和 PathBuf
Rust 使用 Path(类似 &str)和 PathBuf(类似 String)表示文件系统路径:
use std::path::{Path, PathBuf};
// 创建
let path = Path::new("/usr/local/bin/rustc");
let path_buf = PathBuf::from("/home/user/document.txt");
// 组件访问
assert_eq!(path.parent(), Some(Path::new("/usr/local/bin")));
assert_eq!(path.file_name(), Some(OsStr::new("rustc")));
assert_eq!(path.extension(), Some(OsStr::new(""))); // rustc 无扩展名
assert_eq!(path.file_stem(), Some(OsStr::new("rustc")));
// 路径判断
let path = Path::new("/usr/local/bin");
assert!(path.is_absolute());
assert!(!path.is_relative());
assert!(path.is_dir());
assert!(!path.is_file());
// join——连接路径
let base = Path::new("/usr");
let full = base.join("local").join("bin");
assert_eq!(full, PathBuf::from("/usr/local/bin"));
// components——迭代路径组件
for component in path.components() {
println!("{component:?}");
// RootDir, Normal("usr"), Normal("local"), Normal("bin")
}
// ancestors——回溯到根的所有父目录
for ancestor in path.ancestors() {
println!("{ancestor:?}");
// /usr/local/bin
// /usr/local
// /usr
// /
}
// 字符串转换
let path = Path::new("/usr/local/bin");
// to_str——可能返回 None(如果路径包含非 UTF-8 字节)
if let Some(s) = path.to_str() {
println!("UTF-8: {s}");
}
// to_string_lossy——始终返回字符串(非法字节替换为 �)
println!("{}", path.to_string_lossy());
// display——返回 Display 实现
println!("{}", path.display());类型对照表:
| 借用 | 所有权 | 核心能力 |
|---|---|---|
str | String | UTF-8 文本处理 |
OsStr | OsString | 任意字节序列,可能非 UTF-8 |
Path | PathBuf | 路径操作:parent(), join(), components() |
所有字符串和路径类型都实现了 AsRef<Path>——泛型函数可以声明为:
fn open_config<P: AsRef<Path>>(path: P) -> io::Result<String> {
std::fs::read_to_string(path)
}
// 接受多种类型
open_config("config.toml")?;
open_config(String::from("config.toml"))?;
open_config(PathBuf::from("config.toml"))?;
open_config(&path_buf)?;目录操作
use std::fs;
use std::path::Path;
// 创建目录
fs::create_dir("new_directory")?; // 父目录必须存在
fs::create_dir_all("a/b/c/d")?; // 递归创建所有父目录
// 删除目录
fs::remove_dir("empty_dir")?; // 目录必须为空
fs::remove_dir_all("non_empty_dir")?; // 递归删除(类似 rm -r)
// 读取目录内容
let entries = fs::read_dir("/home/user")?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
let type_label = if file_type.is_dir() {
"目录"
} else if file_type.is_file() {
"文件"
} else if file_type.is_symlink() {
"符号链接"
} else {
"其它"
};
println!("{}: {}", entry.file_name().to_string_lossy(), type_label);
}注意:read_dir 不列出 . 和 ..。
DirEntry 方法
let entry: fs::DirEntry = ...;
entry.file_name(); // -> OsString(文件名)
entry.path(); // -> PathBuf(完整路径 = 目录路径 + 文件名)
entry.file_type(); // -> io::Result<FileType>
entry.metadata(); // -> io::Result<Metadata>(大小、权限、时间戳等)常用文件系统函数
下表汇总了 std::fs 中最常用的函数:
| 函数 | 功能 | Unix 等价 | Windows 等价 |
|---|---|---|---|
read_to_string(path) | 读取整个文件到 String | cat | type |
read(path) | 读取整个文件到 Vec<u8> | cat | type |
write(path, data) | 写入数据到文件(截断) | echo > file | echo > file |
copy(src, dst) | 复制文件 | cp -p | CopyFileEx |
rename(src, dst) | 重命名/移动文件 | mv | MoveFileEx |
remove_file(path) | 删除文件 | rm | del |
hard_link(src, dst) | 创建硬链接 | ln | CreateHardLink |
canonicalize(path) | 规范化路径(解析符号链接) | realpath | GetFinalPathNameByHandle |
metadata(path) | 获取文件元数据 | stat | GetFileInformationByHandle |
symlink_metadata(path) | 获取元数据(不跟踪符号链接) | lstat | — |
递归拷贝目录示例
use std::fs;
use std::path::Path;
fn copy_dir(src: &Path, dst: &Path) -> io::Result<()> {
// 创建目标目录
fs::create_dir(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let target = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir(&entry.path(), &target)?;
} else if file_type.is_file() {
fs::copy(entry.path(), &target)?;
}
// 符号链接等特殊类型在此忽略
}
Ok(())
}其他 Reader/Writer 类型
Cursor
内存中的读写器——对字节数组进行读写:
use std::io::{Cursor, Read, Write, Seek, SeekFrom};
// 从字节切片读取
let data = b"Hello, World!";
let mut cursor = Cursor::new(data);
let mut buf = String::new();
cursor.read_to_string(&mut buf)?;
assert_eq!(buf, "Hello, World!");
// 向 Vec<u8> 写入
let mut cursor = Cursor::new(Vec::new());
write!(cursor, "Position: {}", 42)?;
// Seek 改变位置
cursor.seek(SeekFrom::Start(0))?;
let mut output = String::new();
cursor.read_to_string(&mut output)?;
println!("{output}"); // Position: 42sink 和 empty
use std::io::{self, Read, Write};
// sink()——黑洞 writer,丢弃所有数据
let mut sink = io::sink();
sink.write_all(b"这将被丢弃")?;
// empty()——空 reader,永远返回 EOF
let mut empty = io::empty();
let mut buf = [0; 10];
assert_eq!(empty.read(&mut buf)?, 0);
// repeat(byte)——无限重复同一个字节
let mut repeat = io::repeat(b'x');网络 I/O 简介
std::net 模块提供 TCP 和 UDP 的跨平台支持:
TCP 客户端
use std::io::{Read, Write};
use std::net::TcpStream;
let mut stream = TcpStream::connect(("example.com", 80))?;
// 发送 HTTP 请求
stream.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")?;
// 读取响应
let mut response = String::new();
stream.read_to_string(&mut response)?;
println!("{response}");TCP Echo 服务器
use std::io;
use std::net::{TcpListener, TcpStream};
use std::thread;
fn handle_client(mut stream: TcpStream) -> io::Result<()> {
// 将接收到的数据原样写回
io::copy(&mut stream, &mut stream.try_clone()?)?;
Ok(())
}
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("监听 127.0.0.1:8080");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| {
if let Err(e) = handle_client(stream) {
eprintln!("错误:{e}");
}
});
}
Err(e) => eprintln!("连接失败:{e}"),
}
}
Ok(())
}TcpStream 同时实现了 Read 和 Write——是 I/O trait 体系统一体性的最好体现。
注意:上面的 echo 服务器使用阻塞 I/O。对于高性能服务,应使用异步 I/O(tokio、async-std)——这将在异步编程章节中介绍。
错误处理
I/O 操作都返回 io::Result<T>(Result<T, io::Error> 的类型别名)。关键错误信息:
use std::io::{self, ErrorKind};
match File::open("config.toml") {
Ok(file) => { /* 处理文件 */ }
Err(e) => match e.kind() {
ErrorKind::NotFound => eprintln!("配置文件未找到"),
ErrorKind::PermissionDenied => eprintln!("没有读取权限"),
ErrorKind::AlreadyExists => eprintln!("文件已存在"),
ErrorKind::Interrupted => { /* 重试 */ }
_ => eprintln!("其它 I/O 错误:{e}"),
}
}
// 常见模式:传播错误
fn read_config() -> io::Result<String> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}二进制数据、压缩和序列化
标准库的 I/O 只提供原始的字节读写。对于结构化数据,需要外部 crate:
byteorder crate(二进制读写)
// Cargo.toml: byteorder = "1"
use std::io::Cursor;
use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian, BigEndian};
// 写入二进制数据
let mut buf = Vec::new();
buf.write_u32::<LittleEndian>(0x12345678)?;
buf.write_f64::<LittleEndian>(3.14159)?;
// 读取二进制数据
let mut reader = Cursor::new(buf);
let value: u32 = reader.read_u32::<LittleEndian>()?;
assert_eq!(value, 0x12345678);serde crate(序列化框架)
// Cargo.toml: serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
name: String,
version: u32,
features: Vec<String>,
}
// 序列化到 JSON
let config = Config {
name: "MyApp".into(),
version: 1,
features: vec!["logging".into(), "caching".into()],
};
let json = serde_json::to_string(&config)?;
println!("{json}");
// 反序列化
let parsed: Config = serde_json::from_str(&json)?;
assert_eq!(parsed.name, "MyApp");
// 直接写入 writer
serde_json::to_writer(&mut std::io::stdout(), &config)?;小结
- Read/Write/BufRead 三个核心 trait 统一了所有 I/O 操作。任何实现了这些 trait 的类型(文件、网络连接、内存缓冲、管道、终端)都可以互换使用。
- File 实现了 Read 和 Write 但没有实现 BufRead——用
BufReader::new(file)包装获得按行读取能力。BufWriter减少系统调用次数,显式flush()确保错误不丢失。 - Path/PathBuf 处理文件系统路径,所有字符串类型都实现了
AsRef<Path>——泛型函数可以同时接受&str、String、&Path、PathBuf。 - 文件系统操作(
std::fs)提供了创建/删除/复制/重命名/遍历目录等完整功能。canonicalize()解析符号链接和相对路径。 - 网络 I/O(
std::net)中TcpStream同时实现了Read和Write,TcpListener::incoming()返回连接迭代器——与文件 I/O 使用完全相同的模式。