Remove dependencies on a bunch of corefxlab things (#576)

* Remove dependencies on a bunch of corefxlab things
- Used Stream instead of IOutput
- Removed pipelines dependency in most places.
This commit is contained in:
David Fowler 2017-06-22 09:23:52 -07:00 committed by GitHub
parent d28c1c81c0
commit 41f54d001b
40 changed files with 258 additions and 437 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.26510.0 VisualStudioVersion = 15.0.26606.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA69F624-5398-4884-87E4-B816698CDE65}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA69F624-5398-4884-87E4-B816698CDE65}"
EndProject EndProject
@ -52,8 +52,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Signal
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{6CEC3DC2-5B01-45A8-8F0D-8531315DA90B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{6CEC3DC2-5B01-45A8-8F0D-8531315DA90B}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
test\Common\ArrayOutput.cs = test\Common\ArrayOutput.cs
test\Common\ByteArrayExtensions.cs = test\Common\ByteArrayExtensions.cs
test\Common\ChannelExtensions.cs = test\Common\ChannelExtensions.cs test\Common\ChannelExtensions.cs = test\Common\ChannelExtensions.cs
test\Common\TaskExtensions.cs = test\Common\TaskExtensions.cs test\Common\TaskExtensions.cs = test\Common\TaskExtensions.cs
EndProjectSection EndProjectSection

View File

@ -3,6 +3,7 @@
<AspNetCoreIntegrationTestingVersion>0.4.0-*</AspNetCoreIntegrationTestingVersion> <AspNetCoreIntegrationTestingVersion>0.4.0-*</AspNetCoreIntegrationTestingVersion>
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion> <AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
<CoreFxLabsVersion>0.1.0-*</CoreFxLabsVersion> <CoreFxLabsVersion>0.1.0-*</CoreFxLabsVersion>
<CoreFxVersion>4.4.0-*</CoreFxVersion>
<GoogleProtobufVersion>3.1.0</GoogleProtobufVersion> <GoogleProtobufVersion>3.1.0</GoogleProtobufVersion>
<InternalAspNetCoreSdkVersion>2.1.0-*</InternalAspNetCoreSdkVersion> <InternalAspNetCoreSdkVersion>2.1.0-*</InternalAspNetCoreSdkVersion>
<JsonNetVersion>10.0.1</JsonNetVersion> <JsonNetVersion>10.0.1</JsonNetVersion>

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO.Pipelines;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipelines;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;

View File

@ -1,59 +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.Binary;
using System.Runtime;
using System.Runtime.CompilerServices;
namespace System.Buffers
{
internal static class IOutputExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteBigEndian<[Primitive] T>(this IOutput self, T value) where T : struct
{
var size = Unsafe.SizeOf<T>();
if (self.Buffer.Length < size)
{
self.Enlarge(size);
if (self.Buffer.Length < size)
{
return false;
}
}
self.Buffer.WriteBigEndian(value);
self.Advance(size);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWrite(this IOutput self, ReadOnlySpan<byte> data)
{
while (data.Length > 0)
{
if (self.Buffer.Length == 0)
{
self.Enlarge(data.Length);
if (self.Buffer.Length == 0)
{
// Failed to enlarge
return false;
}
}
var toWrite = Math.Min(self.Buffer.Length, data.Length);
// Slice based on what we can fit
var chunk = data.Slice(0, toWrite);
data = data.Slice(toWrite);
// Copy the chunk
chunk.CopyTo(self.Buffer);
self.Advance(chunk.Length);
}
return true;
}
}
}

View File

@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
{ {
try try
{ {
var payload = await _protocol.WriteToArrayAsync(invocationMessage); var payload = _protocol.WriteToArray(invocationMessage);
_logger.LogInformation("Sending Invocation '{invocationId}'", invocationMessage.InvocationId); _logger.LogInformation("Sending Invocation '{invocationId}'", invocationMessage.InvocationId);

View File

@ -2,24 +2,26 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Binary;
using System.Buffers; using System.Buffers;
using System.IO;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{ {
public static class BinaryMessageFormatter public static class BinaryMessageFormatter
{ {
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output) public static bool TryWriteMessage(ReadOnlySpan<byte> payload, MemoryStream output)
{ {
// Try to write the data var length = sizeof(long);
if (!output.TryWriteBigEndian((long)payload.Length)) var buffer = ArrayPool<byte>.Shared.Rent(length);
{ BufferWriter.WriteBigEndian<long>(buffer, payload.Length);
return false; output.Write(buffer, 0, length);
} ArrayPool<byte>.Shared.Return(buffer);
if (!output.TryWrite(payload)) buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
{ payload.CopyTo(buffer);
return false; output.Write(buffer, 0, payload.Length);
} ArrayPool<byte>.Shared.Return(buffer);
return true; return true;
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Binary; using System.Binary;
using System.Buffers;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{ {
@ -16,33 +15,27 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
_state = default(ParserState); _state = default(ParserState);
} }
public bool TryParseMessage(ref BytesReader buffer, out ReadOnlyBuffer<byte> payload) public bool TryParseMessage(ref ReadOnlySpan<byte> buffer, out ReadOnlyBuffer<byte> payload)
{ {
if (_state.Length == null) if (_state.Length == null)
{ {
var lengthBuffer = buffer.TryReadBytes(sizeof(long)); long length = 0;
if (lengthBuffer == null) if (buffer.Length < sizeof(long))
{ {
payload = default(ReadOnlyBuffer<byte>); payload = default(ReadOnlyBuffer<byte>);
return false; return false;
} }
var length = lengthBuffer.Value.ToSingleSpan(); length = buffer.Slice(0, sizeof(long)).ReadBigEndian<long>();
if (length.Length < sizeof(long)) if (length > Int32.MaxValue)
{
payload = default(ReadOnlyBuffer<byte>);
return false;
}
var longLength = length.ReadBigEndian<long>();
if (longLength > Int32.MaxValue)
{ {
throw new FormatException("Messages over 2GB in size are not supported"); throw new FormatException("Messages over 2GB in size are not supported");
} }
buffer.Advance(length.Length);
_state.Length = (int)longLength; buffer = buffer.Slice(sizeof(long));
_state.Length = (int)length;
} }
if (_state.Payload == null) if (_state.Payload == null)
@ -50,13 +43,13 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
_state.Payload = new byte[_state.Length.Value]; _state.Payload = new byte[_state.Length.Value];
} }
while (_state.Read < _state.Payload.Length && buffer.Unread.Length > 0) while (_state.Read < _state.Payload.Length && buffer.Length > 0)
{ {
// Copy what we can from the current unread segment // Copy what we can from the current unread segment
var toCopy = Math.Min(_state.Payload.Length - _state.Read, buffer.Unread.Length); var toCopy = Math.Min(_state.Payload.Length - _state.Read, buffer.Length);
buffer.Unread.Slice(0, toCopy).CopyTo(_state.Payload.Slice(_state.Read)); buffer.Slice(0, toCopy).CopyTo(new Span<byte>(_state.Payload, _state.Read));
_state.Read += toCopy; _state.Read += toCopy;
buffer.Advance(toCopy); buffer = buffer.Slice(toCopy);
} }
if (_state.Read == _state.Payload.Length) if (_state.Read == _state.Payload.Length)

View File

@ -1,32 +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.
namespace System.Buffers
{
internal static class BufferExtensions
{
public static ReadOnlySpan<byte> ToSingleSpan(this ReadOnlyBytes self)
{
if (self.Rest == null)
{
return self.First.Span;
}
else
{
return self.ToSpan();
}
}
public static ReadOnlyBytes? TryReadBytes(this BytesReader self, int count)
{
try
{
return self.ReadBytes(count);
}
catch (ArgumentOutOfRangeException)
{
return null;
}
}
}
}

View File

@ -2,37 +2,44 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Binary;
using System.Buffers; using System.Buffers;
using System.Globalization;
using System.IO;
using System.Text; using System.Text;
using System.Text.Formatting;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{ {
public static class TextMessageFormatter public static class TextMessageFormatter
{ {
private const int Int32OverflowLength = 10;
internal const char FieldDelimiter = ':'; internal const char FieldDelimiter = ':';
internal const char MessageDelimiter = ';'; internal const char MessageDelimiter = ';';
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output) public static bool TryWriteMessage(ReadOnlySpan<byte> payload, Stream output)
{ {
// Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary // Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary
var length = payload.Length;
// Write the length as a string // Write the length as a string
output.Append(length, TextEncoder.Utf8);
// 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 ':' // Write the field delimiter ':'
output.Append(FieldDelimiter, TextEncoder.Utf8); output.WriteByte((byte)FieldDelimiter);
// Write the payload buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
if (!output.TryWrite(payload)) payload.CopyTo(buffer);
{ output.Write(buffer, 0, payload.Length);
return false; ArrayPool<byte>.Shared.Return(buffer);
}
// Terminator // Terminator
output.Append(MessageDelimiter, TextEncoder.Utf8); output.WriteByte((byte)MessageDelimiter);
return true; return true;
} }
} }

View File

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Text; using System.Text;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{ {
public class TextMessageParser public class TextMessageParser
{ {
private const int Int32OverflowLength = 10;
private ParserState _state; private ParserState _state;
public void Reset() public void Reset()
@ -20,9 +21,9 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
/// 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
/// exception if there is a format error in the provided data. /// exception if there is a format error in the provided data.
/// </summary> /// </summary>
public bool TryParseMessage(ref BytesReader buffer, out ReadOnlyBuffer<byte> payload) public bool TryParseMessage(ref ReadOnlySpan<byte> buffer, out ReadOnlyBuffer<byte> payload)
{ {
while (buffer.Unread.Length > 0) while (buffer.Length > 0)
{ {
switch (_state.Phase) switch (_state.Phase)
{ {
@ -65,53 +66,50 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
return false; return false;
} }
private bool TryReadLength(ref BytesReader buffer) private bool TryReadLength(ref ReadOnlySpan<byte> buffer)
{ {
// Read until the first ':' to find the length // Read until the first ':' to find the length
var lengthBuffer = buffer.ReadBytesUntil((byte)TextMessageFormatter.FieldDelimiter); var found = buffer.IndexOf((byte)TextMessageFormatter.FieldDelimiter);
if (lengthBuffer == null) if (found == -1)
{ {
// Insufficient data // Insufficient data
return false; return false;
} }
var lengthSpan = lengthBuffer.Value.ToSingleSpan(); var lengthSpan = buffer.Slice(0, found);
// Parse the length if (!TryParseInt32(lengthSpan, out var length, out var bytesConsumed) || bytesConsumed < lengthSpan.Length)
if (!PrimitiveParser.TryParseInt32(lengthSpan, out var length, out var consumedByLength, encoder: TextEncoder.Utf8) || consumedByLength < lengthSpan.Length)
{ {
if (TextEncoder.Utf8.TryDecode(lengthSpan, out var lengthString, out _)) throw new FormatException($"Invalid length: '{Encoding.UTF8.GetString(lengthSpan.ToArray())}'");
{
throw new FormatException($"Invalid length: '{lengthString}'");
}
throw new FormatException("Invalid length");
} }
buffer = buffer.Slice(found);
_state.Length = length; _state.Length = length;
_state.Phase = ParsePhase.LengthComplete; _state.Phase = ParsePhase.LengthComplete;
return true; return true;
} }
private bool TryReadDelimiter(ref BytesReader buffer, char delimiter, ParsePhase nextPhase, string field) private bool TryReadDelimiter(ref ReadOnlySpan<byte> buffer, char delimiter, ParsePhase nextPhase, string field)
{ {
if (buffer.Unread.Length == 0) if (buffer.Length == 0)
{ {
return false; return false;
} }
if (buffer.Unread[0] != delimiter) if (buffer[0] != delimiter)
{ {
throw new FormatException($"Missing delimiter '{delimiter}' after {field}"); throw new FormatException($"Missing delimiter '{delimiter}' after {field}");
} }
buffer.Advance(1);
buffer = buffer.Slice(1);
_state.Phase = nextPhase; _state.Phase = nextPhase;
return true; return true;
} }
private void ReadPayload(ref BytesReader buffer) private void ReadPayload(ref ReadOnlySpan<byte> buffer)
{ {
if (_state.Payload == null) if (_state.Payload == null)
{ {
@ -125,13 +123,105 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
else else
{ {
// Copy as much as possible from the Unread buffer // Copy as much as possible from the Unread buffer
var toCopy = Math.Min(_state.Length - _state.Read, buffer.Unread.Length); var toCopy = Math.Min(_state.Length - _state.Read, buffer.Length);
buffer.Unread.Slice(0, toCopy).CopyTo(_state.Payload.Slice(_state.Read));
buffer.Slice(0, toCopy).CopyTo(new Span<byte>(_state.Payload, _state.Read));
_state.Read += toCopy; _state.Read += toCopy;
buffer.Advance(toCopy); buffer = buffer.Slice(toCopy);
} }
} }
public static bool TryParseInt32(ReadOnlySpan<byte> text, out int value, out int bytesConsumed)
{
if (text.Length < 1)
{
bytesConsumed = 0;
value = default(int);
return false;
}
int indexOfFirstDigit = 0;
int sign = 1;
if (text[0] == '-')
{
indexOfFirstDigit = 1;
sign = -1;
}
else if (text[0] == '+')
{
indexOfFirstDigit = 1;
}
int overflowLength = Int32OverflowLength + indexOfFirstDigit;
// Parse the first digit separately. If invalid here, we need to return false.
int firstDigit = text[indexOfFirstDigit] - 48; // '0'
if (firstDigit < 0 || firstDigit > 9)
{
bytesConsumed = 0;
value = default(int);
return false;
}
int parsedValue = firstDigit;
if (text.Length < overflowLength)
{
// Length is less than Int32OverflowLength; overflow is not possible
for (int index = indexOfFirstDigit + 1; index < text.Length; index++)
{
int nextDigit = text[index] - 48; // '0'
if (nextDigit < 0 || nextDigit > 9)
{
bytesConsumed = index;
value = parsedValue * sign;
return true;
}
parsedValue = parsedValue * 10 + nextDigit;
}
}
else
{
// Length is greater than Int32OverflowLength; overflow is only possible after Int32OverflowLength
// digits. There may be no overflow after Int32OverflowLength if there are leading zeroes.
for (int index = indexOfFirstDigit + 1; index < overflowLength - 1; index++)
{
int nextDigit = text[index] - 48; // '0'
if (nextDigit < 0 || nextDigit > 9)
{
bytesConsumed = index;
value = parsedValue * sign;
return true;
}
parsedValue = parsedValue * 10 + nextDigit;
}
for (int index = overflowLength - 1; index < text.Length; index++)
{
int nextDigit = text[index] - 48; // '0'
if (nextDigit < 0 || nextDigit > 9)
{
bytesConsumed = index;
value = parsedValue * sign;
return true;
}
// If parsedValue > (int.MaxValue / 10), any more appended digits will cause overflow.
// if parsedValue == (int.MaxValue / 10), any nextDigit greater than 7 or 8 (depending on sign) implies overflow.
bool positive = sign > 0;
bool nextDigitTooLarge = nextDigit > 8 || (positive && nextDigit > 7);
if (parsedValue > int.MaxValue / 10 || parsedValue == int.MaxValue / 10 && nextDigitTooLarge)
{
bytesConsumed = 0;
value = default(int);
return false;
}
parsedValue = parsedValue * 10 + nextDigit;
}
}
bytesConsumed = text.Length;
value = parsedValue * sign;
return true;
}
private struct ParserState private struct ParserState
{ {
public ParsePhase Phase; public ParsePhase Phase;

View File

@ -3,34 +3,22 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
public static class HubProtocolWriteMessageExtensions public static class HubProtocolWriteMessageExtensions
{ {
public static async ValueTask<byte[]> WriteToArrayAsync(this IHubProtocol protocol, HubMessage message) public static byte[] WriteToArray(this IHubProtocol protocol, HubMessage message)
{ {
using (var memoryStream = new MemoryStream()) using (var output = new MemoryStream())
{ {
var pipe = memoryStream.AsPipelineWriter();
// See https://github.com/dotnet/corefxlab/issues/1460, the TextEncoder is unimportant but required.
var output = new PipelineTextOutput(pipe, TextEncoder.Utf8);
// Encode the message // Encode the message
if (!protocol.TryWriteMessage(message, output)) if (!protocol.TryWriteMessage(message, output))
{ {
throw new InvalidOperationException("Failed to write message to the output stream"); throw new InvalidOperationException("Failed to write message to the output stream");
} }
await output.FlushAsync(); return output.ToArray();
// Create a message
return memoryStream.ToArray();
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
@ -11,6 +12,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages); bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages);
bool TryWriteMessage(HubMessage message, IOutput output); bool TryWriteMessage(HubMessage message, Stream output);
} }
} }

View File

@ -47,11 +47,10 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages) public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages)
{ {
var reader = new BytesReader(input.ToArray());
messages = new List<HubMessage>(); messages = new List<HubMessage>();
var parser = new TextMessageParser(); var parser = new TextMessageParser();
while (parser.TryParseMessage(ref reader, out var payload)) while (parser.TryParseMessage(ref input, out var payload))
{ {
// TODO: Need a span-native JSON parser! // TODO: Need a span-native JSON parser!
using (var memoryStream = new MemoryStream(payload.ToArray())) using (var memoryStream = new MemoryStream(payload.ToArray()))
@ -63,9 +62,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
return messages.Count > 0; return messages.Count > 0;
} }
public bool TryWriteMessage(HubMessage message, IOutput output) public bool TryWriteMessage(HubMessage message, Stream output)
{ {
// TODO: Need IOutput-compatible JSON serializer!
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
WriteMessage(message, memoryStream); WriteMessage(message, memoryStream);

View File

@ -12,13 +12,10 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace> <RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="../Common/IOutputExtensions.cs" Link="IOutputExtensions.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" /> <PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" /> <PackageReference Include="System.Buffers.Primitives" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.Binary" Version="$(CoreFxLabsVersion)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,15 +5,12 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -313,7 +310,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
private async Task WriteAsync(ConnectionContext connection, HubMessage hubMessage) private async Task WriteAsync(ConnectionContext connection, HubMessage hubMessage)
{ {
var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol); var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol);
var data = await protocol.WriteToArrayAsync(hubMessage); var data = protocol.WriteToArray(hubMessage);
while (await connection.Transport.Output.WaitToWriteAsync()) while (await connection.Transport.Output.WaitToWriteAsync())
{ {

View File

@ -4,14 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
namespace Microsoft.AspNetCore.SignalR namespace Microsoft.AspNetCore.SignalR
{ {
@ -125,7 +122,7 @@ namespace Microsoft.AspNetCore.SignalR
private async Task WriteAsync(ConnectionContext connection, HubMessage hubMessage) private async Task WriteAsync(ConnectionContext connection, HubMessage hubMessage)
{ {
var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol); var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol);
var payload = await protocol.WriteToArrayAsync(hubMessage); var payload = protocol.WriteToArray(hubMessage);
while (await connection.Transport.Output.WaitToWriteAsync()) while (await connection.Transport.Output.WaitToWriteAsync())
{ {

View File

@ -230,7 +230,7 @@ namespace Microsoft.AspNetCore.SignalR
private async Task SendMessageAsync(ConnectionContext connection, IHubProtocol protocol, HubMessage hubMessage) private async Task SendMessageAsync(ConnectionContext connection, IHubProtocol protocol, HubMessage hubMessage)
{ {
var payload = await protocol.WriteToArrayAsync(hubMessage); var payload = protocol.WriteToArray(hubMessage);
while (await connection.Transport.Output.WaitToWriteAsync()) while (await connection.Transport.Output.WaitToWriteAsync())
{ {

View File

@ -3,7 +3,6 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -2,8 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Binary;
using System.Buffers; using System.Buffers;
using System.IO;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{ {
@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
private const byte LineFeed = (byte)'\n'; private const byte LineFeed = (byte)'\n';
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output) public static bool TryWriteMessage(ReadOnlySpan<byte> payload, MemoryStream output)
{ {
// Write the payload // Write the payload
if (!TryWritePayload(payload, output)) if (!TryWritePayload(payload, output))
@ -22,15 +22,12 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
return false; return false;
} }
if (!output.TryWrite(Newline)) output.Write(Newline, 0, Newline.Length);
{
return false;
}
return true; return true;
} }
private static bool TryWritePayload(ReadOnlySpan<byte> payload, IOutput output) private static bool TryWritePayload(ReadOnlySpan<byte> payload, Stream output)
{ {
// Short-cut for empty payload // Short-cut for empty payload
if (payload.Length == 0) if (payload.Length == 0)
@ -88,22 +85,16 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
return true; return true;
} }
private static bool TryWriteLine(ReadOnlySpan<byte> line, IOutput output) private static bool TryWriteLine(ReadOnlySpan<byte> payload, Stream output)
{ {
if (!output.TryWrite(DataPrefix)) output.Write(DataPrefix, 0, DataPrefix.Length);
{
return false;
}
if (!output.TryWrite(line)) var buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
{ payload.CopyTo(buffer);
return false; output.Write(buffer, 0, payload.Length);
} ArrayPool<byte>.Shared.Return(buffer);
if (!output.TryWrite(Newline)) output.Write(Newline, 0, Newline.Length);
{
return false;
}
return true; return true;
} }

View File

@ -2,9 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO.Pipelines; using System.IO;
using System.IO.Pipelines.Text.Primitives;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Channels; using System.Threading.Tasks.Channels;
@ -44,17 +42,15 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
await context.Response.WriteAsync(":\r\n"); await context.Response.WriteAsync(":\r\n");
await context.Response.Body.FlushAsync(); await context.Response.Body.FlushAsync();
var pipe = context.Response.Body.AsPipelineWriter();
var output = new PipelineTextOutput(pipe, TextEncoder.Utf8); // We don't need the Encoder, but it's harmless to set.
try try
{ {
var ms = new MemoryStream();
while (await _application.WaitToReadAsync(token)) while (await _application.WaitToReadAsync(token))
{ {
while (_application.TryRead(out var buffer)) while (_application.TryRead(out var buffer))
{ {
_logger.SSEWritingMessage(_connectionId, buffer.Length); _logger.SSEWritingMessage(_connectionId, buffer.Length);
if (!ServerSentEventsMessageFormatter.TryWriteMessage(buffer, output)) if (!ServerSentEventsMessageFormatter.TryWriteMessage(buffer, ms))
{ {
// We ran out of space to write, even after trying to enlarge. // We ran out of space to write, even after trying to enlarge.
// This should only happen in a significant lack-of-memory scenario. // This should only happen in a significant lack-of-memory scenario.
@ -64,11 +60,12 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
// Throwing InvalidOperationException here, but it's not quite an invalid operation... // Throwing InvalidOperationException here, but it's not quite an invalid operation...
throw new InvalidOperationException("Ran out of space to format messages!"); throw new InvalidOperationException("Ran out of space to format messages!");
} }
await output.FlushAsync();
} }
} }
ms.Seek(0, SeekOrigin.Begin);
await ms.CopyToAsync(context.Response.Body);
await _application.Completion; await _application.Completion;
} }
catch (OperationCanceledException) catch (OperationCanceledException)

View File

@ -11,10 +11,6 @@
<EnableApiCheck>false</EnableApiCheck> <EnableApiCheck>false</EnableApiCheck>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="../Common/IOutputExtensions.cs" Link="IOutputExtensions.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.csproj" /> <ProjectReference Include="..\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common.Http\Microsoft.AspNetCore.Sockets.Common.Http.csproj" /> <ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common.Http\Microsoft.AspNetCore.Sockets.Common.Http.csproj" />
@ -24,7 +20,8 @@
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" /> <PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" /> <PackageReference Include="System.Memory" Version="$(CoreFxVersion)" />
<PackageReference Include="System.Binary" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" /> <PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
</ItemGroup> </ItemGroup>

View File

@ -1,75 +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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Sockets.Tests.Internal
{
internal class ArrayOutput : IOutput
{
private IList<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();
private int _chunkSize;
private byte[] _activeBuffer;
private int _offset;
public Span<byte> Buffer => _activeBuffer.Slice(_offset);
public ArrayOutput(int chunkSize)
{
_chunkSize = chunkSize;
AdvanceChunk();
}
public void Advance(int bytes)
{
// Determine the new location
_offset += bytes;
Debug.Assert(_offset <= _activeBuffer.Length, "How did we write more data than we had space?");
}
public void Enlarge(int desiredBufferLength = 0)
{
if (desiredBufferLength == 0 || _activeBuffer.Length - _offset < desiredBufferLength)
{
AdvanceChunk();
}
}
public byte[] ToArray()
{
var totalLength = _buffers.Sum(b => b.Count) + _offset;
var arr = new byte[totalLength];
int offset = 0;
foreach (var buffer in _buffers)
{
System.Buffer.BlockCopy(buffer.Array, 0, arr, offset, buffer.Count);
offset += buffer.Count;
}
if (_offset > 0)
{
System.Buffer.BlockCopy(_activeBuffer, 0, arr, offset, _offset);
}
return arr;
}
private void AdvanceChunk()
{
if (_activeBuffer != null)
{
_buffers.Add(new ArraySegment<byte>(_activeBuffer, 0, _offset));
}
_activeBuffer = new byte[_chunkSize];
_offset = 0;
}
}
}

View File

@ -1,47 +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.Buffers;
using System.Collections.Generic;
namespace System
{
internal static class ByteArrayExtensions
{
public static ReadOnlyBytes ToChunkedReadOnlyBytes(this byte[] data, int chunkSize)
{
if (chunkSize == 0)
{
return new ReadOnlyBytes(data);
}
var chunks = new List<byte[]>();
for (var i = 0; i < data.Length; i += chunkSize)
{
var thisChunkSize = Math.Min(chunkSize, data.Length - i);
var chunk = new byte[thisChunkSize];
for (var j = 0; j < thisChunkSize; j++)
{
chunk[j] = data[i + j];
}
chunks.Add(chunk);
}
chunks.Reverse();
ReadOnlyBytes? bytes = null;
foreach (var chunk in chunks)
{
if (bytes == null)
{
bytes = new ReadOnlyBytes(chunk);
}
else
{
bytes = new ReadOnlyBytes(chunk, bytes);
}
}
return bytes.Value;
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
@ -191,7 +192,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
throw new InvalidOperationException("No Parsed Message provided"); throw new InvalidOperationException("No Parsed Message provided");
} }
public bool TryWriteMessage(HubMessage message, IOutput output) public bool TryWriteMessage(HubMessage message, Stream output)
{ {
WriteCalls += 1; WriteCalls += 1;

View File

@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" /> <Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" />
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
<Compile Include="..\Common\ChannelExtensions.cs" Link="ChannelExtensions.cs" /> <Compile Include="..\Common\ChannelExtensions.cs" Link="ChannelExtensions.cs" />
</ItemGroup> </ItemGroup>

View File

@ -2,15 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Channels; using System.Threading.Tasks.Channels;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xunit; using Xunit;
@ -88,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
private byte[] FormatMessageToArray(byte[] message) private byte[] FormatMessageToArray(byte[] message)
{ {
var output = new ArrayOutput(1024); var output = new MemoryStream();
Assert.True(TextMessageFormatter.TryWriteMessage(message, output)); Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
return output.ToArray(); return output.ToArray();
} }

View File

@ -1,12 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Xunit; using Xunit;
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[Theory] [Theory]
[MemberData(nameof(ProtocolTestData))] [MemberData(nameof(ProtocolTestData))]
public async Task WriteMessage(HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string expectedOutput) public void WriteMessage(HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string expectedOutput)
{ {
expectedOutput = Frame(expectedOutput); expectedOutput = Frame(expectedOutput);
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
}; };
var protocol = new JsonHubProtocol(jsonSerializer); var protocol = new JsonHubProtocol(jsonSerializer);
var encoded = await protocol.WriteToArrayAsync(message); var encoded = protocol.WriteToArray(message);
var json = Encoding.UTF8.GetString(encoded); var json = Encoding.UTF8.GetString(encoded);
Assert.Equal(expectedOutput, json); Assert.Equal(expectedOutput, json);
@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
private static byte[] FormatMessageToArray(byte[] message) private static byte[] FormatMessageToArray(byte[] message)
{ {
var output = new ArrayOutput(1024); var output = new MemoryStream();
Assert.True(TextMessageFormatter.TryWriteMessage(message, output)); Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
return output.ToArray(); return output.ToArray();
} }

View File

@ -7,10 +7,6 @@
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp2.0</TargetFrameworks> <TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" /> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.IO;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
{ {
@ -12,8 +12,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
private static readonly Random Random = new Random(); private static readonly Random Random = new Random();
private readonly TextMessageParser _textMessageParser = new TextMessageParser(); private readonly TextMessageParser _textMessageParser = new TextMessageParser();
private readonly BinaryMessageParser _binaryMessageParser = new BinaryMessageParser(); private readonly BinaryMessageParser _binaryMessageParser = new BinaryMessageParser();
private ReadOnlyBytes _binaryInput; private ReadOnlyBuffer<byte> _binaryInput;
private ReadOnlyBytes _textInput; private ReadOnlyBuffer<byte> _textInput;
[Params(32, 64)] [Params(32, 64)]
public int ChunkSize { get; set; } public int ChunkSize { get; set; }
@ -26,30 +26,30 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
{ {
var buffer = new byte[MessageLength]; var buffer = new byte[MessageLength];
Random.NextBytes(buffer); Random.NextBytes(buffer);
var output = new ArrayOutput(MessageLength + 32); var output = new MemoryStream();
if (!BinaryMessageFormatter.TryWriteMessage(buffer, output)) if (!BinaryMessageFormatter.TryWriteMessage(buffer, output))
{ {
throw new InvalidOperationException("Failed to format message"); throw new InvalidOperationException("Failed to format message");
} }
_binaryInput = output.ToArray().ToChunkedReadOnlyBytes(ChunkSize); _binaryInput = output.ToArray();
buffer = new byte[MessageLength]; buffer = new byte[MessageLength];
Random.NextBytes(buffer); Random.NextBytes(buffer);
output = new ArrayOutput(MessageLength + 32); output = new MemoryStream();
if (!TextMessageFormatter.TryWriteMessage(buffer, output)) if (!TextMessageFormatter.TryWriteMessage(buffer, output))
{ {
throw new InvalidOperationException("Failed to format message"); throw new InvalidOperationException("Failed to format message");
} }
_textInput = output.ToArray().ToChunkedReadOnlyBytes(ChunkSize); _textInput = output.ToArray();
} }
[Benchmark] [Benchmark]
public void SingleBinaryMessage() public void SingleBinaryMessage()
{ {
var reader = new BytesReader(_binaryInput); var buffer = _binaryInput.Span;
if (!_binaryMessageParser.TryParseMessage(ref reader, out _)) if (!_binaryMessageParser.TryParseMessage(ref buffer, out _))
{ {
throw new InvalidOperationException("Failed to parse"); throw new InvalidOperationException("Failed to parse");
} }
@ -58,8 +58,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
[Benchmark] [Benchmark]
public void SingleTextMessage() public void SingleTextMessage()
{ {
var reader = new BytesReader(_textInput); var buffer = _textInput.Span;
if (!_textMessageParser.TryParseMessage(ref reader, out _)) if (!_textMessageParser.TryParseMessage(ref buffer, out _))
{ {
throw new InvalidOperationException("Failed to parse"); throw new InvalidOperationException("Failed to parse");
} }

View File

@ -7,11 +7,6 @@
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks> <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Common\ByteArrayExtensions.cs" Link="ByteArrayExtensions.cs" />
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" /> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.3" /> <PackageReference Include="BenchmarkDotNet" Version="0.10.3" />

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Xunit; using Xunit;
@ -28,7 +29,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
Encoding.UTF8.GetBytes("Hello,\r\nWorld!") Encoding.UTF8.GetBytes("Hello,\r\nWorld!")
}; };
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing var output = new MemoryStream(); // Use small chunks to test Advance/Enlarge and partial payload writing
foreach (var message in messages) foreach (var message in messages)
{ {
Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output)); Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output));
@ -46,11 +47,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
[InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] [InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
public void WriteBinaryMessage(int offset, int chunkSize, byte[] encoded, byte[] payload) public void WriteBinaryMessage(int offset, int chunkSize, byte[] encoded, byte[] payload)
{ {
var output = new ArrayOutput(chunkSize); var output = new MemoryStream();
if (offset > 0) if (offset > 0)
{ {
output.Advance(offset); output.Seek(offset, SeekOrigin.Begin);
} }
Assert.True(BinaryMessageFormatter.TryWriteMessage(payload, output)); Assert.True(BinaryMessageFormatter.TryWriteMessage(payload, output));
@ -66,11 +67,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
public void WriteTextMessage(int offset, int chunkSize, byte[] encoded, string payload) public void WriteTextMessage(int offset, int chunkSize, byte[] encoded, string payload)
{ {
var message = Encoding.UTF8.GetBytes(payload); var message = Encoding.UTF8.GetBytes(payload);
var output = new ArrayOutput(chunkSize); var output = new MemoryStream();
if (offset > 0) if (offset > 0)
{ {
output.Advance(offset); output.Seek(offset, SeekOrigin.Begin);
} }
Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output)); Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output));

View File

@ -19,9 +19,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
public void ReadMessage(byte[] encoded, string payload) public void ReadMessage(byte[] encoded, string payload)
{ {
var parser = new BinaryMessageParser(); var parser = new BinaryMessageParser();
var reader = new BytesReader(encoded); ReadOnlySpan<byte> span = encoded.AsSpan();
Assert.True(parser.TryParseMessage(ref reader, out var message)); Assert.True(parser.TryParseMessage(ref span, out var message));
Assert.Equal(reader.Index, encoded.Length); Assert.Equal(0, span.Length);
Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray()); Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray());
} }
@ -32,18 +32,14 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
public void ReadBinaryMessage(byte[] encoded, byte[] payload) public void ReadBinaryMessage(byte[] encoded, byte[] payload)
{ {
var parser = new BinaryMessageParser(); var parser = new BinaryMessageParser();
var reader = new BytesReader(encoded); ReadOnlySpan<byte> span = encoded.AsSpan();
Assert.True(parser.TryParseMessage(ref reader, out var message)); Assert.True(parser.TryParseMessage(ref span, out var message));
Assert.Equal(reader.Index, encoded.Length); Assert.Equal(0, span.Length);
Assert.Equal(payload, message.ToArray()); Assert.Equal(payload, message.ToArray());
} }
[Theory] [Fact]
[InlineData(0)] // No chunking public void ReadMultipleMessages()
[InlineData(4)]
[InlineData(8)]
[InlineData(256)]
public void ReadMultipleMessages(int chunkSize)
{ {
var encoded = new byte[] var encoded = new byte[]
{ {
@ -53,16 +49,15 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, /* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
}; };
var parser = new BinaryMessageParser(); var parser = new BinaryMessageParser();
var buffer = encoded.ToChunkedReadOnlyBytes(chunkSize); ReadOnlySpan<byte> span = encoded.AsSpan();
var reader = new BytesReader(buffer);
var messages = new List<byte[]>(); var messages = new List<byte[]>();
while (parser.TryParseMessage(ref reader, out var message)) while (parser.TryParseMessage(ref span, out var message))
{ {
messages.Add(message.ToArray()); messages.Add(message.ToArray());
} }
Assert.Equal(encoded.Length, reader.Index); Assert.Equal(0, span.Length);
Assert.Equal(2, messages.Count); Assert.Equal(2, messages.Count);
Assert.Equal(new byte[0], messages[0]); Assert.Equal(new byte[0], messages[0]);
@ -75,9 +70,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
public void ReadIncompleteMessages(byte[] encoded) public void ReadIncompleteMessages(byte[] encoded)
{ {
var parser = new BinaryMessageParser(); var parser = new BinaryMessageParser();
var reader = new BytesReader(new ReadOnlyBytes(encoded)); ReadOnlySpan<byte> span = encoded.AsSpan();
Assert.False(parser.TryParseMessage(ref reader, out var message)); Assert.False(parser.TryParseMessage(ref span, out var message));
Assert.Equal(encoded.Length, reader.Index); Assert.Equal(0, span.Length);
} }
} }
} }

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Xunit; using Xunit;
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
Encoding.UTF8.GetBytes("Hello,\r\nWorld!") Encoding.UTF8.GetBytes("Hello,\r\nWorld!")
}; };
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing var output = new MemoryStream();
foreach (var message in messages) foreach (var message in messages)
{ {
Assert.True(TextMessageFormatter.TryWriteMessage(message, output)); Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
public void WriteMessage(int chunkSize, string encoded, string payload) public void WriteMessage(int chunkSize, string encoded, string payload)
{ {
var message = Encoding.UTF8.GetBytes(payload); var message = Encoding.UTF8.GetBytes(payload);
var output = new ArrayOutput(chunkSize); // Use small chunks to test Advance/Enlarge and partial payload writing var output = new MemoryStream();
Assert.True(TextMessageFormatter.TryWriteMessage(message, output)); Assert.True(TextMessageFormatter.TryWriteMessage(message, output));

View File

@ -21,35 +21,28 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{ {
var parser = new TextMessageParser(); var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded); var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer.ToChunkedReadOnlyBytes(chunkSize)); ReadOnlySpan<byte> span = buffer.AsSpan();
Assert.True(parser.TryParseMessage(ref reader, out var message)); Assert.True(parser.TryParseMessage(ref span, out var message));
Assert.Equal(reader.Index, buffer.Length); Assert.Equal(0, span.Length);
Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray()); Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray());
} }
[Theory] [Fact]
[InlineData(0)] // Not chunked public void ReadMultipleMessages()
[InlineData(4)]
[InlineData(8)]
public void ReadMultipleMessages(int chunkSize)
{ {
const string encoded = "0:;14:Hello,\r\nWorld!;"; const string encoded = "0:;14:Hello,\r\nWorld!;";
var parser = new TextMessageParser(); var parser = new TextMessageParser();
var data = Encoding.UTF8.GetBytes(encoded); var data = Encoding.UTF8.GetBytes(encoded);
var buffer = chunkSize > 0 ? ReadOnlySpan<byte> span = data.AsSpan();
data.ToChunkedReadOnlyBytes(chunkSize) :
new ReadOnlyBytes(data);
var reader = new BytesReader(buffer);
var messages = new List<byte[]>(); var messages = new List<byte[]>();
while (parser.TryParseMessage(ref reader, out var message)) while (parser.TryParseMessage(ref span, out var message))
{ {
messages.Add(message.ToArray()); messages.Add(message.ToArray());
} }
Assert.Equal(reader.Index, Encoding.UTF8.GetByteCount(encoded)); Assert.Equal(0, span.Length);
Assert.Equal(2, messages.Count); Assert.Equal(2, messages.Count);
Assert.Equal(new byte[0], messages[0]); Assert.Equal(new byte[0], messages[0]);
@ -68,8 +61,8 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{ {
var parser = new TextMessageParser(); var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded); var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer); ReadOnlySpan<byte> span = buffer.AsSpan();
Assert.False(parser.TryParseMessage(ref reader, out _)); Assert.False(parser.TryParseMessage(ref span, out _));
} }
[Theory] [Theory]
@ -82,8 +75,11 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{ {
var parser = new TextMessageParser(); var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded); var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer); var ex = Assert.Throws<FormatException>(() =>
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, out _)); {
ReadOnlySpan<byte> span = buffer.AsSpan();
parser.TryParseMessage(ref span, out _);
});
Assert.Equal(expectedMessage, ex.Message); Assert.Equal(expectedMessage, ex.Message);
} }
@ -96,8 +92,12 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
// We need to include the ':' so that // We need to include the ':' so that
var buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' }; var buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' };
var reader = new BytesReader(buffer); var reader = new BytesReader(buffer);
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, out _)); var ex = Assert.Throws<FormatException>(() =>
Assert.Equal("Invalid length", ex.Message); {
ReadOnlySpan<byte> span = buffer.AsSpan();
parser.TryParseMessage(ref span, out _);
});
Assert.Equal("Invalid length: 'He<48>lo'", ex.Message);
} }
} }
} }

View File

@ -16,8 +16,6 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" /> <Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" />
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
<Compile Include="..\Common\ByteArrayExtensions.cs" Link="ByteArrayExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public async Task<string> SendInvocationAsync(string methodName, params object[] args) public async Task<string> SendInvocationAsync(string methodName, params object[] args)
{ {
var invocationId = GetInvocationId(); var invocationId = GetInvocationId();
var payload = await _protocol.WriteToArrayAsync(new InvocationMessage(invocationId, nonBlocking: false, target: methodName, arguments: args)); var payload = _protocol.WriteToArray(new InvocationMessage(invocationId, nonBlocking: false, target: methodName, arguments: args));
await Application.Output.WriteAsync(payload); await Application.Output.WriteAsync(payload);

View File

@ -8,7 +8,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
<Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" /> <Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" />
</ItemGroup> </ItemGroup>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Xunit; using Xunit;
@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
[InlineData("data: Hello\r\ndata: \r\n\r\n", "Hello\r\n")] [InlineData("data: Hello\r\ndata: \r\n\r\n", "Hello\r\n")]
public void WriteTextMessage(string encoded, string payload) public void WriteTextMessage(string encoded, string payload)
{ {
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing var output = new MemoryStream();
Assert.True(ServerSentEventsMessageFormatter.TryWriteMessage(Encoding.UTF8.GetBytes(payload), output)); Assert.True(ServerSentEventsMessageFormatter.TryWriteMessage(Encoding.UTF8.GetBytes(payload), output));
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray())); Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));