使用 c# 将 FileStream 编码为 base64
本文关键字:base64 编码 FileStream 使用 | 更新日期: 2023-09-27 18:33:37
我知道如何将一个简单的字符串编码/解码到base64。
但是,如果数据已经写入FileStream对象,我将如何做到这一点。假设我只能访问 FileStream 对象,而不能访问其中以前存储的原始数据。在将文件流刷新到文件之前,如何将文件流编码为 base64。
Ofc 我可以在将 FileStream 写入文件后打开我的文件并对其进行编码/解码,但我想在一个步骤中完成所有这些操作,而无需一个接一个地执行两个文件操作。文件可能会更大,并且在不久前刚刚保存后,加载、编码和再次保存它也需要双倍的时间。
也许你们中的某个人知道更好的解决方案?例如,我可以将FileStream转换为字符串,对字符串进行编码,然后将字符串转换回FileStream,或者我会做什么以及这样的代码会是什么样子?
扩展方法
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(( 方法并对此进行编码。
由于文件会更大,因此您在如何执行此操作方面没有太多选择。您无法就地处理该文件,因为这会破坏您需要使用的信息。我可以看到您有两个选项:
- 读入整个文件,base64编码,重写编码数据。
- 以较小的部分读取文件,并在此过程中进行编码。编码为同一目录中的临时文件。完成后,删除原始文件,然后重命名临时文件。
当然,流的全部意义在于避免这种情况。与其创建内容并将其填充到文件流中,不如将其填充到内存流中。然后对其进行编码,然后才保存到磁盘。
建议使用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);
}
}