在 WinForms 或服务端代码里,经常会拿到一个「只能向前读」的 Stream(网络响应体、上传流、压缩流等),而下游 API 又要求 MemoryStreambyte[]。把网络流完整落到内存,看起来简单,实际很容易踩三个坑:流是否可读完、位置是否归零、以及大文件把内存打爆。

为什么要转 MemoryStream

常见动机:

  1. 下游只接受可 Seek 的流:例如某些第三方库要 stream.Position = 0 再读一遍。
  2. 需要多次消费同一份内容:先算 MD5,再上传,再写本地缓存。
  3. 接口边界清晰:网络 I/O 与业务处理解耦,业务侧只面对内存里的字节。

注意:这不是默认最优方案。若文件可能很大,应优先「边读边写到磁盘」或流式处理,而不是无脑 ToArray()

推荐写法(.NET 现代 API)

如果目标框架支持,优先用 CopyTo / CopyToAsync

public static MemoryStream ToMemoryStream(Stream input)
{
    if (input == null) throw new ArgumentNullException(nameof(input));
    if (!input.CanRead) throw new InvalidOperationException("Stream is not readable.");

    var ms = new MemoryStream();
    input.CopyTo(ms);          // 内部按缓冲块拷贝
    ms.Position = 0;           // 关键:调用方通常希望从头读
    return ms;
}

public static async Task<MemoryStream> ToMemoryStreamAsync(
    Stream input, CancellationToken ct = default)
{
    var ms = new MemoryStream();
    await input.CopyToAsync(ms, 81920, ct).ConfigureAwait(false);
    ms.Position = 0;
    return ms;
}

对应地,如果只要字节数组:

public static byte[] ReadAllBytes(Stream input)
{
    using var ms = ToMemoryStream(input);
    return ms.ToArray();
}

兼容旧代码的缓冲读法

早期笔记里常见「固定 16KB 缓冲循环读」:

public static byte[] ReadFull(Stream input)
{
    byte[] buffer = new byte[16 * 1024];
    using (var ms = new MemoryStream())
    {
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            ms.Write(buffer, 0, read);
        }
        return ms.ToArray();
    }
}

public static MemoryStream ConvertStreamToMemoryStream(Stream stream)
{
    var memoryStream = new MemoryStream();
    if (stream == null) return memoryStream;

    byte[] buffer = ReadFull(stream);
    if (buffer != null && buffer.Length > 0)
    {
        memoryStream.Write(buffer, 0, buffer.Length);
        memoryStream.Position = 0;
    }
    return memoryStream;
}

这种写法和 CopyTo 本质相同,但有两点建议改掉:

容易踩的坑

1. 网络流通常不能 Length / Seek

HttpWebResponse.GetResponseStream() 往往不支持 CanSeek。不要写:

// 危险:很多网络流会抛 NotSupportedException
byte[] buf = new byte[stream.Length];
stream.Read(buf, 0, buf.Length);

应始终按「读到返回 0」循环,或用 CopyTo

2. 流可能已被部分消费

若前面的代码已经读过 header 或探测字节,再转 MemoryStream 会丢数据。需要时在入口就完整缓存。

3. 大文件 OOM

把 2GB 上传直接 ToArray() 会压垮进程。阈值:

4. 用完记得释放

MemoryStream 一般较轻,但持有大数组时仍应 using,并避免无意中复制多份 ToArray()

和 HttpClient 的配合示例

using var resp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
await using var net = await resp.Content.ReadAsStreamAsync();
using var ms = await ToMemoryStreamAsync(net);
// 后续:解析、校验、再转发

小结

场景 建议
小文件、需多次读取 CopyToMemoryStream,并重置 Position
只需一次字节数组 ReadAllBytes / CopyTo + ToArray
大文件 落盘或流式处理,勿整包进内存
旧框架 16KB 缓冲循环读即可,逻辑与 CopyTo 等价

把网络流转成内存流本身不难,难的是在 正确性(读全、归零)资源边界(内存上限) 上一次做对。