AyCode.Core/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs

121 lines
5.0 KiB
C#

using System;
using System.Buffers;
using AyCode.Core.Serializers.Binaries;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Project-specific binary protocol.
///
/// Overrides the base <see cref="WriteHeader"/>/<see cref="ReadHeader"/> hooks to carry the
/// runtime type of the streamed / last data argument in each message. This is needed because
/// our <c>OnReceiveMessage(int, int?, SignalParams, object)</c> convention has the last argument
/// typed as <c>object</c>, so the binder can't tell the deserializer what concrete type to produce.
///
/// With the header in place, the concrete type travels on the wire and is available before the
/// (non-streamed) data argument is read, and before the streamed argument's Task.Run starts.
/// There is no dependency on reading <c>SignalParams</c> first, so it works regardless of whether
/// <c>SignalParams</c> is inline or streamed.
/// </summary>
public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol
{
/// <summary>
/// Parsed SignalParams from current message (arg[2]).
/// Still used for <see cref="SignalParams.IsRawBytesData"/>, which opts out of deserialization.
/// </summary>
private SignalParams? _currentSignalParams;
public AyCodeBinaryHubProtocol() : this(AcBinarySerializerOptions.Default) { }
public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null) : base(options, protocolMode, logger) { }
#region Header: per-message concrete type of the data argument
/// <summary>
/// Writes the AssemblyQualifiedName of the concrete type of the data argument.
/// <para>
/// When chunked mode is active, <paramref name="streamedArg"/> is the argument being streamed.
/// When non-chunked, we pick the last non-null argument from the message (project convention:
/// <c>OnReceiveMessage(int, int?, SignalParams, object data)</c> — the data arg is last).
/// </para>
/// </summary>
protected override void WriteHeader(ref BufferWriterBinaryOutput bw, HubMessage message, object? streamedArg)
{
var typeSource = streamedArg ?? GetDataArg(message);
var typeName = typeSource?.GetType().AssemblyQualifiedName;
WriteNullableString(ref bw, typeName);
}
/// <summary>
/// Reads the type AQN and resolves it via <see cref="Type.GetType(string)"/>.
/// Returns the resolved <see cref="Type"/> (or null if absent / unresolvable).
/// </summary>
protected override object? ReadHeader(ref SequenceReader<byte> r)
{
var typeName = ReadNullableString(ref r);
return typeName != null ? Type.GetType(typeName) : null;
}
private static object? GetDataArg(HubMessage message) => message switch
{
InvocationMessage m when m.Arguments.Length > 0 => m.Arguments[m.Arguments.Length - 1],
StreamInvocationMessage m when m.Arguments.Length > 0 => m.Arguments[m.Arguments.Length - 1],
StreamItemMessage m => m.Item,
CompletionMessage m => m.HasResult ? m.Result : null,
_ => null
};
#endregion
protected override void OnArgumentRead(object? value, int index)
{
if (value is SignalParams sp)
_currentSignalParams = sp;
}
protected override object? ReadSingleArgument(ref SequenceReader<byte> r, Type targetType)
{
r.TryReadLittleEndian(out int argLength);
if (argLength == 0)
return null;
// AsyncSegment: streamed arg marker (INT32 -1) → placeholder for chunked deserialization
if (argLength == -1)
return StreamedArgPlaceholder;
if (argLength == 1)
{
r.TryPeek(out byte marker);
if (marker == 0) { r.Advance(1); return null; }
}
var argSlice = r.UnreadSequence.Slice(0, argLength);
r.Advance(argLength);
// byte[] fast-path: tag only, no VarUInt (argLength implies size)
var argReader = new SequenceReader<byte>(argSlice);
if (argReader.TryPeek(out byte tag) && tag == BinaryTypeCode.ByteArray)
{
return SequenceToByteArray(argSlice.Slice(1));
}
// IsRawBytesData: return raw bytes, consumer deserializes later
if (_currentSignalParams is { IsRawBytesData: true })
return SequenceToByteArray(argSlice);
// Type resolution: prefer concrete type from the per-message header
if (targetType == typeof(object) && _currentHeaderContext is Type headerType)
targetType = headerType;
// Bytes mode: linearize to byte[] → ArrayBinaryInput (fastest deser, no segment overhead)
if (_protocolMode == BinaryProtocolMode.Bytes)
{
var bytes = SequenceToByteArray(argSlice);
return AcBinaryDeserializer.Deserialize(bytes, targetType, Options);
}
return DeserializeFromSequence(argSlice, targetType, Options);
}
}