perf(net): borrow inbound packet storage
Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
This commit is contained in:
parent
7211bb1bf7
commit
e928c5dd02
18 changed files with 1329 additions and 66 deletions
74
src/AcDream.Core.Net/Packets/BorrowedPacket.cs
Normal file
74
src/AcDream.Core.Net/Packets/BorrowedPacket.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
namespace AcDream.Core.Net.Packets;
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free decoded packet view. Every memory member borrows the
|
||||
/// caller's datagram and is valid only while that owner retains the buffer.
|
||||
/// </summary>
|
||||
internal readonly record struct BorrowedPacket(
|
||||
PacketHeader Header,
|
||||
BorrowedOptionalHeader Optional,
|
||||
ReadOnlyMemory<byte> Body,
|
||||
ReadOnlyMemory<byte> FragmentBytes,
|
||||
int FragmentCount)
|
||||
{
|
||||
public BorrowedFragmentEnumerable Fragments =>
|
||||
new(FragmentBytes);
|
||||
}
|
||||
|
||||
internal readonly record struct BorrowedOptionalHeader(
|
||||
uint AckSequence,
|
||||
double TimeSync,
|
||||
float EchoRequestClientTime,
|
||||
uint FlowBytes,
|
||||
ushort FlowInterval,
|
||||
double ConnectRequestServerTime,
|
||||
ulong ConnectRequestCookie,
|
||||
uint ConnectRequestClientId,
|
||||
uint ConnectRequestServerSeed,
|
||||
uint ConnectRequestClientSeed,
|
||||
ReadOnlyMemory<byte> RawBytes,
|
||||
ReadOnlyMemory<byte> RetransmitRequestBytes,
|
||||
int RetransmitRequestCount);
|
||||
|
||||
internal readonly record struct BorrowedPacketDecodeResult(
|
||||
BorrowedPacket Packet,
|
||||
PacketCodec.DecodeError Error)
|
||||
{
|
||||
public bool IsOk => Error == PacketCodec.DecodeError.None;
|
||||
}
|
||||
|
||||
internal readonly record struct BorrowedMessageFragment(
|
||||
MessageFragmentHeader Header,
|
||||
ReadOnlyMemory<byte> Payload);
|
||||
|
||||
internal readonly struct BorrowedFragmentEnumerable(
|
||||
ReadOnlyMemory<byte> encoded)
|
||||
{
|
||||
public Enumerator GetEnumerator() => new(encoded);
|
||||
|
||||
internal struct Enumerator(ReadOnlyMemory<byte> remaining)
|
||||
{
|
||||
private ReadOnlyMemory<byte> _remaining = remaining;
|
||||
|
||||
public BorrowedMessageFragment Current { get; private set; }
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_remaining.IsEmpty)
|
||||
return false;
|
||||
|
||||
if (!MessageFragment.TryParseBorrowed(
|
||||
_remaining,
|
||||
out BorrowedMessageFragment fragment,
|
||||
out int consumed))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"validated packet contained an invalid fragment");
|
||||
}
|
||||
|
||||
Current = fragment;
|
||||
_remaining = _remaining.Slice(consumed);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,80 @@ public sealed class FragmentAssembler
|
|||
return combined;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production borrowed-memory path. A complete single-fragment message
|
||||
/// is returned as a view into the current datagram. Multi-fragment
|
||||
/// payloads are copied only because they must survive that datagram's
|
||||
/// pooled lifetime.
|
||||
/// </summary>
|
||||
internal bool TryIngest(
|
||||
in BorrowedMessageFragment fragment,
|
||||
out ReadOnlyMemory<byte> message,
|
||||
out ushort messageQueue)
|
||||
{
|
||||
MessageFragmentHeader header = fragment.Header;
|
||||
message = ReadOnlyMemory<byte>.Empty;
|
||||
messageQueue = 0;
|
||||
|
||||
if (header.Count == 1 && header.Index == 0)
|
||||
{
|
||||
message = fragment.Payload;
|
||||
messageQueue = header.Queue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_inFlight.TryGetValue(
|
||||
header.Sequence,
|
||||
out PartialMessage? partial))
|
||||
{
|
||||
partial = new PartialMessage(
|
||||
header.Count,
|
||||
header.Queue);
|
||||
_inFlight[header.Sequence] = partial;
|
||||
}
|
||||
else if (partial.TotalFragments != header.Count
|
||||
|| partial.Queue != header.Queue)
|
||||
{
|
||||
// Same sequence with conflicting identity is malformed. Preserve
|
||||
// the first accepted partial instead of corrupting its layout.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (partial.Fragments[header.Index] is null)
|
||||
{
|
||||
partial.Fragments[header.Index] =
|
||||
fragment.Payload.ToArray();
|
||||
partial.ReceivedCount++;
|
||||
}
|
||||
|
||||
if (partial.ReceivedCount < partial.TotalFragments)
|
||||
return false;
|
||||
|
||||
int totalBytes = 0;
|
||||
for (int index = 0;
|
||||
index < partial.TotalFragments;
|
||||
index++)
|
||||
{
|
||||
totalBytes += partial.Fragments[index]!.Length;
|
||||
}
|
||||
|
||||
var combined = new byte[totalBytes];
|
||||
int offset = 0;
|
||||
for (int index = 0;
|
||||
index < partial.TotalFragments;
|
||||
index++)
|
||||
{
|
||||
byte[] payload = partial.Fragments[index]!;
|
||||
payload.CopyTo(combined, offset);
|
||||
offset += payload.Length;
|
||||
}
|
||||
|
||||
_inFlight.Remove(header.Sequence);
|
||||
message = combined;
|
||||
messageQueue = partial.Queue;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Discard all in-flight partial messages.</summary>
|
||||
public void DropAll() => _inFlight.Clear();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,25 +20,75 @@ public readonly record struct MessageFragment(MessageFragmentHeader Header, byte
|
|||
/// </summary>
|
||||
public static (MessageFragment? fragment, int consumed) TryParse(ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (source.Length < MessageFragmentHeader.Size)
|
||||
if (!TryParseLayout(
|
||||
source,
|
||||
out MessageFragmentHeader header,
|
||||
out int payloadLength,
|
||||
out int consumed))
|
||||
{
|
||||
return (null, 0);
|
||||
}
|
||||
|
||||
var header = MessageFragmentHeader.Unpack(source);
|
||||
byte[] payload = source
|
||||
.Slice(MessageFragmentHeader.Size, payloadLength)
|
||||
.ToArray();
|
||||
return (new MessageFragment(header, payload), consumed);
|
||||
}
|
||||
|
||||
internal static bool TryParseBorrowed(
|
||||
ReadOnlyMemory<byte> source,
|
||||
out BorrowedMessageFragment fragment,
|
||||
out int consumed)
|
||||
{
|
||||
if (!TryParseLayout(
|
||||
source.Span,
|
||||
out MessageFragmentHeader header,
|
||||
out int payloadLength,
|
||||
out consumed))
|
||||
{
|
||||
fragment = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
fragment = new BorrowedMessageFragment(
|
||||
header,
|
||||
source.Slice(
|
||||
MessageFragmentHeader.Size,
|
||||
payloadLength));
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryParseLayout(
|
||||
ReadOnlySpan<byte> source,
|
||||
out MessageFragmentHeader header,
|
||||
out int payloadLength,
|
||||
out int consumed)
|
||||
{
|
||||
header = default;
|
||||
payloadLength = 0;
|
||||
consumed = 0;
|
||||
if (source.Length < MessageFragmentHeader.Size)
|
||||
return false;
|
||||
|
||||
header = MessageFragmentHeader.Unpack(source);
|
||||
|
||||
// TotalSize is the fragment's own size including its header. Anything
|
||||
// smaller than the header or larger than the max fragment size is
|
||||
// wire corruption and we refuse to parse.
|
||||
if (header.TotalSize < MessageFragmentHeader.Size
|
||||
|| header.TotalSize > MessageFragmentHeader.MaxFragmentSize)
|
||||
|| header.TotalSize > MessageFragmentHeader.MaxFragmentSize
|
||||
|| header.Count == 0
|
||||
|| header.Index >= header.Count)
|
||||
{
|
||||
return (null, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
int payloadLength = header.TotalSize - MessageFragmentHeader.Size;
|
||||
payloadLength =
|
||||
header.TotalSize - MessageFragmentHeader.Size;
|
||||
if (source.Length < header.TotalSize)
|
||||
return (null, 0);
|
||||
return false;
|
||||
|
||||
var payload = source.Slice(MessageFragmentHeader.Size, payloadLength).ToArray();
|
||||
return (new MessageFragment(header, payload), header.TotalSize);
|
||||
consumed = header.TotalSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,373 @@ public static class PacketCodec
|
|||
return new PacketDecodeResult(packet, DecodeError.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode and verify one packet without materializing packet, optional-
|
||||
/// header, fragment-list, body, or fragment-payload objects. Returned
|
||||
/// memory borrows <paramref name="datagram"/> and must not outlive it.
|
||||
/// </summary>
|
||||
internal static BorrowedPacketDecodeResult TryDecodeBorrowed(
|
||||
ReadOnlyMemory<byte> datagram,
|
||||
IsaacRandom? inboundIsaac)
|
||||
{
|
||||
ReadOnlySpan<byte> wire = datagram.Span;
|
||||
if (wire.Length < PacketHeader.Size)
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.TooShort);
|
||||
}
|
||||
|
||||
PacketHeader header = PacketHeader.Unpack(wire);
|
||||
int bodyLength = header.DataSize;
|
||||
if (wire.Length - PacketHeader.Size < bodyLength)
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.HeaderSizeExceedsBuffer);
|
||||
}
|
||||
|
||||
ReadOnlyMemory<byte> body = datagram.Slice(
|
||||
PacketHeader.Size,
|
||||
bodyLength);
|
||||
if (!TryParseBorrowedOptional(
|
||||
body,
|
||||
header.Flags,
|
||||
out BorrowedOptionalHeader optional,
|
||||
out int optionalConsumed))
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.InvalidOptionalHeader);
|
||||
}
|
||||
|
||||
ReadOnlyMemory<byte> fragmentBytes =
|
||||
ReadOnlyMemory<byte>.Empty;
|
||||
int fragmentCount = 0;
|
||||
uint fragmentHash = 0;
|
||||
if (header.HasFlag(PacketHeaderFlags.BlobFragments))
|
||||
{
|
||||
fragmentBytes = body.Slice(optionalConsumed);
|
||||
ReadOnlySpan<byte> remaining = fragmentBytes.Span;
|
||||
while (!remaining.IsEmpty)
|
||||
{
|
||||
if (!MessageFragment.TryParseLayout(
|
||||
remaining,
|
||||
out _,
|
||||
out int payloadLength,
|
||||
out int consumed))
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.InvalidFragment);
|
||||
}
|
||||
|
||||
fragmentHash +=
|
||||
Hash32.Calculate(
|
||||
remaining.Slice(
|
||||
0,
|
||||
MessageFragmentHeader.Size))
|
||||
+ Hash32.Calculate(
|
||||
remaining.Slice(
|
||||
MessageFragmentHeader.Size,
|
||||
payloadLength));
|
||||
fragmentCount++;
|
||||
remaining = remaining.Slice(consumed);
|
||||
}
|
||||
}
|
||||
|
||||
uint headerHash = header.CalculateHeaderHash32();
|
||||
uint optionalHash = Hash32.Calculate(
|
||||
optional.RawBytes.Span);
|
||||
uint payloadHash = optionalHash + fragmentHash;
|
||||
if (header.HasFlag(
|
||||
PacketHeaderFlags.EncryptedChecksum))
|
||||
{
|
||||
if (inboundIsaac is null)
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.ChecksumMismatch);
|
||||
}
|
||||
|
||||
uint expectedKey =
|
||||
(header.Checksum - headerHash) ^ payloadHash;
|
||||
uint isaacKey = inboundIsaac.Next();
|
||||
if (expectedKey != isaacKey)
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.ChecksumMismatch);
|
||||
}
|
||||
}
|
||||
else if (header.Checksum != headerHash + payloadHash)
|
||||
{
|
||||
return new BorrowedPacketDecodeResult(
|
||||
default,
|
||||
DecodeError.ChecksumMismatch);
|
||||
}
|
||||
|
||||
return new BorrowedPacketDecodeResult(
|
||||
new BorrowedPacket(
|
||||
header,
|
||||
optional,
|
||||
body,
|
||||
fragmentBytes,
|
||||
fragmentCount),
|
||||
DecodeError.None);
|
||||
}
|
||||
|
||||
private static bool TryParseBorrowedOptional(
|
||||
ReadOnlyMemory<byte> bodyMemory,
|
||||
PacketHeaderFlags flags,
|
||||
out BorrowedOptionalHeader optional,
|
||||
out int consumed)
|
||||
{
|
||||
ReadOnlySpan<byte> body = bodyMemory.Span;
|
||||
int position = 0;
|
||||
uint ackSequence = 0;
|
||||
double timeSync = 0;
|
||||
float echoRequestClientTime = 0;
|
||||
uint flowBytes = 0;
|
||||
ushort flowInterval = 0;
|
||||
double connectRequestServerTime = 0;
|
||||
ulong connectRequestCookie = 0;
|
||||
uint connectRequestClientId = 0;
|
||||
uint connectRequestServerSeed = 0;
|
||||
uint connectRequestClientSeed = 0;
|
||||
int retransmitOffset = 0;
|
||||
int retransmitCount = 0;
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.ServerSwitch)
|
||||
&& !Take(body, ref position, 8))
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.RequestRetransmit))
|
||||
{
|
||||
if (!Take(body, ref position, 4))
|
||||
return Invalid(out optional, out consumed);
|
||||
uint count = System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position - 4));
|
||||
if (count > 1024
|
||||
|| body.Length - position < (int)count * 4)
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
|
||||
retransmitOffset = position;
|
||||
retransmitCount = checked((int)count);
|
||||
position += retransmitCount * 4;
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.RejectRetransmit))
|
||||
{
|
||||
if (!Take(body, ref position, 4))
|
||||
return Invalid(out optional, out consumed);
|
||||
uint count = System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position - 4));
|
||||
if (count > 1024
|
||||
|| body.Length - position < (int)count * 4)
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
position += checked((int)count * 4);
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.AckSequence))
|
||||
{
|
||||
if (!Take(body, ref position, 4))
|
||||
return Invalid(out optional, out consumed);
|
||||
ackSequence =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position - 4));
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.LoginRequest))
|
||||
{
|
||||
position = body.Length;
|
||||
optional = BuildOptional(
|
||||
bodyMemory,
|
||||
position,
|
||||
ackSequence,
|
||||
timeSync,
|
||||
echoRequestClientTime,
|
||||
flowBytes,
|
||||
flowInterval,
|
||||
connectRequestServerTime,
|
||||
connectRequestCookie,
|
||||
connectRequestClientId,
|
||||
connectRequestServerSeed,
|
||||
connectRequestClientSeed,
|
||||
retransmitOffset,
|
||||
retransmitCount);
|
||||
consumed = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.WorldLoginRequest)
|
||||
&& !Take(body, ref position, 8))
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.ConnectRequest))
|
||||
{
|
||||
if (body.Length - position < 32)
|
||||
return Invalid(out optional, out consumed);
|
||||
|
||||
connectRequestServerTime =
|
||||
BitConverter.Int64BitsToDouble(
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadInt64LittleEndian(
|
||||
body.Slice(position)));
|
||||
connectRequestCookie =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt64LittleEndian(
|
||||
body.Slice(position + 8));
|
||||
connectRequestClientId =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position + 16));
|
||||
connectRequestServerSeed =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position + 20));
|
||||
connectRequestClientSeed =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position + 24));
|
||||
position += 32;
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.ConnectResponse)
|
||||
&& !Take(body, ref position, 8))
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.CICMDCommand)
|
||||
&& !Take(body, ref position, 8))
|
||||
{
|
||||
return Invalid(out optional, out consumed);
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.TimeSync))
|
||||
{
|
||||
if (!Take(body, ref position, 8))
|
||||
return Invalid(out optional, out consumed);
|
||||
timeSync = BitConverter.Int64BitsToDouble(
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadInt64LittleEndian(
|
||||
body.Slice(position - 8)));
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.EchoRequest))
|
||||
{
|
||||
if (!Take(body, ref position, 4))
|
||||
return Invalid(out optional, out consumed);
|
||||
echoRequestClientTime =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadSingleLittleEndian(
|
||||
body.Slice(position - 4));
|
||||
}
|
||||
|
||||
if (HasFlag(flags, PacketHeaderFlags.Flow))
|
||||
{
|
||||
if (!Take(body, ref position, 6))
|
||||
return Invalid(out optional, out consumed);
|
||||
flowBytes =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(
|
||||
body.Slice(position - 6));
|
||||
flowInterval =
|
||||
System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt16LittleEndian(
|
||||
body.Slice(position - 2));
|
||||
}
|
||||
|
||||
optional = BuildOptional(
|
||||
bodyMemory,
|
||||
position,
|
||||
ackSequence,
|
||||
timeSync,
|
||||
echoRequestClientTime,
|
||||
flowBytes,
|
||||
flowInterval,
|
||||
connectRequestServerTime,
|
||||
connectRequestCookie,
|
||||
connectRequestClientId,
|
||||
connectRequestServerSeed,
|
||||
connectRequestClientSeed,
|
||||
retransmitOffset,
|
||||
retransmitCount);
|
||||
consumed = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BorrowedOptionalHeader BuildOptional(
|
||||
ReadOnlyMemory<byte> body,
|
||||
int consumed,
|
||||
uint ackSequence,
|
||||
double timeSync,
|
||||
float echoRequestClientTime,
|
||||
uint flowBytes,
|
||||
ushort flowInterval,
|
||||
double connectRequestServerTime,
|
||||
ulong connectRequestCookie,
|
||||
uint connectRequestClientId,
|
||||
uint connectRequestServerSeed,
|
||||
uint connectRequestClientSeed,
|
||||
int retransmitOffset,
|
||||
int retransmitCount) =>
|
||||
new(
|
||||
ackSequence,
|
||||
timeSync,
|
||||
echoRequestClientTime,
|
||||
flowBytes,
|
||||
flowInterval,
|
||||
connectRequestServerTime,
|
||||
connectRequestCookie,
|
||||
connectRequestClientId,
|
||||
connectRequestServerSeed,
|
||||
connectRequestClientSeed,
|
||||
body.Slice(0, consumed),
|
||||
retransmitCount == 0
|
||||
? ReadOnlyMemory<byte>.Empty
|
||||
: body.Slice(
|
||||
retransmitOffset,
|
||||
retransmitCount * 4),
|
||||
retransmitCount);
|
||||
|
||||
private static bool Invalid(
|
||||
out BorrowedOptionalHeader optional,
|
||||
out int consumed)
|
||||
{
|
||||
optional = default;
|
||||
consumed = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasFlag(
|
||||
PacketHeaderFlags all,
|
||||
PacketHeaderFlags bit) =>
|
||||
(all & bit) != 0;
|
||||
|
||||
private static bool Take(
|
||||
ReadOnlySpan<byte> body,
|
||||
ref int position,
|
||||
int count)
|
||||
{
|
||||
if (body.Length - position < count)
|
||||
return false;
|
||||
position += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assemble a datagram from a header and an optional-section body.
|
||||
/// Computes the checksum (both unencrypted and ISAAC-encrypted forms)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue