Stream 是 .NET 里"最古老又最常用"的抽象之一——从 1.0 时代就在那儿,每一版 .NET 都在给它打补丁:Read(byte[]) → ReadAsync(byte[]) → Read(Span<byte>) → ReadAsync(Memory<byte>) → ReadByte → PipeReader。如果它真的设计好了,根本不需要这么多补丁。
把 Stream 的所有坑摊开看,背后其实就是同一个设计张力的不同投影:
Stream 试图用"统一抽象"覆盖所有 IO(文件、网络、内存、加密、压缩),但同一个抽象对"高性能网络"和"简单文件读写"是天然矛盾的——前者要零拷贝、背压、内存池,后者只要简单 API。
理解了这层张力,就能看清 .NET 流式 API 二十年的演进:每一层补丁(Span、Memory、ValueTask、Pipelines)都是在重新回答"统一抽象到底要做到什么程度"。本文围绕这条主线把 Stream 从底层机制到选型陷阱彻底讲透。
一、Stream 是什么:抽象基类的雄心与代价
1.1 统一 IO 抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public abstract class Stream : MarshalByRefObject, IAsyncDisposable, IDisposable
{
public abstract bool CanRead { get; }
public abstract bool CanWrite { get; }
public abstract bool CanSeek { get; }
public abstract long Length { get; }
public abstract long Position { get; set; }
public abstract int Read(byte[] buffer, int offset, int count);
public abstract void Write(byte[] buffer, int offset, int count);
public abstract long Seek(long offset, SeekOrigin origin);
public abstract void SetLength(long value);
public abstract void Flush();
}
|
1
2
3
4
5
6
7
8
9
| 设计意图:
让"读文件 / 读网络 / 读内存 / 读加密"用同一套 API
→ 业务代码只依赖 Stream,不依赖具体实现
→ 可以层层包装(装饰器模式)
代价:
→ 抽象被迫提供所有方法,但每个子类都不一定能实现
→ CanRead / CanWrite / CanSeek 标志位遍布 API
→ 不支持的操作直接抛 NotSupportedException
|
1.2 四维能力(实际只有三维常用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 能力 属性 典型实现
─────────────────────────────────────────────────
Read CanRead FileStream / NetworkStream / MemoryStream
Write CanWrite FileStream / NetworkStream / MemoryStream
Seek CanSeek FileStream ✅ / NetworkStream ❌ / MemoryStream ✅
Timeout CanTimeout NetworkStream ✅ / FileStream ❌
CanSeek = false 时:
- Position / Length / SetLength / Seek 都抛 NotSupportedException
- Position 是"游标"概念,对网络流没意义
CanTimeout = true 时:
- ReadTimeout / WriteTimeout 控制读写超时(毫秒)
- 实际效果因实现而异(FileStream 在 Windows 上有 native 重叠 IO)
|
1.3 一个 Stream 可以"什么都不支持"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class NullStream : Stream // 实际叫 Stream.Null
{
public override bool CanRead => true;
public override bool CanWrite => true;
public override bool CanSeek => true;
public override long Length => 0;
public override long Position { get; set; }
public override int Read(byte[] buf, int off, int count) => 0; // EOF
public override void Write(byte[] buf, int off, int count) { /* 丢弃 */ }
public override long Seek(long off, SeekOrigin org) => off;
public override void Flush() { }
public override void SetLength(long v) { }
}
|
Stream.Null 是"黑洞"——读立刻 EOF,写什么都不存。这种"无所不接"的抽象,让 Stream 可以做单元测试替身。
二、最坑的"暗契约":Read 不保证读满
这是新手最容易踩的雷,也是 Stream 抽象的"原罪"。
2.1 反模式:以为一次 Read 能读满
1
2
3
4
5
6
7
| // ❌ 高风险写法
public async Task<byte[]> ReadNAsync(Stream s, int n)
{
byte[] buf = new byte[n];
int read = await s.ReadAsync(buf, 0, n); // ⚠️ read 可能 < n
return buf; // ⚠️ 后半段可能是 0
}
|
1
2
3
4
5
6
7
8
9
10
11
| 为什么 Read 可能返回少于请求?
- FileStream:通常能读满(除非到 EOF)
- NetworkStream:可能 1 字节就返回!
* TCP 是字节流,每次 Read 看到的是"当前缓冲区到达的数据"
* 没有"凑够 N 字节"的概念
* 如果对方写 4 字节后 sleep,下次 Read 就只有 4 字节
- CryptoStream:可能解一帧就返回
- GZipStream:可能解一 chunk 就返回
→ 契约:Read 返回"实际读到的字节数",可能是 0(EOF)到 count
|
2.2 正确做法:循环到 N 或 EOF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public static async Task<int> ReadExactAsync(Stream s, byte[] buf, int offset, int count)
{
int total = 0;
while (total < count)
{
int read = await s.ReadAsync(buf, offset + total, count - total);
if (read == 0) break; // EOF
total += read;
}
return total;
}
// .NET 6+ 已经原生提供:
await s.ReadExactlyAsync(buf); // 读满,否则抛 EndOfStreamException
await s.ReadAtLeastAsync(buf, 100); // 至少 100 字节
|
1
2
3
4
5
6
7
| ReadExactly 的实现:
.NET 6 引入的"长痛不如短痛"API
替代手写循环(手写循环是 .NET 头号反模式之一)
注意:ReadExactly 在网络流上"卡住"直到读满或 EOF
→ 必须配合 CancellationToken
→ 否则恶意对端可以让调用永久阻塞
|
2.3 Read 返回 0 的意义
1
2
3
4
5
6
7
8
| 0 = EOF(流结束)
1~count = 实际读到
异常 = IO 错误
注意:
- NetworkStream 上对端关连接 → Read 返回 0
- 永远不会"Read 返回 0 然后又读到数据"
- 收到 0 后再调 Read 还是 0
|
三、API 演进史:从 byte[] 到 PipeReader
每一代 .NET 都在补 Stream 的洞。
3.1 第一代:同步 byte[] API(.NET 1.0)
1
2
3
| byte[] buf = new byte[1024];
int read = stream.Read(buf, 0, 1024);
stream.Write(buf, 0, read);
|
1
2
3
4
| 问题:
- 同步阻塞 → 高并发下线程爆炸
- byte[] 必须分配 → GC 压力
- offset + count 三参数 → 容易越界
|
3.2 第二代:APM 异步(.NET 1.1+)
1
2
| stream.BeginRead(buf, 0, 1024, callback, state);
// 在 callback 里调 EndRead 拿到字节数
|
1
2
3
4
| 问题:
- 回调地狱
- 状态对象装箱
- 异常处理复杂
|
3.3 第三代:TAP 异步(.NET 4.5)
1
| int read = await stream.ReadAsync(buf, 0, 1024);
|
1
2
3
| 问题:
- 仍然 byte[] 分配
- Task<int> 装箱(每个 await 一个 Task 对象)
|
3.4 第四代:Span / Memory(.NET Core 2.1+)
1
2
3
4
5
6
7
8
9
| // 同步版本
int read = stream.Read(buf.AsSpan(0, 1024));
// 异步版本(基于 ValueTask)
int read = await stream.ReadAsync(buf.AsMemory(0, 1024));
// stackalloc 友好
Span<byte> stackBuf = stackalloc byte[256];
stream.Read(stackBuf);
|
1
2
3
4
5
6
7
8
| 改进:
- Span 零分配
- ValueTask<int> 避免每次 await 创建 Task
- 可以用 stackalloc
遗留:
- 仍然要"主动拉"(pull)
- 仍然要循环到 N 字节
|
3.5 第五代:ReadExactly / ReadAtLeast(.NET 6+)
1
2
| await stream.ReadExactlyAsync(buf); // 自动循环
await stream.ReadAtLeastAsync(buf, 100, throwOnEndOfStream: false);
|
3.6 第六代:Pipelines(.NET Core 2.1+,命名空间 System.IO.Pipelines)
1
2
3
4
5
6
7
8
9
| PipeReader reader = PipeReader.Create(stream);
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
// 处理 buffer(多段 Memory 拼接)
ProcessProtocol(buffer);
// 告诉 reader "我处理了多少"
reader.AdvanceTo(buffer.Start, buffer.End);
|
1
2
3
4
5
6
7
8
| 革命性:
1. 不用自己分配 buffer(Pipe 内部池化)
2. 不用循环到 N 字节(Pipe 自动缓冲)
3. 多段连续内存(ReadOnlySequence<byte>)
4. 背压(Pipe 满了写不动)
5. 零拷贝(数据停留在 Pipe 池里)
ASP.NET Core / Kestrel / SignalR 全部用 Pipelines
|
四、同步 vs 异步:FileStream 的"假异步"陷阱
这是另一个高频坑。
4.1 默认 FileStream 异步是"假的"
1
2
3
4
| // 看起来是异步
using var fs = new FileStream("data.bin", FileMode.Open);
byte[] buf = new byte[4096];
int read = await fs.ReadAsync(buf, 0, 4096);
|
1
2
3
4
5
6
7
8
9
| 实际行为:
- 默认构造的 FileStream,ReadAsync 是"假异步"
- 内部用一个线程池线程做同步 Read
- 异步调用反而多了线程切换开销
为什么?
- Windows 文件 IO 默认是同步的
- 要真正异步需要 FILE_FLAG_OVERLAPPED + 完成端口
- .NET 默认不开(兼容性、性能权衡)
|
4.2 真异步:FileOptions.Asynchronous
1
2
3
4
5
6
7
8
9
10
11
12
| // ✅ 真异步
using var fs = new FileStream(
"data.bin",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
options: FileOptions.Asynchronous // ← 关键
);
int read = await fs.ReadAsync(buf, 0, 4096);
// 这才是真正的异步(Windows 用 IOCP,Linux 用 io_uring)
|
1
2
3
4
5
6
7
8
9
10
11
| FileOptions.Asynchronous 的效果:
- Windows:内部用 OVERLAPPED + IOCP
- Linux:内部用 io_uring(.NET 6+)
- 不占用线程池线程
- 大量并发 IO 时差距巨大
FileSteam 静态方法:
File.OpenRead("...") // 没开 Asynchronous
File.Open("...", FileMode) // 没开
→ 想要真异步,必须用完整 FileStream 构造
|
4.3 性能对比
1
2
3
4
5
6
7
8
9
10
11
12
13
| 场景:1 个进程同时读 1000 个文件,每文件 1MB
默认(假异步):
1000 个线程池线程阻塞
CPU 跑满,实际 IO 不快
→ 12 秒
FileOptions.Asynchronous(真异步):
0 个线程池线程阻塞
IOCP / io_uring 工作
→ 2 秒
→ 6 倍差距,这只是开了个开关
|
4.4 同步 ReadAsync 反而慢
1
2
3
4
5
6
7
8
9
10
11
| // 真异步下,单线程顺序读:
await fs.ReadAsync(buf);
await fs.ReadAsync(buf);
await fs.ReadAsync(buf);
// 比同步:
fs.Read(buf);
fs.Read(buf);
fs.Read(buf);
// 慢一点(每次有 IOCP 唤醒开销)
// 但高并发下,真异步完胜
|
五、装饰器模式:Stream 的灵魂
Stream 是 GoF《设计模式》里 Decorator(装饰器)的教科书案例。
5.1 经典装饰器组合
1
2
3
4
5
6
7
8
9
10
| // 三层包装:加密 + 压缩 + 文件
using var fs = new FileStream("data.gz.enc", FileMode.Create);
using var gz = new GZipStream(fs, CompressionMode.Compress);
using var cs = new CryptoStream(gz, aes.CreateEncryptor(), CryptoStreamMode.Write);
byte[] data = Encoding.UTF8.GetBytes("Hello, World!");
cs.Write(data, 0, data.Length);
// 数据流:
// Write → cs 加密 → gz 压缩 → fs 落盘
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 图示:
┌─────────────────────┐
│ CryptoStream │
│ - 加密 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ GZipStream │
│ - 压缩 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ BufferedStream │
│ - 缓冲 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ FileStream │
│ - 落盘 │
└─────────────────────┘
|
5.2 装饰器的"自动 dispose 链"
1
2
3
4
5
6
7
| using var fs = new FileStream(...);
using var gz = new GZipStream(fs, ...);
// gz.Dispose() 内部会调用 fs.Dispose()
// 所以只 using 外层就够
// 但很多人不知道这点
// 容易在 finally 里手动 dispose 所有层
|
5.3 装饰器的所有权陷阱
1
2
3
4
5
6
7
8
9
10
11
12
| // 假设你有一个"共享底层流"
using var fs = new FileStream(...);
// 包装两个装饰器,都接管 fs
using var cs1 = new CryptoStream(fs, enc1, Write);
using var cs2 = new CryptoStream(fs, enc2, Write);
cs1.Write(...); // 内部调 fs.Write
cs2.Write(...); // 也调 fs.Write!
// 实际写入是混乱的(fs 是有状态的)
// 正确做法:每个底层流只对应一个装饰器链
|
5.4 BufferedStream:什么时候有用
1
2
3
4
5
6
7
8
9
| using var fs = new FileStream(...);
using var bs = new BufferedStream(fs, 8192); // 8KB 缓冲
// 现代 FileStream 默认就有 bufferSize
// NetworkStream 默认 64KB Socket 缓冲
// → BufferedStream 在大多数情况下是冗余的
// 但 CryptoStream / GZipStream 没有 buffer
// → 包在它们外面有用
|
六、CopyTo / CopyToAsync:被低估的 API
6.1 自实现 vs 内置
1
2
3
4
5
6
7
8
9
10
11
| // ❌ 手写循环
public async Task CopyAsync(Stream src, Stream dst)
{
byte[] buf = new byte[8192];
int read;
while ((read = await src.ReadAsync(buf, 0, 8192)) > 0)
await dst.WriteAsync(buf, 0, read);
}
// ✅ 内置(已经做了优化)
await src.CopyToAsync(dst, bufferSize: 8192);
|
6.2 CopyTo 的实现细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 简化版
public virtual async Task CopyToAsync(Stream dest, int bufferSize, CancellationToken ct)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int read;
while ((read = await ReadAsync(buffer, ct)) > 0)
await dest.WriteAsync(buffer.AsMemory(0, read), ct);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
|
1
2
3
4
5
| 内置的优势:
- 用 ArrayPool(零分配)
- 自适应 bufferSize
- Stream 重写时有优化路径(如 MemoryStream → MemoryStream 直接拷贝)
- 取消支持
|
6.3 FileStream → FileStream 的最优写法
1
2
3
4
5
6
7
8
9
| // ❌ 慢
await src.CopyToAsync(dst);
// ✅ 快(用底层 Windows API CopyFile)
if (src is FileStream fs1 && dst is FileStream fs2)
{
// .NET Core 内部已经针对 FileStream 优化
// 直接调 Windows CopyFileEx
}
|
七、Pipelines:Stream 的接班人
7.1 为什么需要 Pipelines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Stream 抽象的根本问题:
1. 应用必须分配 buffer(byte[])
2. 应用必须循环 Read
3. 应用必须处理"读不满"的情况
4. 没有背压(写太快时 Stream 不阻塞)
5. 单段 buffer(数据可能跨段)
→ 写一个高性能 HTTP 服务器,光是处理"读满 N 字节"就要 100 行代码
Pipelines 重新设计:
1. Pipe 内部管理 buffer(池化)
2. Pipe 自动累积数据
3. 调用方一次拿到"已经够多"的数据
4. Pipe 满了写不动(背压)
5. ReadOnlySequence<byte> 支持多段
|
7.2 PipeReader / PipeWriter
1
2
3
4
5
6
7
8
9
| var pipe = new Pipe(new PipeOptions(
pool: ArrayPool<byte>.Shared,
readerScheduler: PipeScheduler.ThreadPool,
writerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: 1024 * 1024, // 写暂停阈值(背压)
resumeWriterThreshold: 512 * 1024)); // 写恢复阈值
PipeWriter writer = pipe.Writer;
PipeReader reader = pipe.Reader;
|
7.3 协议解析示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 解析"4 字节长度 + N 字节 body"协议
async Task ProcessAsync(PipeReader reader)
{
while (true)
{
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
while (TryReadMessage(ref buffer, out var message))
{
ProcessMessage(message);
}
// 告诉 reader:消费到哪,下次从哪开始
reader.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted) break;
}
}
bool TryReadMessage(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> message)
{
if (buffer.Length < 4) { message = default; return false; }
Span<byte> lengthBytes = stackalloc byte[4];
buffer.Slice(0, 4).CopyTo(lengthBytes);
int length = BitConverter.ToInt32(lengthBytes);
if (buffer.Length < 4 + length) { message = default; return false; }
message = buffer.Slice(4, length);
buffer = buffer.Slice(4 + length);
return true;
}
|
1
2
3
4
5
| 对比 Stream 版本:
- 没有手动 buffer 分配
- 没有手动"读满 4 字节"循环
- 处理"半个消息"自动等下次数据
- 多段 Sequence 天然支持
|
7.4 Stream 与 Pipe 的桥接
1
2
3
4
5
6
7
8
9
10
11
| // 从 Stream 创建 PipeReader
PipeReader reader = PipeReader.Create(stream, new StreamPipeReaderOptions(
bufferSize: 4096,
leaveOpen: false));
// 从 Stream 创建 PipeWriter
PipeWriter writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(
leaveOpen: false));
// 或者反过来:把 Pipe 当 Stream 用
Stream stream = pipe.Writer.AsStream();
|
1
2
3
4
5
| ASP.NET Core 的请求体就是 PipeReader:
HttpContext.Request.BodyReader : PipeReader
HttpContext.Response.BodyWriter : PipeWriter
→ 不再是 Stream,而是 Pipeline
|
八、常见误用清单
8.1 不 Dispose 流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // ❌ 流泄漏
public byte[] Read(string path)
{
var fs = new FileStream(path, FileMode.Open);
var ms = new MemoryStream();
fs.CopyTo(ms);
return ms.ToArray(); // fs 和 ms 都没 Dispose
}
// ✅ using
public byte[] Read(string path)
{
using var fs = new FileStream(path, FileMode.Open);
using var ms = new MemoryStream();
fs.CopyTo(ms);
return ms.ToArray();
}
|
8.2 异步方法不 await
1
2
3
4
5
6
7
8
9
10
11
12
| // ❌ 火并忘记(fire and forget)
public void BadWrite(Stream s, byte[] data)
{
s.WriteAsync(data, 0, data.Length); // ⚠️ 没 await
// 方法立即返回,写还没完成
}
// ✅ await 或显式处理
public async Task WriteAsync(Stream s, byte[] data)
{
await s.WriteAsync(data);
}
|
8.3 不开 Asynchronous
1
2
3
4
5
6
7
8
| // ❌ 假异步
using var fs = File.OpenRead("data.bin"); // 默认 options
await fs.ReadAsync(buf); // 实际是线程池阻塞
// ✅ 真异步
using var fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read,
FileShare.Read, 4096, FileOptions.Asynchronous);
await fs.ReadAsync(buf);
|
8.4 不用 ArrayPool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // ❌ 每次 new
public async Task<byte[]> ReadAsync(Stream s)
{
byte[] buf = new byte[4096]; // ⚠️ GC 压力
using var ms = new MemoryStream();
int read;
while ((read = await s.ReadAsync(buf)) > 0)
ms.Write(buf, 0, read);
return ms.ToArray();
}
// ✅ ArrayPool
public async Task<byte[]> ReadAsync(Stream s)
{
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
try
{
using var ms = new MemoryStream();
int read;
while ((read = await s.ReadAsync(buf.AsMemory(0, 4096))) > 0)
ms.Write(buf, 0, read);
return ms.ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
// ✅✅ CopyToAsync(最简)
public async Task<byte[]> ReadAsync(Stream s)
{
using var ms = new MemoryStream();
await s.CopyToAsync(ms);
return ms.ToArray();
}
|
8.5 信任 CanSeek
1
2
3
4
5
6
7
8
9
| // ❌ 假设 stream 可 Seek
stream.Position = 0;
stream.Read(...);
// ✅ 检查
if (stream.CanSeek)
stream.Position = 0;
else
throw new InvalidOperationException("Stream is not seekable");
|
8.6 CryptoStream 不 FlushFinalBlock
1
2
3
4
5
6
7
8
9
10
| // ❌ 加密数据不完整
using var cs = new CryptoStream(ms, enc, CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
// 没有 cs.FlushFinalBlock() 或 Dispose → AES 填充没写
// ✅ Dispose 自动 flush
using (var cs = new CryptoStream(ms, enc, CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
} // ← Dispose 触发 FlushFinalBlock
|
九、性能基准
9.1 Read 模式对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| [MemoryDiagnoser]
public class StreamBench
{
private MemoryStream _stream;
[GlobalSetup]
public void Setup()
{
var data = new byte[1_000_000];
new Random().NextBytes(data);
_stream = new MemoryStream(data);
}
[Benchmark]
public int ReadAll_Array()
{
_stream.Position = 0;
byte[] buf = new byte[4096];
int total = 0;
int read;
while ((read = _stream.Read(buf, 0, buf.Length)) > 0) total += read;
return total;
}
[Benchmark]
public async Task<int> ReadAllAsync_Array()
{
_stream.Position = 0;
byte[] buf = new byte[4096];
int total = 0;
int read;
while ((read = await _stream.ReadAsync(buf, 0, buf.Length)) > 0) total += read;
return total;
}
[Benchmark]
public async Task<int> ReadAllAsync_Pooled()
{
_stream.Position = 0;
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
try
{
int total = 0;
int read;
while ((read = await _stream.ReadAsync(buf.AsMemory(0, 4096))) > 0) total += read;
return total;
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
[Benchmark]
public async Task<int> ReadAll_CopyToAsync()
{
_stream.Position = 0;
using var ms = new MemoryStream();
await _stream.CopyToAsync(ms, 4096);
return (int)ms.Length;
}
}
|
1
2
3
4
5
6
7
8
9
10
| 典型结果(.NET 8):
ReadAll_Array : ~280 μs, 4096 B allocated
ReadAllAsync_Array : ~320 μs, 5 KB allocated
ReadAllAsync_Pooled : ~300 μs, 0 B allocated ← 推荐
ReadAll_CopyToAsync : ~280 μs, 0 B allocated ← 最简洁
观察:
- CopyToAsync 性能与最优手写持平
- ArrayPool 版几乎零分配
- 普通 byte[] 分配每秒万次 → GC 噪音
|
十、Stream 子类速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| 按用途分类:
文件:
FileStream - 文件 IO(默认 buffered)
FileStream(FileOptions.Asynchronous) - 真异步
UnmanagedMemoryStream - 内存映射文件 / 非托管内存
网络:
NetworkStream - Socket 包装
SslStream - SSL/TLS 加密层
AuthenticatedStream - 基类,可继承做认证
内存:
MemoryStream - byte[] 包装
ReadOnlyMemoryStream - 不可变切片
RecyclableMemoryStream - Microsoft.IO.RecyclableMemoryStream 库,池化版
管道:
AnonymousPipeServerStream / ClientStream - 父子进程
NamedPipeServerStream / ClientStream - 命名管道
装饰器:
BufferedStream - 缓冲
CryptoStream - 加密
GZipStream / DeflateStream / BrotliStream - 压缩
其他:
Stream.Null - 黑洞
IsolatedStorageFileStream - 沙盒存储
|
十一、选型决策
11.1 读文件
1
2
3
4
5
6
7
8
9
10
11
12
| 默认:
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read, 4096, FileOptions.Asynchronous);
→ 读大文件 / 高并发必开 Asynchronous
→ 简单一次性读取:File.ReadAllBytesAsync(内部就是上面的封装)
需要 seek:
✅ FileStream 默认支持
需要内存映射:
MemoryMappedFile.CreateFromFile(...)
|
11.2 写文件
1
2
3
4
5
6
7
8
9
| 默认:
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write,
FileShare.None, 4096, FileOptions.Asynchronous);
→ 加 BufferedStream 收益不大(FileStream 已有 buffer)
→ FileOptions.WriteThrough 用于关键数据(绕过 OS 缓存,直接落盘)
追加:
File.AppendAllTextAsync / AppendAllBytesAsync
|
11.3 网络 IO
1
2
3
4
5
6
7
| 服务器:
Socket → NetworkStream → 用 PipeReader 包装
→ 高性能场景直接用 Pipelines API
客户端:
HttpClient(已经用 Pipelines 内部)
→ 不要手动 new NetworkStream
|
11.4 压缩 + 加密
1
2
3
| 写:CryptoStream → GZipStream → FileStream
读:FileStream → GZipStream → CryptoStream
注意 dispose 顺序(外层先 dispose,自动级联)
|
十二、小结
本文讲了 Stream 抽象从 .NET 1.0 到 9 的演进:
- Stream 的"统一抽象"雄心:用基类覆盖所有 IO,代价是 CanXxx 标志遍布
- 最大的暗契约:Read 不保证读满,循环到 N 字节是基本功
- API 六代演进:同步 → APM → TAP → Span/Memory → ReadExactly → Pipelines
- FileStream 的"假异步"陷阱:必须显式开 FileOptions.Asynchronous
- 装饰器模式:GZipStream(CryptoStream(FileStream)) 的级联与 dispose
- CopyToAsync 内部已经用 ArrayPool + Span 优化,优先用
- Pipelines 是 Stream 的"接班人":池化 buffer + 背压 + 多段 Sequence
- ASP.NET Core 内部已经从 Stream 迁移到 PipeReader/Writer
- 6 类常见误用:不 Dispose / 不 await / 不开 Async / 不用 Pool / 信任 CanSeek / 不 Flush
- 子类速查与选型决策
1
2
3
4
| 记住三句话:
1. Stream 是"拉"模式 + "同步"基础,Pipelines 是"推"模式 + 背压原生
2. FileStream 不开 Asynchronous 等于自废武功
3. Stream.Read 返回值永远不能假设"读满"——这是抽象的代价
|
下次写文件/网络代码时,先问自己:要不要直接用 Pipelines?不需要的话至少开 FileOptions.Asynchronous + ArrayPool + CopyToAsync。性能立刻上一个台阶。