使用 c# 将 FileStream 编码为 base64

本文关键字:base64 编码 FileStream 使用 | 更新日期: 2023-09-27 18:33:37

我知道如何将一个简单的字符串编码/解码到base64

但是,如果数据已经写入FileStream对象,我将如何做到这一点。假设我只能访问 FileStream 对象,而不能访问其中以前存储的原始数据。在将文件流刷新到文件之前,如何将文件流编码为 base64

Ofc 我可以在将 FileStream 写入文件后打开我的文件并对其进行编码/解码,但我想在一个步骤中完成所有这些操作,而无需一个接一个地执行两个文件操作。文件可能会更大,并且在不久前刚刚保存后,加载、编码和再次保存它也需要双倍的时间。

也许你们中的某个人知道更好的解决方案?例如,我可以将FileStream转换为字符串,对字符串进行编码,然后将字符串转换回FileStream,或者我会做什么以及这样的代码会是什么样子?

使用 c# 将 FileStream 编码为 base64

一个简单的

扩展方法

public static class Extensions
{
    public static Stream ConvertToBase64(this Stream stream)
    {
        byte[] bytes;
        using (var memoryStream = new MemoryStream())
        {
            stream.CopyTo(memoryStream);
            bytes = memoryStream.ToArray();
        }
        string base64 = Convert.ToBase64String(bytes);
        return new MemoryStream(Encoding.UTF8.GetBytes(base64));
    }
}

在处理大型流时,例如大小超过 4GB 的文件 - 您不希望将文件加载到内存中(作为Byte[](,因为它不仅非常慢,而且可能导致崩溃,因为即使在 64 位进程中,Byte[]也不能超过 2GB(或 4GB 与 gcAllowVeryLargeObjects(。

幸运的是,.NET 中有一个名为 ToBase64Transform 的简洁帮助程序,它以块的形式处理流。出于某种原因,Microsoft把它放在System.Security.Cryptography中,它实现了ICryptoTransform(用于CryptoStream(,但忽略了它("任何其他名字的玫瑰......"(只是因为您没有执行任何加密任务。

您可以像这样使用它CryptoStream

using System.Security.Cryptography;
using System.IO;
//
using( FileStream   inputFile    = new FileStream( @"C:'VeryLargeFile.bin", FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 1024 * 1024, useAsync: true ) ) // When using `useAsync: true` you get better performance with buffers much larger than the default 4096 bytes.
using( CryptoStream base64Stream = new CryptoStream( inputFile, new ToBase64Transform(), CryptoStreamMode.Read ) )
using( FileStream   outputFile   = new FileStream( @"C:'VeryLargeBase64File.txt", FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1024 * 1024, useAsync: true ) )
{
    await base64Stream.CopyToAsync( outputFile ).ConfigureAwait(false);
}

一个简单的 Stream 扩展方法可以完成这项工作:

public static class StreamExtensions
{
    public static string ConvertToBase64(this Stream stream)
    {
        if (stream is MemoryStream memoryStream)
        {
            return Convert.ToBase64String(memoryStream.ToArray());
        }
        var bytes = new Byte[(int)stream.Length];
        stream.Seek(0, SeekOrigin.Begin);
        stream.Read(bytes, 0, (int)stream.Length);
        return Convert.ToBase64String(bytes);
    }
}

读取(和写入(的方法并针对相应的类(无论是文件流,内存流等(进行了优化,并将为您完成工作。对于像这样的简单任务,不需要读者等。

唯一的缺点是流被复制到字节数组中,但不幸的是,这就是通过 Convert.ToBase64String 转换为 base64 的方式。

你可以尝试这样的事情:

    public Stream ConvertToBase64(Stream stream)
    {
        Byte[] inArray = new Byte[(int)stream.Length];
        Char[] outArray = new Char[(int)(stream.Length * 1.34)];
        stream.Read(inArray, 0, (int)stream.Length);
        Convert.ToBase64CharArray(inArray, 0, inArray.Length, outArray, 0);
        return new MemoryStream(Encoding.UTF8.GetBytes(outArray));
    }

您还可以将字节编码为 Base64。如何从流中获取此信息,请参阅此处: 如何在 C# 中将流转换为 byte[]?

或者我认为也应该可以使用.ToString(( 方法并对此进行编码。

由于文件会更大,因此您在如何执行此操作方面没有太多选择。您无法就地处理该文件,因为这会破坏您需要使用的信息。我可以看到您有两个选项:

  1. 读入整个文件,base64编码,重写编码数据。
  2. 以较小的部分读取文件,并在此过程中进行编码。编码为同一目录中的临时文件。完成后,删除原始文件,然后重命名临时文件。

当然,流的全部意义在于避免这种情况。与其创建内容并将其填充到文件流中,不如将其填充到内存流中。然后对其进行编码,然后才保存到磁盘。

建议使用ToBase64Transform的答案是有效的,但有一个很大的问题。不确定这是否应该是一个答案,但如果我知道这一点,它会为我节省很多时间。

我在ToBase64Transform遇到的问题是,一次读取 3 个字节是硬编码的。如果对构造函数CryptoStream中指定的输入流的每次写入都类似于 websocket 或任何具有不平凡开销或延迟的东西,这可能是一个巨大的问题。

底线 - 如果你正在做这样的事情:

using var cryptoStream = new CryptoStream(httpRequestBodyStream, new ToBase64Transform(), CryptoStreamMode.Write);

可能值得分叉类ToBase64Transform将硬编码的 3/4 字节值修改为更大的值,以便减少写入次数。就我而言,使用默认的 3/4 值,传输速率约为 100 KB/s。更改为 768/1024(相同比率(有效,传输速率约为 50-100 MB/s,因为写入次数更少。

    public class BiggerBlockSizeToBase64Transform : ICryptoTransform
    {
        // converting to Base64 takes 3 bytes input and generates 4 bytes output
        public int InputBlockSize => 768;
        public int OutputBlockSize => 1024;
        public bool CanTransformMultipleBlocks => false;
        public virtual bool CanReuseTransform => true;
        public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
        {
            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
            // For now, only convert 3 bytes to 4
            byte[] tempBytes = ConvertToBase64(inputBuffer, inputOffset, 768);
            Buffer.BlockCopy(tempBytes, 0, outputBuffer, outputOffset, tempBytes.Length);
            return tempBytes.Length;
        }
        public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
        {
            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
            // Convert.ToBase64CharArray already does padding, so all we have to check is that
            // the inputCount wasn't 0
            if (inputCount == 0)
            {
                return Array.Empty<byte>();
            }
            // Again, for now only a block at a time
            return ConvertToBase64(inputBuffer, inputOffset, inputCount);
        }
        private byte[] ConvertToBase64(byte[] inputBuffer, int inputOffset, int inputCount)
        {
            char[] temp = new char[1024];
            Convert.ToBase64CharArray(inputBuffer, inputOffset, inputCount, temp, 0);
            byte[] tempBytes = Encoding.ASCII.GetBytes(temp);
            if (tempBytes.Length != 1024)
                throw new Exception();
            return tempBytes;
        }
        private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
        {
            if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
        }
        // Must implement IDisposable, but in this case there's nothing to do.
        public void Dispose()
        {
            Clear();
        }
        public void Clear()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing) { }
        ~BiggerBlockSizeToBase64Transform()
        {
            // A finalizer is not necessary here, however since we shipped a finalizer that called
            // Dispose(false) in desktop v2.0, we need to keep it in case any existing code had subclassed
            // this transform and expects to have a base class finalizer call its dispose method.
            Dispose(false);
        }
    }