Avoiding serializing to MemoryStream
This commit is contained in:
parent
0255979a73
commit
046553cfe4
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Text;
|
using System.Buffers.Text;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
{
|
{
|
||||||
|
|
@ -23,18 +22,37 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
return decoded.Slice(0, written);
|
return decoded.Slice(0, written);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int Int32OverflowLength = 10;
|
||||||
|
|
||||||
public byte[] Encode(byte[] payload)
|
public byte[] Encode(byte[] payload)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = new byte[Base64.GetMaxEncodedToUtf8Length(payload.Length)];
|
var maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(payload.Length);
|
||||||
|
|
||||||
var status = Base64.EncodeToUtf8(payload, buffer, out _, out var written);
|
// Int32OverflowLength + length of separator (':') + length of terminator (';')
|
||||||
|
if (int.MaxValue - maxEncodedLength < Int32OverflowLength + 2)
|
||||||
|
{
|
||||||
|
throw new FormatException("The encoded message exceeds the maximum supported size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//The format is: [{length}:{message};] so allocate enough to be able to write the entire message
|
||||||
|
Span<byte> buffer = new byte[Int32OverflowLength + 1 + maxEncodedLength + 1];
|
||||||
|
|
||||||
|
buffer[Int32OverflowLength] = (byte)':';
|
||||||
|
var status = Base64.EncodeToUtf8(payload, buffer.Slice(Int32OverflowLength + 1), out _, out var written);
|
||||||
Debug.Assert(status == OperationStatus.Done);
|
Debug.Assert(status == OperationStatus.Done);
|
||||||
|
|
||||||
using (var stream = new MemoryStream())
|
buffer[Int32OverflowLength + 1 + written] = (byte)';';
|
||||||
|
var prefixLength = 0;
|
||||||
|
var prefix = written;
|
||||||
|
do
|
||||||
{
|
{
|
||||||
LengthPrefixedTextMessageWriter.WriteMessage(buffer.Slice(0, written), stream);
|
buffer[Int32OverflowLength - 1 - prefixLength] = (byte)('0' + prefix % 10);
|
||||||
return stream.ToArray();
|
prefix /= 10;
|
||||||
|
prefixLength++;
|
||||||
}
|
}
|
||||||
|
while (prefix > 0);
|
||||||
|
|
||||||
|
return buffer.Slice(Int32OverflowLength - prefixLength, prefixLength + 1 + written + 1).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
public static class LengthPrefixedTextMessageParser
|
public static class LengthPrefixedTextMessageParser
|
||||||
{
|
{
|
||||||
private const int Int32OverflowLength = 10;
|
private const int Int32OverflowLength = 10;
|
||||||
|
private const char FieldDelimiter = ':';
|
||||||
|
private const char MessageDelimiter = ';';
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an
|
/// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an
|
||||||
|
|
@ -25,7 +27,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
|
|
||||||
var remaining = buffer.Slice(index);
|
var remaining = buffer.Slice(index);
|
||||||
|
|
||||||
if (!TryReadDelimiter(remaining, LengthPrefixedTextMessageWriter.FieldDelimiter, "length"))
|
if (!TryReadDelimiter(remaining, FieldDelimiter, "length"))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +44,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
|
|
||||||
remaining = remaining.Slice(length);
|
remaining = remaining.Slice(length);
|
||||||
|
|
||||||
if (!TryReadDelimiter(remaining, LengthPrefixedTextMessageWriter.MessageDelimiter, "payload"))
|
if (!TryReadDelimiter(remaining, MessageDelimiter, "payload"))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
{
|
{
|
||||||
length = 0;
|
length = 0;
|
||||||
// Read until the first ':' to find the length
|
// Read until the first ':' to find the length
|
||||||
index = buffer.IndexOf((byte)LengthPrefixedTextMessageWriter.FieldDelimiter);
|
index = buffer.IndexOf((byte)FieldDelimiter);
|
||||||
|
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
|
||||||
{
|
|
||||||
public static class LengthPrefixedTextMessageWriter
|
|
||||||
{
|
|
||||||
private const int Int32OverflowLength = 10;
|
|
||||||
|
|
||||||
internal const char FieldDelimiter = ':';
|
|
||||||
internal const char MessageDelimiter = ';';
|
|
||||||
|
|
||||||
public static void WriteMessage(ReadOnlySpan<byte> payload, Stream output)
|
|
||||||
{
|
|
||||||
// Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary
|
|
||||||
|
|
||||||
// Write the length as a string
|
|
||||||
|
|
||||||
// Super inefficient...
|
|
||||||
var lengthString = payload.Length.ToString(CultureInfo.InvariantCulture);
|
|
||||||
var buffer = ArrayPool<byte>.Shared.Rent(Int32OverflowLength);
|
|
||||||
var encodedLength = Encoding.UTF8.GetBytes(lengthString, 0, lengthString.Length, buffer, 0);
|
|
||||||
output.Write(buffer, 0, encodedLength);
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
|
|
||||||
// Write the field delimiter ':'
|
|
||||||
output.WriteByte((byte)FieldDelimiter);
|
|
||||||
|
|
||||||
buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
|
|
||||||
payload.CopyTo(buffer);
|
|
||||||
output.Write(buffer, 0, payload.Length);
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
|
|
||||||
// Terminator
|
|
||||||
output.WriteByte((byte)MessageDelimiter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
|
{
|
||||||
|
public class Base64EncoderTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(Payloads))]
|
||||||
|
public void VerifyDecode(string payload, string encoded)
|
||||||
|
{
|
||||||
|
var message = Encoding.UTF8.GetBytes(payload);
|
||||||
|
var encodedMessage = Encoding.UTF8.GetString(new Base64Encoder().Encode(message));
|
||||||
|
Assert.Equal(encoded, encodedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(Payloads))]
|
||||||
|
public void VerifyEncode(string payload, string encoded)
|
||||||
|
{
|
||||||
|
var encodedMessage = Encoding.UTF8.GetBytes(encoded);
|
||||||
|
var decodedMessage = Encoding.UTF8.GetString(new Base64Encoder().Decode(encodedMessage).ToArray());
|
||||||
|
Assert.Equal(payload, decodedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> Payloads =>
|
||||||
|
new object[][]
|
||||||
|
{
|
||||||
|
new object[] { "", "0:;" },
|
||||||
|
new object[] { "ABC", "4:QUJD;" },
|
||||||
|
new object[] { "A\nR\rC\r\n;DEF1234567890", "28:QQpSDUMNCjtERUYxMjM0NTY3ODkw;" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Tests.Internal.Encoders
|
|
||||||
{
|
|
||||||
public class LengthPrefixedTextMessageFormatterTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void WriteMultipleMessages()
|
|
||||||
{
|
|
||||||
const string expectedEncoding = "0:;14:Hello,\r\nWorld!;";
|
|
||||||
var messages = new[]
|
|
||||||
{
|
|
||||||
new byte[0],
|
|
||||||
Encoding.UTF8.GetBytes("Hello,\r\nWorld!")
|
|
||||||
};
|
|
||||||
|
|
||||||
var output = new MemoryStream();
|
|
||||||
foreach (var message in messages)
|
|
||||||
{
|
|
||||||
LengthPrefixedTextMessageWriter.WriteMessage(message, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Equal(expectedEncoding, Encoding.UTF8.GetString(output.ToArray()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("0:;", "")]
|
|
||||||
[InlineData("3:ABC;", "ABC")]
|
|
||||||
[InlineData("11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")]
|
|
||||||
public void WriteMessage(string encoded, string payload)
|
|
||||||
{
|
|
||||||
var message = Encoding.UTF8.GetBytes(payload);
|
|
||||||
var output = new MemoryStream();
|
|
||||||
|
|
||||||
LengthPrefixedTextMessageWriter.WriteMessage(message, output);
|
|
||||||
|
|
||||||
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue