【2025 Rust学习 --- 18 IO操作和网络】

devtools/2025/1/16 10:04:21/

输入与输出

Rust 标准库中的输入和输出的特性是围绕 3 个特型组织的,即 ReadBufReadWrite

  • 实现了 Read 的值具有面向字节的输入方法。它们叫作读取器。
  • 实现了 BufRead 的值是缓冲读取器它是 Read的子特型 ,外加读取文本行等方法。
  • 实现了 Write 的值能支持面向字节和 UTF-8 文本的输出。它们叫作写入器。

在这里插入图片描述

读取器与写入器

读取器和写入器都有标准特型(std::io::Readstd::io::Write),因此编写适用于各种输入通道或输出通道的泛型代码是很常见的。

将所有字节从任意读取器复制到任意写入器的函数:

rust">use std::io::{self, Read, Write, ErrorKind};
const DEFAULT_BUF_SIZE: usize = 8 * 1024;
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)-> io::Result<u64>where R: Read, W: Write
{let mut buf = [0; DEFAULT_BUF_SIZE];let mut written = 0;loop {let len = match reader.read(&mut buf) {Ok(0) => return Ok(written),Ok(len) => len,Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,Err(e) => return Err(e),};writer.write_all(&buf[..len])?;written += len as u64;}
}

这是 Rust 标准库中 std::io::copy() 的实现。由于是泛型的,因此可以使用它将数据从 File 复制到 TcpStream,从标准输入复制到内存中的 Vec…

std::io 的 Read、BufRead 和 Write 这 3 个特型以及 Seek 非常常用,下 面是一个只包含这些特型的 prelude 模块:use std::io::prelude::*;

导入 std::io 模块本身: use std::io::{self, Read, Write, ErrorKind};

self 关键字将 io 声明成了 std::io 模块的别名。这样, std::io::Resultstd::io::Error 就可以更简洁地写为 io::Resultio::Error 了。

读取器

std::io::Read 有以下几个读取数据的方法。所有这些方法都需要对读取器本身进行可变引用。

  • reader.read(&mut buffer)(读取)  从数据源中读取一些字节并将它们存储在给定的 buffer 中。buffer 参数 的类型是 &mut[u8]。此方法最多会读取 buffer.len() 字节。  返回类型是 io::Result,它是 Result 的类型别名。成功时,这个 u64 的值是已读取的字节数——可能等于或小于 buffer.len(),就算数据源突然涌入更多数据也不会超出。

    • 返回Ok(0) 则意味着 没有更多输入可以读取了。  

    • 出错时,.read() 会返回 Err(err),其中 err 是一个 io::Error 值。

      io::Error 是可打印的;为了便于程序处理,它有一个 .kind() 方法,该方法会返回 io::ErrorKind 类型的错误代码。此枚举的成员都有 PermissionDenied 和 ConnectionReset 之类的名称。大多数表示 严重的错误,不容忽视,但有一种错误需要特殊处理。 io::ErrorKind::Interrupted 对应于 Unix 错误码 EINTR,表示读取恰好被某种信号中断了。除非程序的设计目标之一就是对信号中断做一些巧妙的处 理,否则就应该再次尝试读取。

      .read() 方法是非常底层的,甚至还继承了底层操作系统的某些怪癖。如果你正在为一种新型数据源实现 Read 特型,那么这会给你很大的发 挥空间。但如果你想读取一些数据,则会很痛苦。因此,Rust 提供了几个更高级的便捷方法。这些方法是 Read 中的默认实现,而且都处理了 ErrorKind::Interrupted,这样你就不必自己处理了。

  • reader.read_to_end(&mut byte_vec)(读至末尾)  从这个读取器读取所有剩余的输入,将其追加到 Vec 型的 byte_vec 中。返回 io::Result,即已读取的字节数。  此方法对要装入向量中的数据量没有任何限制,因此不要在不受信任的来源上使用它。(可以用接下来要讲的 .take() 方法加以限制。)

  • reader.read_to_string(&mut string)(读取字符串)  和上一个方法类似,但此方法会将数据附加到给定的 String。如果流不是 有效的 UTF-8,则返回 ErrorKind::InvalidData 错误。  在某些编程语言中,字节输入和字符输入是由不同的类型处理的。如今, UTF-8 占据了绝对主导地位,所以 Rust 承认这一事实标准并且处处支持 UTF8。开源的 encoding crate 可以支持其他字符集。

  • reader.read_exact(&mut buf)(精确读满)  读取足够的数据来填充给定的缓冲区。参数类型是 &[u8]。如果读取器在 读够 buf.len() 字节之前就耗尽了数据,那么此方法就会返回 ErrorKind::UnexpectedEof 错误。

此外,还有 3 个适配器方法可以按值获取 reader,将其转换为迭代器或另一种读取器。

  • reader.bytes()(字节迭代器)  返回输入流中各字节的迭代器。条目类型是 io::Result,因此每字 节都需要进行错误检查。此外,此方法会为每字节调用一次 reader.read(), 如果没有缓冲,则会非常低效。
  • reader.chain(reader2)(串联)  返回新的读取器,先生成来自 reader 的所有输入,然后再生成来自 reader2 的所有输入。
  • reader.take(n)(取出 n 个)  返回新的读取器,从与 reader 相同的数据源读取,但仅限于前 n 字节的 输入。 没有关闭读取器的方法。读取器和写入器通常会实现 Drop 以便自行关闭。

缓冲读取器

为了提高效率,可以对读取器和写入器进行缓冲,这基本上意味着它们有一块内存(缓冲区),用于保存一些输入数据或输出数据。这可以减少一些系统调用, 如图应用程序会从 BufReader 中读取数据,在本例中是通过调用 其 .read_line() 方法实现的。BufReader 会依次从操作系统获取更大块的输入。

在这里插入图片描述

BufReader 缓冲区的实际大小默认为几千字节,因此 一次 read 系统调用就可以服务数百次 .read_line() 调用。这很重要,因为系统调用很慢。

操作系统也有自己的缓冲区。同理:系统调用固然慢,但从 磁盘读取数据更慢。

缓冲读取器同时实现了 Read 特型和 BufRead 特型,后者添加了以下方法:

  • reader.read_line(&mut line)(读一行)  读取一行文本并将其追加到 line,line 是一个 String。行尾的换行符 ‘\n’ 包含在 line 中。  如果输入带有 Windows 风格的行尾结束符号 “\r\n”,则这两个字符都会 包含在 line 中。  返回值是 io::Result,表示已读取的字节数,包括行尾结束符 号(如果有的话)。  如果读取器在输入的末尾,则会保持 line 不变并返回 Ok(0)。
  • reader.lines()(文本行迭代器)  返回生成各个输入行的迭代器。条目类型是 io::Result。字符串中不包含换行符。如果输入带有 Windows 风格的行尾结束符号 “\r\n”,则这两个字符都会被去掉。  在绝大多数场景中,这个方法足够你进行文本输入了。
  • reader.read_until(stop_byte, &mut byte_vec)(读到 stop_byte 为止)和 reader.split(stop_byte)(根据 stop_byte 拆分)  与 .read_line() 和 .lines() 类似,但这两个方法是面向字节的,会生成 Vec 而不是 String。你要自选分隔符 stop_byte。
  • BufRead 还提供了 .fill_buf().consume(n),这是一对底层方法,用于直接访问读取器的内部缓冲区。

读取行

用于实现 Unix grep 实用程序的函数,该函数会在多行文本(通常 是通过管道从另一条命令输入的文本)中搜索给定字符串:【现在仅支持终端搜索】

rust">use std::io;
use std::io::prelude::*;
fn grep(target: &str) -> io::Result<()> {let stdin = io::stdin();for line_result in stdin.lock().lines() {let line = line_result?;if line.contains(target) {println!("{}", line);}}Ok(())
}

要调用 .lines(),所以需要一个实现了 BufRead 的输入源。在这种情况 下,可以调用 io::stdin()获取通过管道传输给我们的数据。但是,Rust 标准库使用互斥锁保护着 stdin。因此要调用 .lock() 来锁定 stdin 以供当 前线程独占使用,这会返回一个实现了 BufRead 的 StdinLock 值。在循环结束时,StdinLock 会被丢弃,释放互斥锁。(如果没有互斥锁,那么当两个线 程试图同时从 stdin 读取时就会导致未定义行为。C 语言也有相同的问题,且 以相同的方式来解决:所有 C 标准输入函数和输出函数都会在幕后获得锁。唯 一的不同在于,在 Rust 中,锁是 API 的一部分。

该函数的其余部分都很简单:它会调用 .lines() 并对生成的迭代器进行循 环。因为这个迭代器会生成 Result 值,所以要用 ? 运算符检查错误。 假如我们想完善这个 grep 程序,让它支持在磁盘上搜索文件。可以把这个函数 变成泛型函数:

rust">fn grep<R>(target: &str, reader: R) -> io::Result<()> where R: BufRead
{for line_result in reader.lines() {let line = line_result?;if line.contains(target) {println!("{}", line);}}Ok(())
}

现在可以将 StdinLock 或带缓冲的 File 传给它

rust">let stdin = io::stdin();
grep(&target, stdin.lock())?; // 正确
let f = File::open(file)?;
grep(&target, BufReader::new(f))?; // 同样正确

请注意,File 不会自动缓冲。File 实现了 Read 但没实现 BufRead。但是, 为 File或任意无缓冲读取器创建缓冲读取器很容易:

BufReader::new(reader) 就是做这个的。(要设置缓冲区的大小,请使用 BufReader::with_capacity(size, reader)) 在大多数语言中,文件默认是带缓冲的。如果想要无缓冲的输入或输出,就必须 弄清楚如何关闭缓冲。在 Rust 中,File 和 BufReader 是两个独立的库特性,因为有时你想要不带缓冲的文件,有时你想要不带文件的缓冲(例如想要缓冲来自网络的输入)。 下面是一个包括错误处理和一些粗略的参数解析的完整grep程序。

rust">// grep——搜索stdin或其他文件,以便用给定的字符串进行逐行匹配
use std::error::Error;
use std::io::{self, BufReader};
use std::io::prelude::*;
use std::fs::File;
use std::path::PathBuf;
fn grep<R>(target: &str, reader: R) -> io::Result<()> where R: BufRead
{for line_result in reader.lines() {let line = line_result?;if line.contains(target) {println!("{}", line);}}Ok(())
}// 获取命令行参数。第一个参数是要搜索的字符串,其他参数是一些文件名
fn grep_main() -> Result<(), Box<dyn Error>> {let mut args = std::env::args().skip(1);let target = match args.next() {Some(s) => s,None => Err("usage: grep PATTERN FILE...")?};let files: Vec<PathBuf> = args.map(PathBuf::from).collect();if files.is_empty() {let stdin = io::stdin();grep(&target, stdin.lock())?; //grep终端} else {for file in files {    //grep filelet f = File::open(file)?;grep(&target, BufReader::new(f))?; //drop自动关闭file_fd}} Ok(())
}fn main() {let result = grep_main();if let Err(err) = result {eprintln!("{}", err);std::process::exit(1);}
}

收集行

有些读取器方法(包括 .lines())会返回生成 Result 值的迭代器。当你第一次想要将文件的所有行都收集到一个大型向量中时,就会遇到如何摆脱 Result 的问题

rust">// 正确,但不是你想要的
let results: Vec<io::Result<String>> = reader.lines().collect();// 错误:不能把Result的集合转换成Vec<String>
let lines: Vec<String> = reader.lines().collect();
  • 该迭代器的每个元素是一个 Result<String> 类型。
  • 使用 collect() 方法将这些 Result<String> 收集到一个 Vec<io::Result<String>> 中。
  • 每个元素仍然是一个 Result<String>,这意味着你需要进一步处理这些结果。

第二次尝试无法编译:遇到这些错误怎么办?最直观的解决方法是编写一个 for 循环并检查每个条目是否有错:

rust">let mut lines = vec![];
for line_result in reader.lines() {lines.push(line_result?);
}

这固然没错,但这里最好还是用 .collect(),事实上确实可以做到。只要知道该请求哪种类型就可以了:

rust">let lines = reader.lines().collect::<io::Result<Vec<String>>>()?;

这是怎么做到的呢?标准库中包含了 Result 对 FromIterator 的实现,这个实现让一切成为可能:

rust">impl<T, E, C> FromIterator<Result<T, E>> for Result<C, E> where C: FromIterator<T>
{...
}

假设 C 是任意集合类型,比如 Vec 或 HashSet。只要已经知道如何从 T 值的迭代器构建出 C,就可以从生成 Result<T,E> 值的迭代器构建出 Result<C,E>只需从迭代器中提取各个 值并从 Ok 结果构建出集合即可,但一旦看到 Err,就停止并将其传出

换句话说,io::Result<Vec<String>> 是一种集合类型,因此 .collect() 方法可以创建并填充该类型的值。

也可以:

使用 filter_map 或者 mapcollect 结合使用

方法一:使用 filter_map
rust">use std::fs::File;
use std::io::{self, BufRead};fn main() -> io::Result<()> {let file = File::open("example.txt")?;let reader = io::BufReader::new(file);let lines: Vec<String> = reader.lines().filter_map(|line| line.ok()).collect();println!("{:?}", lines);Ok(())
}
  • filter_map 方法会过滤掉所有 Err 值,并将 Ok 值映射为其内部值。
  • line.ok()Result<String> 转换为 Option<String>,其中 Ok(value) 变为 Some(value),而 Err(_) 变为 None
  • filter_map 会自动过滤掉所有的 None 值,只保留 Some 值。
方法二:使用 mapcollect
rust">use std::fs::File;
use std::io::{self, BufRead};fn main() -> io::Result<()> {let file = File::open("example.txt")?;let reader = io::BufReader::new(file);let lines: Vec<String> = reader.lines().map(|result| result.unwrap_or_else(|_| String::from(""))).collect();println!("{:?}", lines);Ok(())
}
  • map 方法会对每个 Result<String> 应用一个闭包。
  • result.unwrap_or_else(|_| String::from("")) 会处理 Result,如果成功则返回 String,如果失败则返回一个空字符串。
  • collect 方法将处理后的结果收集到一个 Vec<String> 中。

写入器

输入主要是用方法完成的,而输出略有不同

要将输出发送到写入器,请使用 write!() 宏和 writeln!() 宏。它们和 print!() 和 println!() 类似,但有两点区别:

  • 一是每个 write 宏都接受一个额外的写入器作为第一参数。
  • 二是它们会返回 Result,因此必须处理错误。这就是为什么要在每行末尾使用 ? 运算符。 print 宏不会返回 Result,如果写入失败,它们只会 panic。由于写入的是终 端,所以极少失败。

Write 特型有以下几个方法

  • writer.write(&buf)(写入)  将切片 buf 中的一些字节写入底层流。此方法会返回 io::Result。成功时,这给出了已写入的字节数,如果流突然提前关闭,那么这个值可能会小于 buf.len()。  和 Reader::read() 一样,这是一个要避免直接使用的底层方法
  • writer.write_all(&buf)(写入全部)  将切片 buf 中的所有字节都写入。返回 Result<()>。
  • writer.flush()(刷新缓冲区)  将可能被缓冲在内存中的数据刷新到底层流中。返回 Result<()>。  请注意,虽然 println! 宏和 eprintln! 宏会自动刷新 stdout 流和 stderr 流的缓冲区,但 print! 宏和 eprint! 宏不会。使用它们时,可能要 手动调用 flush()。 与读取器一样,写入器也会在被丢弃时自动关闭
  • 正如 BufReader::new(reader) 会为任意读取器添加缓冲区一样, BufWriter::new(writer) 也会为任意写入器添加缓冲区
  • 要设置缓冲区的大小,请使用 BufWriter::with_capacity(size, writer)

当丢弃 BufWriter 时,所有剩余的缓冲数据都将写入底层写入器。但是,如果在此写入过程中发生错误,则错误会被忽略。(由于错误发生在 BufWriter 的 .drop() 方法内部,因此没有合适的地方来报告。)为了确保应用程序会注意 到所有输出错误,请在丢弃带缓冲的写入器之前将它手动 .flush() 一下

文件

打开文件的两个方法:

  • File::open(filename)(打开)  打开现有文件进行读取。此方法会返回一个 io::Result,如果该 文件不存在则报错。
  • File::create(filename)(创建)  创建一个用于写入的新文件。如果存在具有给定文件名的文件,则会将其截 断。

请注意,File 类型位于文件系统模块 std::fs 中,而不是 std::io 中。 当这两个方法都不符合需求时,可以使用 OpenOptions 来指定所期望的确切行为:

rust">use std::fs::OpenOptions;let log = OpenOptions::new().append(true) // 如果文件已存在,则追加到末尾.open("server.log")?;let file = OpenOptions::new().write(true).create_new(true) // 如果文件已存在,则失败.open("new_file.txt")?;

方法 .append()、.write()、.create_new() 等是可以链式调用的:每个 方法都会返回 self。这种链式调用的设计模式很常见,所以在 Rust 中它有一 个专门的名字——构建器(builder)。另一个例子是 std::process::Command。File 打开后的行为就和任何读取器或写入器一样。如果需要,可以为它添加缓冲区。File 在被丢弃时会自动关闭。

寻址

File 还实现了 Seek 特型,这意味着你可以在 File 中“跳来跳去”,而不是从 头到尾一次性读取或写入。Seek 的定义如下

rust">pub trait Seek {fn seek(&mut self, pos: SeekFrom) -> io::Result<u64>;
}
pub enum SeekFrom {Start(u64), End(i64),Current(i64)
}

此枚举让 seek 方法表现得很好:可以用 file.seek(SeekFrom::Start(0)) 倒回到开头,还能用 file.seek(SeekFrom::Current(-8)) 回退几字节,等等。 在文件中寻址很慢。无论使用的是硬盘还是固态驱动器(SSD),每一次寻址的开销都接近于读取数兆字节的数据。

其他读取器与写入器

迄今为止,本章一直在使用 File 作为示范的主力,但还有许多其他有用的读取 器类型和写入器类型。

  • io::stdin()(标准输入)  返回标准输入流的读取器。类型是 io::Stdin。由于它被所有线程共享, 因此每次读取都会获取和释放互斥锁。  Stdin 有一个 .lock() 方法,该方法会获取互斥锁并返回 io::StdinLock这是一个带缓冲的读取器,在被丢弃之前会持有互斥锁。因此,对 StdinLock 的单个操作就避免了互斥开销。Rust 1.8 开始,已经可以直接调用 io::stdin().lock()

    rust">let stdin = io::stdin();
    let lines = stdin.lock().lines(); 
    
  • io::stdout()(标准输出)和 io::stderr()(标准错误)  返回标准输出流(Stdout)类型和标准错误流(Stderr)类型的写入 器。它们也有互斥锁和 .lock() 方法。

  • Vec<u8>(u8 向量)  实现了 Write。写入 Vec<u8> 会使用新数据扩展向量。  (但是,String 没有实现 Write。要使用 Write 构建字符串,需要首先 写入 Vec,然后使用 String::from_utf8(vec) 将向量转换为字符串)

  • Cursor::new(buf)(新建)  创建一个 Cursor(游标,一个从 buf 读取数据的缓冲读取器)。这样你 就创建了一个能读取 String 的读取器。参数 buf 可以是实现了 AsRef<[u8]> 的任意类型,因此也可以传递 &[u8]、&str 或 Vec<u8>

    Cursor 的内部平平无奇,只有两个字段:buf 本身和一个整数,该整数是 buf 中下一次读取开始的偏移量。此位置的初始值为 0。

    Cursor 实现了 Read、BufRead 和 Seek。如果 buf 的类型是 &mut [u8]Vec<u8>,那么 Cursor 也实现了 Write。写入游标会覆盖 buf 中 从当前位置开始的字节。如果试图直接写到超出 &mut [u8] 末尾的位置,就会导致一次“部分写入”或一个 io::Error。不过,使用游标写入 Vec 的结尾就没有这个问题:它会增长此向量。因此,Cursor<&mut [u8]>Cursor<Vec<u8>> 实现了所有这 4 个 std::io::prelude 特型。

  • std::net::TcpStream(Tcp 流)  表示 TCP 网络连接。由于 TCP 支持双向通信,因此它既是读取器又是写入器。  类型关联函数 TcpStream::connect(("hostname", PORT)) 会尝试 连接到服务器并返回 io::Result<TcpStream>

  • std::process::Command(命令)

    支持启动子进程并通过管道将数据传输到其标准输入:

    rust">use std::process::{Command, Stdio};let mut child = Command::new("grep").arg("-e").arg("a.*e.*i.*o.*u") .stdin(Stdio::piped()).spawn()?;
    let mut to_child = child.stdin.take().unwrap();
    for word in my_words {writeln!(to_child, "{}", word)?;
    }
    drop(to_child); // 关闭grep的stdin,以便让它退出
    child.wait()?;
    

    child.stdin 的类型是 Option<std::process::ChildStdin>,这 里在建立子进程时使用了 .stdin(Stdio::piped()),因此当 .spawn() 成功时,child.stdin 必然已经就位。如果没提供,那么 child.stdin 就是 None。  Command 还有两个类似的方法 .stdout().stderr(),可用于请求 child.stdoutchild.stderr 中的读取器。

std::io 模块还提供了一些返回普通读取器和写入器的函数:

  • io::sink()(地漏)  这是无操作写入器。所有的写入方法都会返回 Ok,但只是把数据扔掉了。
  • io::empty()(空白)  这是无操作读取器。读取总会成功,但只会返回“输入结束”(EOF)。
  • io::repeat(byte)(重复)  返回一个会无限重复给定字节的读取器。

二进制数据和序列化

许多开源 crate 建立在 std::io 框架之上,以提供额外的特性。

1、byteorder crate 提供了 ReadBytesExt 特型和 WriteBytesExt 特型,为所有读取器和写入器添加了二进制输入和输出的方法:

rust">use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian};
let n = reader.read_u32::<LittleEndian>()?;
writer.write_i64::<LittleEndian>(n as i64)?;

2、flate2 crate 提供了用于读取和写入 gzip 数据的适配器方法:

rust">use flate2::read::GzDecoder;
let file = File::open("access.log.gz")?;
let mut gzip_reader = GzDecoder::new(file);

3、serde crate 及其关联的格式类 crate(如 serde_json)实现了序列化和反序列化:它们在 Rust 结构体和字节之间来回转换

有一些数据(文字冒险游戏中的映射表)存储在 HashMap 中:

rust">type RoomId = String; // 每个房间都有唯一的名字
type RoomExits = Vec<(char, RoomId)>; // ……并且存在一个出口列表
type RoomMap = HashMap<RoomId, RoomExits>; // 房间名和出口的简单映射表// 创建一个简单映射表
let mut map = RoomMap::new();
map.insert("Cobble Crawl".to_string(),vec![('W', "Debris Room".to_string())]);
map.insert("Debris Room".to_string(),vec![('E', "Cobble Crawl".to_string()),('W', "Sloping Canyon".to_string())]);

将此数据转换为 JSON 输出只需一行代码:

serde_json::to_writer(&mut std::io::stdout(), &map)?;

在内部,serde_json::to_writer 使用了 serde::Serialize 特型的 serialize 方法。该库会将 serde::Serialize 特型附加到所有它知道如何 序列化的类型中,包括我们的数据中出现过的类型:字符串、字符、元组、向量 和 HashMap。 serde 很灵活。在这个程序中,输出是 JSON 数据,因为我们选择了 serde_json 序列化器。其他格式(如 MessagePack)也有对应的序列化器支持。

同样,可以将此输出发送到文件、Vec 或任意写入器。前面的代码会将数据打印到 stdout 为:

rust">{"Debris Room":[["E","Cobble Crawl"],["W","Sloping Canyon"]],"Cobble Crawl":
[["W","Debris Room"]]}

serde 还包括对供派生的两个关键 serde 特型的支持:

rust">#[derive(Serialize, Deserialize)]
struct Player {location: String,items: Vec<String>,health: u32
}

\#[derive] 属性会让编译多花费一点儿时间,所以在 Cargo.toml 文件中将它列为依赖项时,要明确要求 serde 支持它

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

简而言之,构建系统会为 struct Player 自动生 成 serde::Serialize 和 serde::Deserialize 的实现,因此序列化 Player 的值很简单:

serde_json::to_writer(&mut std::io::stdout(), &player)?;

输出:

{"location":"Cobble Crawl","items":["a wand"],"health":3}

文件与目录

文件和目录的特性位于 std::path 模块和 std::fs 模块

OsStr 与 Path

操作系统并不会强制要求其文件名是有效的 Unicode。下面是创建文本文件的两个 Linux shell 命令。第一个使用了有效的 UTF-8 文件名,第二个则 没有:

$ echo "hello world" > ô.txt
$ echo "O brave new world, that has such filenames in't" > $'\xf4'.txt

这两个命令都没有任何报错就通过了,因为 Linux 内核并不检查 UTF-8 的格式 有效性。对内核来说,任意字节串(除了 null 字节和斜杠)都是可接受的文件名

在 Windows 上的情况类似:几乎任意 16 位“宽字符”字符串都是可接受的文件名,即使字符串不是有效的 UTF-16 也可以。操作系统处理的其他字符串也是 如此,比如命令行参数和环境变量。

Rust 字符串始终是有效的 Unicode。文件名在实践中几乎总是 Unicode,但 Rust 必须以某种方式处理罕见的例外情况。这就是 Rust 会有 std::ffi::OsStr 和 OsString 的原因。

  • OsStr 是一种字符串类型,它是 UTF-8 的超集。OsStr 的任务是表示当前系统 上的所有文件名、命令行参数和环境变量,无论它们是不是有效的 Unicode。 在 Unix 上,OsStr 可以保存任意字节序列。在 Windows 上,OsStr 使用 UTF-8 的扩展格式存储,可以对任意 16 位值序列(包括不符合标准的半代用区 码点)进行编码。 所以我们有两种字符串类型:str 用于实际的 Unicode 字符串,而 OsStr 用于 操作系统可能抛出的任意文字。还有用于文件名的 std::path::Path,这纯 粹是一个便捷名称。Path 与 OsStr 完全一样,只是添加了许多关于文件名的便捷方法。绝对路径和相对路径都使用 Path 表示。 对于路径中的单个组件,请使用 OsStr。
  • 最后,每种字符串类型都有对应的拥有型版本:String 拥有分配在堆上的 str,std::ffi:: OsString 拥有分配在堆上的 OsStr,而 std::path::PathBuf 拥有分配在堆上的 Path。

在这里插入图片描述

在这里插入图片描述

所有这 3 种类型都实现了一个公共特型 AsRef,因此我们可以轻松地声明一个接受“任意文件名类型”作为参数的泛型函数。

rust">use std::path::Path;
use std::io;
fn swizzle_file<P>(path_arg: P) -> io::Result<()>where P: AsRef<Path>
{let path = path_arg.as_ref();...
}

Path 与 PathBuf 的方法

  • Path::new(str)(新建)  将 &str 或 &OsStr 转换为 &Path。这不会复制字符串。新的 &Path 会 指向与原始 &str 或 &OsStr 相同的字节。[类似的方法 OsStr::new(str) 会将 &str 转换为 &OsStr]

    rust">use std::path::Path;
    let home_dir = Path::new("/home/fwolfe");
    
  • path.parent()(父目录)  返回路径的父目录(如果有的话)。返回类型是 Option<&Path>。  这不会复制路径。path 的父路径一定是 path 的子串。

  • path.file_name()(文件名)  返回 path 的最后一个组件(如果有的话)。返回类型是 Option<&OsStr>。  典型情况下,path 由目录、斜杠和文件名组成,此方法会返回文件名。

  • path.is_absolute()(是绝对路径?)和 path.is_relative()(是 相对路径?)  这两个方法会指出文件是绝对路径(如 Unix 路径 /usr/bin/advent 或 Windows 路径 C:\Program Files)还是相对路径(如 src/main.rs)。

  • path1.join(path2)(联结)  联结两个路径,返回一个新的 PathBuf:

    rust">let path1 = Path::new("/usr/share/dict");
    assert_eq!(path1.join("words"),Path::new("/usr/share/dict/words"));
    

    如果 path2 本身是绝对路径,则只会返回 path2 的副本,因此该方法可 用于将任意路径转换为绝对路径。

let abs_path = std::env::current_dir()?.join(any_path);

  • path.components()(组件迭代器)  返回从左到右访问给定路径各个组件的迭代器。这个迭代器的条目类型是 std::path::Component,这是一个枚举,可以代表所有可能出现在文件名中 的不同部分:

    rust">pub enum Component<'a> {Prefix(PrefixComponent<'a>), // 驱动器路径或共享路径(在Windows 上)RootDir, // 根目录,`/`或`\`CurDir, // 特殊目录`.`ParentDir, // 特殊目录`..`Normal(&'a OsStr) // 普通文件或目录名
    }
    
  • path.ancestors()(祖先迭代器)  返回一个从 path 开始一直遍历到根路径的迭代器。生成的每个条目也都是 Path:首先是 path 本身,然后是它的父级,接下来是它的祖父级,以此类推:

    rust">let file = Path::new("/home/jimb/calendars/calendar-18x18.pdf");
    assert_eq!(file.ancestors().collect::<Vec<_>>(),vec![Path::new("/home/jimb/calendars/calendar-18x18.pdf"),Path::new("/home/jimb/calendars"),Path::new("/home/jimb"),Path::new("/home"),Path::new("/")]);
    

这些方法只针对内存中的字符串进行操作。Path 也有一些能查询文件系统的方 法:.exists()、.is_file()、.is_dir()、.read_dir()、.canonica lize() 等。

将 Path 转换为字符串有以下 3 个方法,每个方法都容许 Path 中存在无效 UTF-8。

  • path.to_str()(转字符串)  将 Path 转换为字符串,返回 Option<&str>。如果 path 不是有效的 UTF-8,则返回 None。

  • path.to_string_lossy()(转字符串,宽松版)  基本上和上一个方法一样,但该方法在任何情况下都会设法返回某种字符 串。如果 path 不是有效的 UTF-8,则该方法会制作一个副本,用 Unicode 代用字符 � 替代每个无效的字节序列。返回类型为 std::borrow::Cow<str>:借用或拥有的字符串。要从此值 获取 String,请使用其 .to_owned() 方法。

  • path.display()(转显示)  用于打印路径: println!("Download found. You put it in: {}", dir_path.display());  此方法返回的值不是字符串,但它实现了 std::fmt::Display,因此可 以与 format!()、println!() 和类似的宏一起使用。如果路径不是有效的 UTF-8,则输出可能包含 � 字符。

访问文件系统的函数

这些函数都会返回 io::Result 值。除非另行说明,否则它们的返回值都是 Result<()>

在这里插入图片描述

所有这些函数都是通过调用操作系统实现的

例如, std::fs::canonicalize(path) 不仅会使用字符串处理来从给定的 path 中消除 . 和 ..,还会使用当前工作目录解析相对路径,并追踪符号链接。如果路径不存在,则会报错。

std::fs::metadata(path)std::fs::symlink_metadata(path) 生成的 Metadata 类型包含文件类型和大小、权限、时间戳等信息。

为便于使用,Path 类型将其中一些内置成了方法,比如 path.metadata()std::fs::metadata(path) 是一样的。

读取目录

要列出目录的内容,请使用 std::fs::read_dir 或 Path 中的等效方法 .read_dir()

rust">for entry_result in path.read_dir()? {let entry = entry_result?;println!("{}", entry.file_name().to_string_lossy());
}

代码中有两行用到了 ? 运算符。第 1 行检查了打开目录时的错 误。第 2 行检查了读取下一个条目时的错误。

entry 的类型是 std::fs::DirEntry,这个结构体提供了数个方法:

  • entry.file_name()(文件名)  文件或目录的名称,是 OsString 类型的。

  • entry.path()(路径)  与 entry.file_name() 基本相同,但 entry.path() 联结了原始路径,生成了一个新的 PathBuf。如果正在列出的目录是 “/home/jimb”,并且 entry.file_name() 是 “.emacs”,那么 entry.path() 将返回 PathBuf::from(“/home/jimb/.emacs”).

  • entry.file_type()(文件类型)  返回 io::Result<FileType>

    FileType 有 .is_file() 方 法、.is_dir() 方法和 .is_symlink() 方法。

  • entry.metadata()(元数据)  获取有关此条目的其他元数据。特殊目录 . 和 .. 在读取目录时不会列出。

递归地将目录树从磁盘上的一个位置复制到另一个位置:

rust">use std::fs;
use std::io;
use std::path::Path;/// 把`src`中的任何内容复制到目标路径`dst`
fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path) -> io::Result<()>
{if src_type.is_file() {fs::copy(src, dst)?;} else if src_type.is_dir() {copy_dir_to(src, dst)?;} else {return Err(io::Error::new(io::ErrorKind::Other,format!("don't know how to copy: {}",src.display())));}Ok(())
}/// 把现有目录`src`复制到目标路径`dst`
fn copy_dir_to(src: &Path, dst: &Path) -> io::Result<()> {if !dst.is_dir() {fs::create_dir(dst)?;}for entry_result in src.read_dir()? {let entry = entry_result?;let file_type = entry.file_type()?;copy_to(&entry.path(), &file_type, &dst.join(entry.file_name()))?;}Ok(())
}

特定于平台的特性

目前为止,copy_to 函数既可以复制文件也可以复制目录。接下来我们还打 算在 Unix 上支持符号链接。 没有可移植的方法来创建同时适用于 Unix 和 Windows 的符号链接,但标准库提 供了一个特定于 Unix 的 symlink 函数:

use std::os::unix::fs::symlink;

copy_to 中的 if 表达式添加 一个分支即可:

rust">...
} else if src_type.is_symlink() {let target = src.read_link()?;symlink(target, dst)?;
...

如果只是为 Unix 系统(如 Linux 和 macOS)编译我们的程序,那么就能这么用。

std::os 模块包含各种特定于平台的特性,比如 symlink。std::os 在标准 库中的实际主体如下所示(这里为了看起来整齐,调整了代码格式):

rust">//! 特定于操作系统的功能
#[cfg(unix)] 					pub mod unix;
#[cfg(windows)] 				pub mod windows;
#[cfg(target_os = "ios")] 		pub mod ios;
#[cfg(target_os = "linux")] 	pub mod linux;
#[cfg(target_os = "macos")] 	pub mod macos;
...

\#[cfg] 属性表示条件编译:这些模块中的每一个仅在某些平台上存在。这就是 为什么使用 std::os::unix 修改后的程序只能针对 Unix 成功编译,因为在其他平台上 std::os::unix 不存在。 如果希望代码在所有平台上编译,并支持 Unix 上的符号链接,则必须也在程序 中使用 #[cfg]。在这种情况下,最简单的方法是在 Unix 上导入 symlink,同时在其他系统上定义自己的 symlink 模拟实现:

rust">#[cfg(unix)]
use std::os::unix::fs::symlink;/// 在未提供`symlink`的平台上提供的模拟实现
#[cfg(not(unix))]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, _dst: Q)-> std::io::Result<()>
{Err(io::Error::new(io::ErrorKind::Other,format!("can't copy symbolic link: {}",src.as_ref().display())))
}

symlink 只是特殊情况。大多数特定于 Unix 的特性不是独立函数,而是将新方法添加到标准库类型的扩展特型

prelude 模块可用于同时启用所有这些扩展:use std::os::unix::prelude::*;

在 Unix 上,这会向 std::fs::Permissions 添加 .mode() 方法,从 而支持表达 Unix 权限所需的底层 u32 值。同样,这还会给 std::fs::Metadata 添加一些访问器方法,从而得以访问底层 struct stat 的字段(比如 .uid() 可获得文件所有者的用户 ID)为 std::fs::Metadata 添加了访问器。

总而言之,std::os 中的内容非常基础。更多特定于平台的功能可通过第三方 crate 获得,比如用于访问 Windows 注册表的 winreg

网络

要编写较底层的网络代码,可以使用 std::net 模块,该模块为 TCP 网络和 UDP 网络提供了跨平台支持。

可以使用 native_tls crate 来支持 SSL/TLS。

这些模块为网络上直接的、阻塞型的输入和输出提供了一些基础构件。用几行代码就可以编写一个简单的服务器,只要使用 std::net 并为每个连接启动一个线程即可。

下面是一个“回显”(echo)服务器:

rust">use std::net::TcpListener;
use std::io;
use std::thread::spawn;/// 不断接受连接,为每个连接启动一个线程
fn echo_main(addr: &str) -> io::Result<()> {let listener = TcpListener::bind(addr)?;println!("listening on {}", addr);loop {// 等待客户端连入let (mut stream, addr) = listener.accept()?;println!("connection received from {}", addr);// 启动一个线程来处理此客户端let mut write_stream = stream.try_clone()?;spawn(move || { // 回显从`stream`中收到的一切io::copy(&mut stream, &mut write_stream).expect("error in client thread: ");println!("connection closed");});}
}
fn main() {echo_main("127.0.0.1:17007").expect("error: ");
}

回显服务器会简单地重复发给它的所有内容

对于高性能服务器,需要使用异步输入和输出

更高层级的协议由第三方 crate 提供支持。

例如,reqwest crate 为 HTTP 客户 端提供了一个漂亮的 API。下面是一个完整的命令行程序,该程序可以通过 http: 或 https: URL 获取对应文档,并将其内容打印到你的终端。此代码是使用 reqwest = “0.11” 编写的,并启用了其 “blocking” 特性。reqwest 还提供了一个异步接口。

rust">use std::error::Error;
use std::io;fn http_get_main(url: &str) -> Result<(), Box<dyn Error>> {// 发送HTTP请求并获取响应let mut response = reqwest::blocking::get(url)?;if !response.status().is_success() {Err(format!("{}", response.status()))?;}// 读取响应体并写到标准输出let stdout = io::stdout();io::copy(&mut response, &mut stdout.lock())?;Ok(())
}
fn main() {let args: Vec<String> = std::env::args().collect();if args.len() != 2 {eprintln!("usage: http-get URL");return;}if let Err(err) = http_get_main(&args[1]) {eprintln!("error: {}", err); }
}

actix-web 框架为 HTTP 服务器提供了一些高层次抽象,比如 Service 特型 和 Transform 特型,这两个特型可以帮助你从一些可插接部件组合出应用程序。

websocket crate 实现了 WebSocket 协议。


http://www.ppmy.cn/devtools/150919.html

相关文章

C语言二级考试

你必须知道的 二级考试不是编写程序&#xff0c;或者说不只是编程的考核&#xff0c;它还会考核计算机C语言相关语言还有内涵等基础知识&#xff0c;比较全面综合&#xff08;说人话&#xff0c;要看最新考纲具备一定的基础知识&#xff09; 考试时间 120 分钟 分值 100 分&…

Python编程中的两种主要的编程模式

在Python编程中&#xff0c;有两种主要的编程模式被广泛使用&#xff1a;面向过程编程&#xff08;Procedural Programming&#xff09; 和 面向对象编程&#xff08;Object-Oriented Programming, OOP&#xff09;。这两种模式各有优缺点&#xff0c;适用于不同的场景。 1. 面…

RustDesk ID更新脚本

RustDesk ID更新脚本 此PowerShell脚本自动更新RustDesk ID和密码&#xff0c;并将信息安全地存储在Bitwarden中。 特点 使用以下选项更新RustDesk ID&#xff1a; 使用系统主机名生成一个随机的9位数输入自定义值 为RustDesk生成新的随机密码将RustDesk ID和密码安全地存储…

UML系列之Rational Rose笔记九:组件图

一、新建组件图 二、组件图成品展示 三、工作台介绍 最主要的还是这个component组件&#xff1b; 然后还有这几个&#xff0c;正常是用不到的&#xff1b;基本的使用第四部分介绍一下&#xff1a; 四、基本使用示例 这些&#xff0c;主要是运用package还有package specifica…

gateway worker 分布式

有三个文件start_register.php&#xff0c;start_gateway.php&#xff0c;start_businessworker.php&#xff0c; 一、start_register.php &#xff08;1&#xff09;是用于通讯的&#xff0c;注册地址的&#xff1b; 二、start_gateway.php &#xff08;1&#xff09;用于跟…

css实现响应式详解

一、媒体查询&#xff08;Media Queries&#xff09; 基本概念 媒体查询是 CSS3 中用于根据不同的设备特性&#xff08;如屏幕宽度、高度、设备类型等&#xff09;应用不同样式规则的技术。它允许你为特定的媒体类型&#xff08;如屏幕、打印、手持设备等&#xff09;和条件&a…

【BLE】CC2541之AT指令实时修改设备名称

本文最后修改时间&#xff1a;2020年02月07日 19:32 一、本节简介 本文以SimpleBLEPeripheral工程为例&#xff0c;介绍如何使用AT指令来修改设备名称&#xff0c;且不需要重启设备&#xff0c;断开连接时即可看到广播中修改后的设备名称。 二、实验平台 1&#xff09;CC2541…

【Apache Paimon】-- 源码解读之 PaimonSparkSessionExtensions

目录 1. spark.sql.extensions 简介 2. PaimonSparkSessionExtensions 详解 (1) 解析器扩展 (Parser Extensions) (2) 分析器扩展 (Analyzer Extensions) (3) 表值函数扩展 (Table Function Extensions) (4) 优化器扩展 (Optimizer Extensions) (5) 执行计划扩展 (Plann…