121 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|