System.IO.Pipelines 与“零拷贝”:在 .NET 打造高吞吐二进制 RPC 🚀
目录
0. TL;DR —— 为什么选 Pipelines 🎯
PipeReader.ReadAsync()返回ReadOnlySequence<byte>,天然支持跨段缓冲与半帧,搭配AdvanceTo(consumed, examined)实现背压。SequenceReader<byte>可以在不拷贝到托管数组的情况下解析协议字段;BinaryPrimitives操作Span/ReadOnlySpan更高效。- Kestrel 暴露
BodyReader/BodyWriter,HTTP 形态也能享受 Pipes 的收益。 - 配合内存池、写合并、并发限流,能在吞吐、延迟、分配三项上显著优于传统
Stream。
1. 帧协议 📦
大端(网络序)固定头 8 字节:
len:uint32 // 含头,总长度
type:uint16 // 0=Ping, 1=Echo, 2=Sum, 0xFFFF=Error
flags:uint16 // bit0=压缩; 其他保留
payload: len-8
- Ping:空载
- Echo:原样返回 payload(示例中演示第一段
Span回声) - Sum:payload 为 N 个 int32(BE),返回 int32(BE)之和
- Error:返回错误码/消息(演示版为简单文本)
2. 代码公共部分:帧编解码 🧑💻
src/Rpc.Protocol/Frame.cs
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
namespace Rpc.Protocol;
public static class Frame
{
public const int HeaderSize = 8;
public const ushort TypePing = 0;
public const ushort TypeEcho = 1;
public const ushort TypeSum = 2;
public const ushort TypeError = 0xFFFF;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseFrame(
ref ReadOnlySequence<byte> buffer,
out ushort type,
out ushort flags,
out ReadOnlySequence<byte> payload)
{
type = 0; flags = 0; payload = default;
if (buffer.Length < HeaderSize) return false;
Span<byte> header = stackalloc byte[HeaderSize];
buffer.Slice(0, HeaderSize).CopyTo(header);
uint len = BinaryPrimitives.ReadUInt32BigEndian(header);
type = BinaryPrimitives.ReadUInt16BigEndian(header.Slice(4));
flags = BinaryPrimitives.ReadUInt16BigEndian(header.Slice(6));
if (len < HeaderSize) throw new InvalidOperationException("Invalid length");
if (buffer.Length < len) return false; // 半帧
var frame = buffer.Slice(0, len);
payload = frame.Slice(HeaderSize, len - HeaderSize);
buffer = buffer.Slice(len);
return true;
}
public static void WriteFrame(PipeWriter writer, ushort type, ushort flags, ReadOnlySpan<byte> payload)
{
int len = HeaderSize + payload.Length;
Span<byte> span = writer.GetSpan(len);
BinaryPrimitives.WriteUInt32BigEndian(span, (uint)len);
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(4), type);
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(6), flags);
payload.CopyTo(span.Slice(HeaderSize));
writer.Advance(len);
}
}
要点
- 解析时仅拷头部到栈;payload 始终是原始
ReadOnlySequence<byte>的切片(零拷贝)。 - 写入时一次性拿到足够
Span,减少Advance/Flush次数。 len的下限校验防御异常输入。
3. Demo A:TCP + Pipelines 🌐
src/Rpc.TcpServer/Program.cs
using System.Buffers;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading.Channels;
using Rpc.Protocol;
var listener = new TcpListener(IPAddress.Loopback, 5001);
listener.Start();
Console.WriteLine("TCP RPC listening on 127.0.0.1:5001");
while (true)
{
var client = await listener.AcceptTcpClientAsync();
_ = Task.Run(() => Handle(client));
}
static async Task Handle(TcpClient client)
{
const int MaxInFlight = 32;
var workQueue = Channel.CreateBounded<(ushort type, ReadOnlySequence<byte> payload)>(new BoundedChannelOptions(MaxInFlight)
{
SingleReader = true,
SingleWriter = true
});
using var _ = client;
client.NoDelay = true;
var stream = client.GetStream();
var reader = PipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: 64 * 1024));
var networkWriter = PipeWriter.Create(stream, new StreamPipeWriterOptions(MemoryPool<byte>.Shared, 64 * 1024, leaveOpen: true));
var sendPipe = new Pipe(new PipeOptions(
pool: MemoryPool<byte>.Shared,
pauseWriterThreshold: 256 * 1024,
打造高吞吐二进制RPC的.NET实践

最低0.47元/天 解锁文章
313

被折叠的 条评论
为什么被折叠?



