rust 是否可以多次重用HTTP请求主体而不将其读入内存?

kpbwa7wx  于 2023-02-23  发布在  其他
关注(0)|答案(2)|浏览(124)

我的需求是一个大文件上传,使用curl -T <file>,服务器端是一个HTTP Server在Rust中,实现可以是actix-web(或者hyper,如果其他实现更合适的话),使用HTTP/1.1,在HTTP/1.1中,服务器只能读取文件上传数据流一次,我想计算文件的MD5SHA-1SHA-256(或BLAKE3),而不需要将文件数据流读入内存,然后上传到S3,如果有并行计算的解决方案会更好,但可能会更复杂,关键还是要重用流。
最好不要使用channel来实现它,因为流将流经多个channel来计算校验和,这可能比重用流来同时计算性能更低。
这需要基于Rust 2021 Edition,actix-web 4.x版本(最新是4.3.0),而且如果使用tokio,至少要1.0.0+(我目前是1.25.0),这是因为Rust中库版本的兼容性太差,各个次要版本之间可能存在API不兼容的问题。
请问我的需求是否可以用Rust来实现?在Go语言中,除了io.Pipe,似乎没有更好的解决方案,但是io.Pipe的性能看起来并不好。
非常感谢您的建议和耐心!
更新:您可以使用Go或Java来满足此需求。

vof42yt1

vof42yt11#

不把文件放到内存中就不可能进行计算,至少在标准硬件上是这样的,解决方案是用BufRead的实现程序(比如BufReader)来分块阅读文件。
这在synchronous rust中是完全可能的,但是因为你指定时雄,所以我使用tokio的IO类型和traits创建了这个async,你可以使用标准库的IO类型和traits创建同样的东西,它们的命名是相似的。
首先,一个函数接受一个读取器,更新一个哈希函数切片,然后将这些字节写入一个写入器,这基本上是tokio::io::copy_buf,但中间有哈希函数。

pub async fn calculate_hashes<R, W>(
    mut reader: R,
    hashers: &mut [&mut dyn Update],
    mut writer: W,
) -> Result<(), std::io::Error>
where
    R: AsyncBufRead + Unpin,
    W: AsyncWrite + Unpin,
{
    loop {
        let part = reader.fill_buf().await?;
        if part.is_empty() {
            break;
        }

        for hasher in hashers.iter_mut() {
            hasher.update(part);
        }

        writer.write_all(part).await?;

        let part_len = part.len();
        reader.consume(part_len);
    }

    Ok(())
}

这使用了digest机箱中的一个特征:幸运的是,已经有了使用digest的特定哈希函数的实现:md-5sha1sha2(确保您使用的版本依赖于相同的digest版本,目前这些版本都是0.10.5)。
下面是正在使用的函数:

pub async fn md5_sha1_sha256<R, W>(
    reader: R,
    writer: W,
) -> Result<(Md5, Sha1, Sha256), std::io::Error>
where
    R: AsyncBufRead + Unpin,
    W: AsyncWrite + Unpin,
{
    let mut md5_hasher = Md5::new();
    let mut sha1_hasher = Sha1::new();
    let mut sha256_hasher = Sha256::new();

    calculate_hashes(
        reader,
        &mut [&mut md5_hasher, &mut sha1_hasher, &mut sha256_hasher],
        writer,
    )
    .await?;

    Ok((md5_hasher, sha1_hasher, sha256_hasher))
}

如果你想更进一步,你可以创建一个 Package 器类型,为所有的Update类型实现AsyncWrite,这样你就不需要分离hasherswriter了,你也可以反过来把hashers分离成你特定的散列函数类型或者特定数量的泛型参数。这避免了dyn。我在这里使用了dyn切片,这样在不太具体的情况下对其他人有帮助。
Here's a link to the whole thing on playground.由于缺少依赖项,它无法编译。

a6b3iqyw

a6b3iqyw2#

下面是Go语言的一个实现思路:计算请求主体的校验和,同时将其写入S3存储器。

// ...

md5Hasher := md5.New()
sha1Hasher := sha1.New()
sha256Hasher := sha256.New()

type Checksum struct {
    Md5    string `json:"md5"`
    Sha1   string `json:"sha1"`
    Sha256 string `json:"sha256"`
}

hasher := io.MultiWriter(md5Hasher, sha1Hasher, sha256Hasher)
tr := io.TeeReader(r.Body, hasher)

// Upload the file to S3.
_, err := uploader.Upload(&s3manager.UploadInput{
    Bucket: aws.String("bucket"),
    Key:    aws.String("key"),
    Body:   tr,
})
if err != nil {
    log.Printf("failed to upload file, %v", err)
    w.WriteHeader(http.StatusInternalServerError)
    return
}

checksum := Checksum{
    Md5:    fmt.Sprintf("%x", md5Hasher.Sum(nil)),
    Sha1:   fmt.Sprintf("%x", sha1Hasher.Sum(nil)),
    Sha256: fmt.Sprintf("%x", sha256Hasher.Sum(nil)),
}

respBody, _ := json.MarshalIndent(checksum, "", "  ")
fmt.Printf("%s\n", respBody)

w.Write(respBody)

但是现在我在想,是否可以先通过io.MultiWriter计算Checksum,然后再决定是否上传Request Body,但是在这一点上,似乎没有好的方法可以将已经读取的Request Body用作上传逻辑,而不将其读取到内存中或写入临时文件中。

相关问题