在 Rust 中检测没有读取 0 字节的 EOF

Detecting EOF without 0-byte read in Rust

我一直在研究一些代码,这些代码以块的形式从 Read 类型(input)中读取数据,并对每个块进行一些处理。问题是最终的块需要用不同的函数处理。据我所知,有几种方法可以从 Read 中检测到 EOF,但其中 none 感觉特别适合这种情况。我正在寻找更惯用的解决方案。

我目前的做法是维护两个buffer,这样如果下一次读到零字节,也就是EOF,因为buffer的长度是非零的,所以可以保持之前的读取结果:

use std::io::{Read, Result};

const BUF_SIZE: usize = 0x1000;

fn process_stream<I: Read>(mut input: I) -> Result<()> {
    // Stores a chunk of input to be processed
    let mut buf = [0; BUF_SIZE];
    let mut prev_buf = [0; BUF_SIZE];
    let mut prev_read = input.read(&mut prev_buf)?;

    loop {
        let bytes_read = input.read(&mut buf)?;
        if bytes_read == 0 {
            break;
        }

        // Some function which processes the contents of a chunk
        process_chunk(&prev_buf[..prev_read]);

        prev_read = bytes_read;
        prev_buf.copy_from_slice(&buf[..]);
    }

    // Some function used to process the final chunk differently from all other messages
    process_final_chunk(&prev_buf[..prev_read]);
    Ok(())
}

我觉得这是一种非常丑陋的方法,我不需要在这里使用两个缓冲区。

我能想到的另一种方法是强加 Seek on input and use input.read_exact(). I could then check for an UnexpectedEof errorkind 来确定我们已经到达输入的末尾,然后向后查找以再次读取最后的块(再次查找和读取是必要的)这里是因为缓冲区的内容在 UnexpectedEof 错误的情况下未定义)。但这似乎一点也不符合常理:遇到错误、向后查找并再次读取以检测我们是否已到达文件末尾是非常奇怪的。

我理想的解决方案是这样的,使用一个假想的 input.feof() 函数,如果最后一个 input.read() 调用到达 EOF,则 returns 为真,例如 feof syscall in C

fn process_stream<I: Read>(mut input: I) -> Result<()> {
    // Stores a chunk of input to be processed
    let mut buf = [0; BUF_SIZE];
    let mut bytes_read = 0;

    loop {
        bytes_read = input.read(&mut buf)?;

        if input.feof() {
            break;
        }

        process_chunk(&buf[..bytes_read]);
    }

    process_final_chunk(&buf[..bytes_read]);
    Ok(())
}

任何人都可以建议一种更惯用的实现方法吗?谢谢!

既然您认为 read_exact() 是一个可能的解决方案,那么我们可以认为非最终块恰好包含 BUF_SIZE 个字节。 那为什么不尽可能多地读取以填充这样的缓冲区并用函数处理它,然后,当绝对不可能时(因为到达 EOF),用另一个函数处理不完整的最后一个块?

请注意,C 中的 feof() 不会 猜测 下一次读取尝试将达到 EOF;它只是报告在上一次读取尝试期间可能已设置的 EOF 标志。 因此,要设置 EOF 并 feof() 报告它,必须首先遇到返回 0 的读取尝试(如下例所示)。

use std::fs::File;
use std::io::{Read, Result};

const BUF_SIZE: usize = 0x1000;

fn process_chunk(bytes: &[u8]) {
    println!("process_chunk {}", bytes.len());
}
fn process_final_chunk(bytes: &[u8]) {
    println!("process_final_chunk {}", bytes.len());
}

fn process_stream<I: Read>(mut input: I) -> Result<()> {
    // Stores a chunk of input to be processed
    let mut buf = [0; BUF_SIZE];

    loop {
        let mut bytes_read = 0;
        while bytes_read < BUF_SIZE {
            let r = input.read(&mut buf[bytes_read..])?;
            if r == 0 {
                break;
            }
            bytes_read += r;
        }
        if bytes_read == BUF_SIZE {
            process_chunk(&buf);
        } else {
            process_final_chunk(&buf[..bytes_read]);
            break;
        }
    }
    Ok(())
}

fn main() {
    let file = File::open("data.bin").unwrap();
    process_stream(file).unwrap();
}
/*
$ dd if=/dev/random of=data.bin bs=1024 count=10
$ cargo run
process_chunk 4096
process_chunk 4096
process_final_chunk 2048
$ dd if=/dev/random of=data.bin bs=1024 count=8
$ cargo run
process_chunk 4096
process_chunk 4096
process_final_chunk 0
*/

read of std::io::Read returns Ok(n)时,不仅表示the buffer buf has been filled in with n bytes of data from this source.,而且还表示索引n(含)保持不变。考虑到这一点,您实际上根本不需要 prev_buf,因为当 n 为 0 时,缓冲区的所有字节都将保持不变(让它们成为先前读取的那些字节).

prog-fh 的解决方案是您想要进行的那种处理,因为它只会将完整的块交给 process_chunk。由于 read 可能返回 0BUF_SIZE 之间的值,因此这是必需的。有关详细信息,请参阅上面的这一部分 link:

It is not an error if the returned value n is smaller than the buffer size, even when the reader is not at the end of the stream yet. This may happen for example because fewer bytes are actually available right now (e. g. being close to end-of-file) or because read() was interrupted by a signal.

但是,我建议您考虑当您从 read 得到一个 Ok(0) 时应该发生什么,该 Ok(0) 并不代表文件永远结束。看这部分:

If n is 0, then it can indicate one of two scenarios:

  1. This reader has reached its “end of file” and will likely no longer be able to produce bytes. Note that this does not mean that the reader will always no longer be able to produce bytes.

因此,如果您要获得返回 Ok(BUF_SIZE), Ok(BUF_SIZE), 0, Ok(BUF_SIZE) 的读取序列(这完全有可能,它只是 IO 中的一个障碍),您是否不想考虑最后一个 Ok(BUF_SIZE) 作为读取块?如果您将 Ok(0) 永远视为 EOF,那么这里可能是一个错误。

可靠地确定什么应该被视为最后一个块的唯一方法是将预期的长度(以字节为单位,而不是块的数量)作为协议的一部分预先发送。给定一个变量 expected_len,然后您可以通过 expected_len - expected_len % BUF_SIZE 确定最后一个块的起始索引,而结束索引只是 expected_len 本身。