-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathP2PBinaryFrame.cs
More file actions
166 lines (145 loc) · 7.08 KB
/
Copy pathP2PBinaryFrame.cs
File metadata and controls
166 lines (145 loc) · 7.08 KB
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using System.Buffers.Binary;
using System.Text;
namespace SpawnDev.ILGPU.P2P;
/// <summary>
/// Binary wire framing for <see cref="P2PMessageType.BufferSend"/> and
/// <see cref="P2PMessageType.BufferData"/> tensor chunks.
///
/// Before rc.14 every buffer chunk travelled over the WebRTC data channel inside
/// a JSON envelope - <see cref="BufferChunk.Data"/> (byte[]) was base64-encoded by
/// System.Text.Json and then the whole thing was JSON-serialized again into the
/// outer <see cref="P2PMessage"/>. WebRTC data channels are natively binary
/// (SIPSorcery PPID 53 `WebRTC_Binary` + browser RTCDataChannel with byte[] sends),
/// so the base64 expansion and double JSON pass were pure waste per Rule #4
/// "Performance Is the Mission."
///
/// This framing sends chunk metadata as a fixed-size big-endian header followed
/// by the raw tensor bytes with no encoding. Every multi-byte integer read/write
/// goes through <see cref="BinaryPrimitives"/>, matching the style of
/// SpawnDev.WebTorrent's <c>Wire._message</c>.
///
/// Wire layout:
/// <code>
/// offset size field
/// 0 1 msgType (0x02 BufferSend / 0x03 BufferData)
/// 1 2 bufferIdLen uint16, big-endian, utf-8 byte count
/// 3 N bufferId utf-8 bytes
/// 3+N 4 chunkIndex int32, big-endian
/// 7+N 4 totalChunks int32, big-endian
/// 11+N 4 totalBytes int32, big-endian (matches BufferChunk.TotalBytes schema)
/// 15+N * data raw chunk bytes (length implicit: frame length - header)
/// </code>
///
/// The 1-byte discriminator is safe alongside JSON: System.Text.Json output always
/// starts with <c>{</c> (0x7B) for an object, so bytes 0x00-0x1F are reserved for
/// binary framing. The receive side peeks <c>data[0]</c> to route.
/// </summary>
public static class P2PBinaryFrame
{
/// <summary>
/// Size of the fixed header (everything except bufferId and data).
/// 1 (type) + 2 (bufferIdLen) + 4 (chunkIndex) + 4 (totalChunks) + 4 (totalBytes) = 15 bytes.
/// </summary>
public const int FixedHeaderSize = 1 + 2 + 4 + 4 + 4;
/// <summary>
/// Absolute maximum bufferId length in utf-8 bytes. Keeps bufferIdLen sane and bounded
/// so a malformed header cannot point past the end of a short frame.
/// </summary>
public const int MaxBufferIdBytes = 1024;
/// <summary>
/// Absolute wire-message cap. SIPSorcery's <c>RTCDataChannel.send</c> throws
/// <c>ApplicationException</c> above 262,144 bytes (SCTP_DEFAULT_MAX_MESSAGE_SIZE).
/// </summary>
public const int MaxFrameSize = 256 * 1024;
/// <summary>
/// Encode a BufferSend (0x02) or BufferData (0x03) chunk to a binary wire frame.
/// Throws if the frame would exceed <see cref="MaxFrameSize"/> (caller should chunk smaller).
/// </summary>
public static byte[] EncodeBufferChunk(P2PMessageType type, BufferChunk chunk)
{
if (type != P2PMessageType.BufferSend && type != P2PMessageType.BufferData)
throw new ArgumentException(
$"Binary framing only supports BufferSend / BufferData, got {type}", nameof(type));
ArgumentNullException.ThrowIfNull(chunk);
ArgumentNullException.ThrowIfNull(chunk.Data);
var bufferIdBytes = Encoding.UTF8.GetBytes(chunk.BufferId ?? string.Empty);
if (bufferIdBytes.Length > MaxBufferIdBytes)
throw new ArgumentException(
$"bufferId utf-8 length {bufferIdBytes.Length} exceeds MaxBufferIdBytes {MaxBufferIdBytes}",
nameof(chunk));
var frameSize = FixedHeaderSize + bufferIdBytes.Length + chunk.Data.Length;
if (frameSize > MaxFrameSize)
throw new ArgumentException(
$"Frame size {frameSize} exceeds MaxFrameSize {MaxFrameSize}; reduce MaxChunkSize.",
nameof(chunk));
var frame = new byte[frameSize];
var span = frame.AsSpan();
span[0] = (byte)(type == P2PMessageType.BufferSend ? 0x02 : 0x03);
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(1, 2), (ushort)bufferIdBytes.Length);
bufferIdBytes.CopyTo(span.Slice(3, bufferIdBytes.Length));
var cursor = 3 + bufferIdBytes.Length;
BinaryPrimitives.WriteInt32BigEndian(span.Slice(cursor, 4), chunk.ChunkIndex);
cursor += 4;
BinaryPrimitives.WriteInt32BigEndian(span.Slice(cursor, 4), chunk.TotalChunks);
cursor += 4;
BinaryPrimitives.WriteInt32BigEndian(span.Slice(cursor, 4), chunk.TotalBytes);
cursor += 4;
chunk.Data.CopyTo(span.Slice(cursor, chunk.Data.Length));
return frame;
}
/// <summary>
/// Attempt to decode a binary buffer-chunk frame. Returns false for malformed or
/// non-binary frames (never throws on bad input so the wire reader can cheaply
/// discard and move on).
/// </summary>
public static bool TryDecodeBufferChunk(byte[] data, out BufferChunk chunk, out P2PMessageType type)
{
chunk = default!;
type = default;
if (data == null || data.Length < FixedHeaderSize) return false;
var span = data.AsSpan();
var typeByte = span[0];
if (typeByte == 0x02) type = P2PMessageType.BufferSend;
else if (typeByte == 0x03) type = P2PMessageType.BufferData;
else return false;
int bufferIdLen = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(1, 2));
if (bufferIdLen > MaxBufferIdBytes) return false;
// Header fully in bounds?
var headerEnd = FixedHeaderSize + bufferIdLen;
if (headerEnd > span.Length) return false;
string bufferId;
try { bufferId = Encoding.UTF8.GetString(span.Slice(3, bufferIdLen)); }
catch { return false; }
var cursor = 3 + bufferIdLen;
int chunkIndex = BinaryPrimitives.ReadInt32BigEndian(span.Slice(cursor, 4));
cursor += 4;
int totalChunks = BinaryPrimitives.ReadInt32BigEndian(span.Slice(cursor, 4));
cursor += 4;
int totalBytes = BinaryPrimitives.ReadInt32BigEndian(span.Slice(cursor, 4));
cursor += 4;
// Sanity: reassembler in P2PBufferTransfer.ReceiveChunk will re-validate these too,
// but catching here lets us drop obviously-malformed frames without the allocation.
if (chunkIndex < 0) return false;
if (totalChunks < 1) return false;
if (totalBytes < 0) return false;
if (chunkIndex >= totalChunks) return false;
var dataLen = span.Length - cursor;
if (dataLen < 0) return false;
var chunkData = span.Slice(cursor, dataLen).ToArray();
chunk = new BufferChunk
{
BufferId = bufferId,
ChunkIndex = chunkIndex,
TotalChunks = totalChunks,
TotalBytes = totalBytes,
Data = chunkData,
};
return true;
}
/// <summary>
/// Fast non-allocating check: is this a binary buffer-chunk frame?
/// Used by the receive-side dispatcher before deciding to JSON-parse.
/// </summary>
public static bool IsBinaryFrame(byte[] data)
=> data != null && data.Length > 0 && (data[0] == 0x02 || data[0] == 0x03);
}