使用 c# 将 FileStream 编码为 base64

Posted

技术标签:

【中文标题】使用 c# 将 FileStream 编码为 base64【英文标题】:Encode a FileStream to base64 with c# 【发布时间】:2013-10-08 16:02:04 【问题描述】:

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

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

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

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

【问题讨论】:

我不确定我是否完全理解您的问题,但可以使用内置类来提供将二进制数据转换为基数 64 数据或从基数 64 数据转换的流。然后,您可以在写入和文件输出流之间插入这样的流(例如通常通过压缩流和加密流来完成)。一个例子在这里:netpl.blogspot.co.uk/2011/05/builtin-base64-streaming.html How to convert an Stream into a byte[] in C#?的可能重复 Is there a Base64Stream for .NET? where?的可能重复 this 不是答案吗? 别忘了:stream.Seek(0, SeekOrigin.Begin);在方法的开头... ;-) 【参考方案1】:

一个简单的 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 的方式。

【讨论】:

这不是一个通用的解决方案,因为许多流类型不支持 Length 或 Seek() Rhys Bevilaqua,通常您需要寻找到流的开头来阅读全部内容,或者“知道”您在开头(这违反了 SOLID 原则)。只有流不实现这两种方法 - 几乎所有其他方法(内存、文件等)都有它。你总是可以有第二个实现,你逐个缓冲区读取直到流的“结束”,但这不是那么有效和直接。 我厌倦了在 .NET 中将所有内容缓冲到 byte[] 中。这是非常浪费的。现在是端到端流 API 的最佳时机。 更新了一点以支持 MemoryStream 类的 ToArray() 方法。【参考方案2】:

在处理大型流时,例如超过 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);

【讨论】:

注意 leaveOpen 属性在 netstandard2.0 中无效,但在 472 中被接受 这几乎是完美的,除了生成的流不支持我需要的 Seek。我发誓我每天都在使用 C# System lib,我发现我需要正确地重新实现一些东西。 @YarekT 如果您使用的是流,那么您永远不需要寻找(唯一的例外是FileStream)。如果您发现自己需要在非磁盘流中查找,那么您的系统可能设计错误。 @Dai 有趣。我正在使用FluentFtpUploadAsync 方法,该方法采用流。我以为它不会寻找,但它确实如此。我猜他们是为常规文件流设计的。 @YarekT 根据这个问题,这是因为 FluentFTP 需要事先知道流的长度(这是合理的),但是它通过要求流可搜索来做到这一点(这是错误的 - 但结果System.IO.Stream 的设计不会让它暴露 Length 除非 CanSeek == true, grrr): github.com/robinrodricks/FluentFTP/issues/668【参考方案3】:

一个简单的扩展方法

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));
    

【讨论】:

这会导致stream 被完全缓冲到内存中(也有多个副本,因为您没有设置初始capacity)。对于大于几兆字节的文件,这不是一个实用的解决方案 - 并且对于大于 2GB 的文件肯定会中断(因为MemoryStream 在内部使用单个Byte[])。人们还报告 MemoryStream 超过 256MB 的大小:***.com/questions/15595061/… @Dai 我认为,如果您尝试对大流进行 base64 编码,也许有比 base64 编码更好的选择?我这样做是为了在 JSON Web 请求中提供一个文件,如果有一个大文件 (MBs+),那么这样做对我来说没有意义。 这个解决方案浪费了大量的内存(几次)和cpu时间。它可以做得非常优化! @VasilPopov 请发布您的最佳解决方案。 @PhillipCopley 我在下面做了几篇文章【参考方案4】:

你可以尝试类似的东西:

    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));
    

【讨论】:

1.34从何而来? 一个字节包含 8 位。 base64 不使用字节,而是使用字符。不是任何字符,而是可以转换为 6 位的特定字符。所以 in-array 比 our-array 小了 6/8 倍。 8 除以 6 是 1,33333,所以如果你取 1.34,输出数组总是足够大。 您需要从Convert.ToBase64CharArray 获取新尺寸,然后执行Array.Resize<Char>(ref base64Chars, newSize);。否则,最终输出中会有额外的字节。 1.34 错了!额外的 0.0333 为您提供了一些空间,这对于小长度来说太小了,而对于大数组来说则不必要的大。你应该做一个天花板(int)Math.Ceiling(stream.Length * 8.0 / 6.0),而不是地板(铸成int),这样你就可以得到确切的长度。 请注意,如果stream 恰好是MemoryStream,您可以只使用stream.ToArray() 并避免8/6 计算。【参考方案5】:

您还可以将字节编码为 Base64。如何从流中获取此信息,请参见此处:How to convert an Stream into a byte[] in C#?

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

【讨论】:

由于输入是流,更有用的答案是将流转换为另一个(B64 编码)流的答案。【参考方案6】:

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

    读入整个文件,base64编码,重写编码数据。 以较小的片段读取文件,同时进行编码。编码为同一目录中的临时文件。完成后,删除原始文件,并重命名临时文件。

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

【讨论】:

以上是关于使用 c# 将 FileStream 编码为 base64的主要内容,如果未能解决你的问题,请参考以下文章

如何定期将 c# FileStream 刷新到磁盘?

(12)C#传智:File类,泛型,字典,FileStream,StreamReader,多态

C# 计算输入和输出 FileStream 的 MD5

C# 之 FileStream类介绍

C# 之 FileStream类介绍

C# 使用从 FileStream 创建的 StreamWriter 覆盖文件