System.OutOfMemoryException .NET 4.8 WPF应用程序

zwghvu4y  于 2023-11-20  发布在  .NET
关注(0)|答案(1)|浏览(189)

我正在开发一个WPF(WCF架构)应用程序。这个应用程序有2个主要的解决方案,客户端(前端)和控制器(后端),客户端与后端通信的方式是通过反射调用,本质上它建立了一个XML文档,包含要执行的方法,方法所在的命名空间和方法参数等信息。
所以这个具体的方法是上传文档。它可以完美地工作,直到文件大小> 1GB。当客户端为反射调用构建XML文档时,就会发生错误。
我们像这样添加到XML文档中:

  1. result.Add(new XElement(itemProperty.Name, new XCData(data)));

字符串
在这种情况下,itemProperty.Name是方法名称,XCData是方法args。所以要上传的文档显然是参数,我们在byte[]中接收它。我们需要将它作为字符串传递给XCData构造函数,因此使用以下内容:

  1. string data= Convert.ToBase64String((byte[])value)


请注意,这适用于较小的文件,但对于1GB的文件,当试图将byte[]转换为base64 String时,会抛出“System.OutOfMemoryException”异常。
我尝试过分块阅读数组,但是当调用stringBuilder.ToString()时,同样的异常被抛出.

  1. public static string HandleBigByteArray(byte[] arr)
  2. {
  3. try
  4. {
  5. #region approach 2
  6. int chunkSize = 104857600;//100MB
  7. StringBuilder b64StringBuilder = new StringBuilder();
  8. int offset = 0;
  9. while (offset < arr.Length)
  10. {
  11. int remainingBytes = arr.Length - offset;
  12. int bytesToEncode = Math.Min(chunkSize, remainingBytes);
  13. string base64Chunk = Convert.ToBase64String(arr, offset, bytesToEncode);
  14. b64StringBuilder.Append(base64Chunk);
  15. offset += bytesToEncode;
  16. }
  17. return b64StringBuilder.ToString();
  18. #endregion
  19. }
  20. catch (Exception)
  21. {
  22. throw;
  23. }
  24. }


我不知道该怎么做,也不知道如何进一步调试/处理这个问题。

vnzz0bqm

vnzz0bqm1#

这里的基本问题是,在构造Base64字符串时,当您执行b64StringBuilder.ToString()时,您试图超过系统上最大可能的.NET字符串长度,正如HitScan在this answer to * What is the maximum possible length of a .NET string? * 中所解释的那样,在64位系统上,最多只能有int.MaxValue / 2个字符。
为了进一步细分,您可以在.NET Framework上分配的最大连续内存块是int.MaxValue字节,即2GB。char占用2个字节,而Base64编码将字符数增加了33%,因此您可以编码的最大字节数组大小约为3/4GB--这正是您所看到的。
(Note如果您设置gcAllowVeryLargeObjects,您将能够在内存中分配最多4GB的数组,因此,如果您这样做,请将上述计算结果乘以系数2。)

若要解决此特定问题,您可以建立一个XText序列,其中包含有限大小的部分区块,并将它们全部新增至itemProperty.Name元素,而不是建立一个包含byte []数组之整个Base64内容的单一XCData。写入XML时,它们会格式化为单一连续文字值。

为此,首先介绍以下扩展方法:

  1. public static partial class XmlExtensions
  2. {
  3. const int DefaultChunkLength = 8000;
  4. const int Base64BytesPerChunk = 3; // Base64 encodes 3 bytes to 4 characters.
  5. public static IEnumerable<XText> ToBase64XTextChunks(this Stream stream, int chunkLength = DefaultChunkLength)
  6. {
  7. return stream.ToBase64StringChunks(chunkLength).Select(s => new XText(s));
  8. }
  9. public static IEnumerable<XText> ToBase64XTextChunks(this IEnumerable<byte []> chunks, int chunkLength = DefaultChunkLength)
  10. {
  11. return chunks.Select(b => new ArraySegment<byte>(b)).ToBase64XTextChunks(chunkLength);
  12. }
  13. // In .NET Core I would use Memory<T> and/or ReadOnlyMemory<T> instead of ArraySegment<T>.
  14. public static IEnumerable<XText> ToBase64XTextChunks(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength = DefaultChunkLength)
  15. {
  16. return chunks.ToBase64StringChunks(chunkLength).Select(s => new XText(s));
  17. }
  18. internal static IEnumerable<string> ToBase64StringChunks(this Stream stream, int chunkLength = DefaultChunkLength)
  19. {
  20. if (stream == null)
  21. throw new ArgumentNullException("stream");
  22. if (chunkLength < 1 || chunkLength > int.MaxValue / Base64BytesPerChunk)
  23. throw new ArgumentOutOfRangeException("chunkLength < 1 || chunkLength > int.MaxValue / Base64BytesPerChunk");
  24. var buffer = new byte[Math.Max(300, Base64BytesPerChunk * DefaultChunkLength)];
  25. return ToBase64StringChunksEnumerator(stream.ReadAllByteChunks(buffer), chunkLength);
  26. }
  27. internal static IEnumerable<string> ToBase64StringChunks(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength = DefaultChunkLength)
  28. {
  29. if (chunks == null)
  30. throw new ArgumentNullException("chunks");
  31. if (chunkLength < 1 || chunkLength > int.MaxValue / 3)
  32. throw new ArgumentOutOfRangeException("chunkLength < 1 || chunkLength > int.MaxValue / 3");
  33. return ToBase64StringChunksEnumerator(chunks, chunkLength);
  34. }
  35. static IEnumerable<string> ToBase64StringChunksEnumerator(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength)
  36. {
  37. var buffer = new byte[Base64BytesPerChunk*chunkLength];
  38. foreach (var chunk in chunks.ToFixedSizedChunks(buffer))
  39. {
  40. yield return Convert.ToBase64String(chunk.Array, chunk.Offset, chunk.Count);
  41. }
  42. }
  43. internal static IEnumerable<ArraySegment<byte>> ReadAllByteChunks(this Stream stream, byte [] buffer)
  44. {
  45. if (stream == null)
  46. throw new ArgumentNullException("stream");
  47. if (buffer == null)
  48. throw new ArgumentNullException("buffer");
  49. if (buffer.Length < 1)
  50. throw new ArgumentException("buffer.Length < 1");
  51. return ReadAllByteChunksEnumerator(stream, buffer);
  52. }
  53. static IEnumerable<ArraySegment<byte>> ReadAllByteChunksEnumerator(Stream stream, byte [] buffer)
  54. {
  55. int nRead;
  56. while ((nRead = stream.Read(buffer, 0, buffer.Length)) > 0)
  57. yield return new ArraySegment<byte>(buffer, 0, nRead);
  58. }
  59. }
  60. public static partial class EnumerableExtensions
  61. {
  62. public static IEnumerable<ArraySegment<T>> ToFixedSizedChunks<T>(this IEnumerable<ArraySegment<T>> chunks, T [] buffer)
  63. {
  64. if (chunks == null)
  65. throw new ArgumentNullException("chunks");
  66. if (buffer.Length == 0)
  67. throw new ArgumentException("buffer.Length == 0");
  68. return ToFixedSizedChunksEnumerator(chunks, buffer);
  69. }
  70. static IEnumerable<ArraySegment<T>> ToFixedSizedChunksEnumerator<T>(IEnumerable<ArraySegment<T>> chunks, T [] buffer)
  71. {
  72. int bufferIndex = 0;
  73. bool anyRead = false, anyReturned = false;
  74. foreach (var chunk in chunks)
  75. {
  76. anyRead = true;
  77. int chunkIndex = 0;
  78. while (chunkIndex < chunk.Count)
  79. {
  80. int toCopy = Math.Min(buffer.Length - bufferIndex, chunk.Count - chunkIndex);
  81. if (toCopy > 0)
  82. {
  83. chunk.CopyTo(chunkIndex, buffer, bufferIndex, toCopy);
  84. bufferIndex += toCopy;
  85. if (bufferIndex == buffer.Length)
  86. {
  87. yield return new ArraySegment<T>(buffer, 0, bufferIndex);
  88. bufferIndex = 0;
  89. anyReturned = true;
  90. }
  91. }
  92. chunkIndex += toCopy;
  93. }
  94. }
  95. // If passed an enumerable of empty chunks we should still return one empty chunk. But if there were no chunks at all, return nothing.
  96. if (bufferIndex > 0 || (anyRead && !anyReturned))
  97. yield return new ArraySegment<T>(buffer, 0, bufferIndex);
  98. }
  99. public static void CopyTo<T>(this ArraySegment<T> from, int fromIndex, T [] destination, int destinationIndex, int count)
  100. {
  101. Buffer.BlockCopy(from.Array, checked(from.Offset + fromIndex), destination, destinationIndex, count);
  102. }
  103. }

字符串
现在,假设您的byte[] arr值实际上是从fileName文件中读取的,您可以执行以下操作:

  1. var result = new XElement("root");
  2. var e = new XElement(itemProperty.Name);
  3. using (var stream = File.OpenRead(fileName))
  4. {
  5. foreach (var text in stream.ToBase64XTextChunks())
  6. e.Add(text);
  7. }
  8. result.Add(e);


扩展方法stream.ToBase64XTextChunks()以块为单位读取流,并以块为单位进行编码,这样就不会达到最大数组大小或最大字符串长度。
但如果内存中已经有了巨大的byte [] arr数组,则可以执行以下操作:

  1. foreach (var text in new [] { arr }.ToBase64XTextChunks())
  2. e.Add(text);


注意到

  • 出于性能方面的考虑,我建议将缓冲区和字符串分配保持在80,000字节以下,这样它们就不会在large object heap上运行。
  • 在.NET Core中,我将使用Memory<T>ReadOnlyMemory<T>,而不是旧的ArraySegment<T>

演示小提琴here
话虽如此,您也非常接近达到其他内存限制,包括:

  • 最大byte []阵列大小,即int.MaxValue,即2 GB。

设置gcAllowVeryLargeObjects不会增加此限制。gcAllowVeryLargeObjects只会增加数组可以容纳的内存量。长度仍限制为int.MaxValue
此限制在客户端和服务器端都将是一个问题。

  • StringBuilder可容纳的最大字符数,从引用源可以看出,它是int.MaxValue
  • 您的服务器的可用虚拟内存总量。您当前的设计似乎根本没有限制上载大小 *。这可能会使您的服务器容易受到拒绝服务攻击,攻击者会继续上载数据,直到您的服务器内存不足。

如果您有大量的客户端同时上传大量数据,即使没有客户端尝试DOS攻击,您的服务器也会再次耗尽内存。

  • 客户端的可用虚拟内存。如果客户端在资源受限的环境(如智能手机)中运行,则可能无法分配大量内存。

我建议您重新考虑允许任意大的文件上传并将其缓冲在客户端和服务器端的内存中的设计。即使您决定出于业务原因需要支持大于1或2GB的文件上传,我建议你采用一个一种解决方案,其中使用XmlWriterXmlReader写入和读取内容,而无需将整个内容加载到内存中。要开始,请参阅

展开查看全部

相关问题