Simplify the JSON and protobuf protocols (#524)

- Remove message type and message format
- Updated tests
This commit is contained in:
David Fowler 2017-06-06 07:18:23 -10:00 committed by GitHub
parent 38efde7b50
commit 59a8f5f3b5
36 changed files with 215 additions and 686 deletions

View File

@ -1,5 +1,4 @@
import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters"
import { Message, MessageType } from "../Microsoft.AspNetCore.SignalR.Client.TS/Message";
describe("Text Message Formatter", () => {
it("should return empty array on empty input", () => {
@ -7,9 +6,9 @@ describe("Text Message Formatter", () => {
expect(messages).toEqual([]);
});
([
["0:T:;", [new Message(MessageType.Text, "")]],
["5:T:Hello;", [new Message(MessageType.Text, "Hello")]],
] as [[string, Message[]]]).forEach(([payload, expected_messages]) => {
["0:;", [""]],
["5:Hello;", ["Hello"]],
] as [[string, string[]]]).forEach(([payload, expected_messages]) => {
it(`should parse '${payload}' correctly`, () => {
let messages = TextMessageFormat.parse(payload);
expect(messages).toEqual(expected_messages);
@ -18,16 +17,13 @@ describe("Text Message Formatter", () => {
([
["ABC", new Error("Invalid length: 'ABC'")],
["1:X:A;", new Error("Unknown type value: 'X'")],
["1:T:A;12ab34:", new Error("Invalid length: '12ab34'")],
["1:T:A;1:asdf:", new Error("Unknown type value: 'asdf'")],
["1:T:A;1::", new Error("Message is incomplete")],
["1:T:A;1:AB:", new Error("Message is incomplete")],
["1:T:A;5:T:A", new Error("Message is incomplete")],
["1:T:A;5:T:AB", new Error("Message is incomplete")],
["1:T:A;5:T:ABCDE", new Error("Message is incomplete")],
["1:T:A;5:X:ABCDE", new Error("Message is incomplete")],
["1:T:A;5:T:ABCDEF", new Error("Message missing trailer character")],
["1:A;12ab34:", new Error("Invalid length: '12ab34'")],
["1:A;1:", new Error("Message is incomplete")],
["1:A;1:AB:", new Error("Message missing trailer character")],
["1:A;5:A", new Error("Message is incomplete")],
["1:A;5:AB", new Error("Message is incomplete")],
["1:A;5:ABCDE", new Error("Message is incomplete")],
["1:A;5:ABCDEF", new Error("Message missing trailer character")],
] as [[string, Error]]).forEach(([payload, expected_error]) => {
it(`should fail to parse '${payload}'`, () => {
expect(() => TextMessageFormat.parse(payload)).toThrow(expected_error);

View File

@ -216,7 +216,7 @@ class TestConnection implements IConnection {
};
send(data: any): Promise<void> {
var invocation = TextMessageFormat.parse(data)[0].content.toString();
var invocation = TextMessageFormat.parse(data)[0];
this.lastInvocationId = JSON.parse(invocation).invocationId;
if (this.sentData) {
this.sentData.push(invocation);
@ -235,7 +235,7 @@ class TestConnection implements IConnection {
receive(data: any): void {
var payload = JSON.stringify(data);
this.onDataReceived(`${payload.length}:T:${payload};`);
this.onDataReceived(TextMessageFormat.write(payload));
}
onDataReceived: DataReceived;

View File

@ -1,10 +1,4 @@
import { Message, MessageType } from './Message';
let knownTypes = {
"T": MessageType.Text,
"B": MessageType.Binary
};

function splitAt(input: string, searchString: string, position: number): [string, number] {
let index = input.indexOf(searchString, position);
if (index < 0) {
@ -23,7 +17,7 @@ export namespace TextMessageFormat {
return input.length >= requiredLength;
}
function parseMessage(input: string, position: number): [number, Message] {
function parseMessage(input: string, position: number): [number, string] {
var offset = position;
// Read the length
@ -36,20 +30,11 @@ export namespace TextMessageFormat {
}
let length = Number.parseInt(lenStr);
// Required space is: 3 (type flag, ":", ";") + length (payload len)
if (!hasSpace(input, offset, 3 + length)) {
// Required space is: (";") + length (payload len)
if (!hasSpace(input, offset, 1 + length)) {
throw new Error("Message is incomplete");
}
// Read the type
var [typeStr, offset] = splitAt(input, ":", offset);
// Parse the type
var messageType = knownTypes[typeStr];
if (messageType === undefined) {
throw new Error(`Unknown type value: '${typeStr}'`);
}
// Read the payload
var payload = input.substr(offset, length);
offset += length;
@ -60,18 +45,14 @@ export namespace TextMessageFormat {
}
offset += 1;
if (messageType == MessageType.Binary) {
// We need to decode and put in an ArrayBuffer. Throw for now
// This will require our own Base64-decoder because the browser
// built-in one only decodes to strings and throws if invalid UTF-8
// characters are found.
throw new Error("TODO: Support for binary messages");
}
return [offset, new Message(messageType, payload)];
return [offset, payload];
}
export function parse(input: string): Message[] {
export function write(output: string): string {
return `${output.length}:${output};`;
}
export function parse(input: string): string[] {
if (input.length == 0) {
return []
}

View File

@ -72,9 +72,9 @@ export class HubConnection {
let messages = Formatters.TextMessageFormat.parse(data);
for (var i = 0; i < messages.length; ++i) {
console.log(`Received message: ${messages[i].content}`);
console.log(`Received message: ${messages[i]}`);
var message = JSON.parse(messages[i].content.toString());
var message = JSON.parse(messages[i]);
switch (message.type) {
case MessageType.Invocation:
this.InvokeClientMethod(<InvocationMessage>message);
@ -159,9 +159,8 @@ export class HubConnection {
}
});
//TODO: separate conversion to enable different data formats
let data = JSON.stringify(invocationDescriptor);
let message = `${data.length}:T:${data};`;
// TODO: separate conversion to enable different data formats
let message = this.framePayload(invocationDescriptor);
this.connection.send(message)
.catch(e => {
@ -192,8 +191,7 @@ export class HubConnection {
});
// TODO: separate conversion to enable different data formats
let data = JSON.stringify(invocationDescriptor);
let message = `${data.length}:T:${data};`;
let message = this.framePayload(invocationDescriptor);
this.connection.send(message)
.catch(e => {
@ -213,6 +211,11 @@ export class HubConnection {
this.connectionClosedCallback = callback;
}
private framePayload(invocationDescriptor: InvocationMessage): string {
let data = JSON.stringify(invocationDescriptor);
return Formatters.TextMessageFormat.write(data);
}
private createInvocation(methodName: string, args: any[]): InvocationMessage {
let id = this.id;
this.id++;

View File

@ -1,14 +0,0 @@
export enum MessageType {
Text,
Binary,
}
export class Message {
public type: MessageType;
public content: ArrayBuffer | string;
constructor(type: MessageType, content: ArrayBuffer | string) {
this.type = type;
this.content = content;
}
}

View File

@ -328,31 +328,24 @@ JSON payloads are wrapped in an outer message framing to support batching over v
The body will be formatted as below and encoded in UTF-8. Identifiers in square brackets `[]` indicate fields defined below, and parenthesis `()` indicate grouping.
```
([Length]:[Type]:[Body];)([Length]:[Type]:[Body];)... continues until end of the connection ...
([Length]:[Body];)([Length]:[Body];)... continues until end of the connection ...
```
* `[Length]` - Length of the `[Body]` field in bytes, specified as UTF-8 digits (`0`-`9`, terminated by `:`). If the body is a binary frame, this length indicates the number of Base64-encoded characters, not the number of bytes in the final decoded message!
* `[Type]` - A single-byte UTF-8 character indicating the type of the frame, see the list of frame Types below
* `[Body]` - The body of the message, the content of which depends upon the value of `[Type]`
The following values are valid for `[Type]`:
* `T` - Indicates a text frame, the `[Body]` contains UTF-8 encoded text data.
* `B` - Indicates a binary frame, the `[Body]` contains Base64 encoded binary data.
Note: If there is no `[Body]` for a frame, there does still need to be a `:` and `;` delimiting the body. So, for example, the following is an encoding of a single text frame `A`: `T1:T:A;`
Note: If there is no `[Body]` for a frame, there does still need to be a `:` and `;` delimiting the body. So, for example, the following is an encoding of a single text frame `A`: `1:A;`
For example, when sending the following frames (`\n` indicates the actual Line Feed character, not an escape sequence):
* Type=`Text`, "Hello\nWorld"
* Type=`Binary`, `0x01 0x02`
* Type=`Text`, `<<no body>>`
* "Hello\nWorld"
* `<<no body>>`
The encoding will be as follows
```
T11:T:Hello
World;4:B:AQI=;0:T:;
11:Hello
World;0:;
```
Note that the final frame still ends with the `;` terminator, and that since the body may contain `;`, newlines, etc., the length is specified in order to know exactly where the body ends.
@ -456,27 +449,21 @@ Protobuf payloads are wrapped in an outer message framing described below.
#### Binary encoding
```
([Length][Type][Body])([Length][Type][Body])... continues until end of the connection ...
([Length][Body])([Length][Body])... continues until end of the connection ...
```
* `[Length]` - A 64-bit integer in Network Byte Order (Big-endian) representing the length of the body in bytes
* `[Type]` - An 8-bit integer indicating the type of the message.
* `0x00` => `Text` - `[Body]` is UTF-8 encoded text data
* `0x01` => `Binary` - `[Body]` is raw binary data
* All other values are reserved and must **not** be used. An endpoint may reject a frame using any other value and terminate the connection.
* `[Body]` - The body of the message, exactly `[Length]` bytes in length. `Text` frames are always encoded in UTF-8.
* `[Body]` - The body of the message, exactly `[Length]` bytes in length.
For example, when sending the following frames (`\n` indicates the actual Line Feed character, not an escape sequence):
* Type=`Text`, "Hello\nWorld"
* Type=`Binary`, `0x01 0x02`
* "Hello\nWorld"
* `0x01 0x02`
The encoding will be as follows, as a list of binary digits in hex (text in parentheses `()` are comments). Whitespace and newlines are irrelevant and for illustration only.
```
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0B (start of frame; 64-bit integer value: 11)
0x00 (Type = Text)
0x68 0x65 0x6C 0x6C 0x6F 0x0A 0x77 0x6F 0x72 0x6C 0x64 (UTF-8 encoding of 'Hello\nWorld')
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 (start of frame; 64-bit integer value: 2)
0x01 (Type = Binary)
0x01 0x02 (body)
```

View File

@ -2,16 +2,10 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Formatting;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Channels;
@ -19,7 +13,6 @@ using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
@ -40,7 +33,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
private readonly CancellationTokenSource _connectionActive = new CancellationTokenSource();
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
private readonly ConcurrentDictionary<string, InvocationHandler> _handlers = new ConcurrentDictionary<string, InvocationHandler>();
private readonly MessageParser _parser = new MessageParser();
private int _nextId = 0;

View File

@ -6,45 +6,22 @@ using System.Buffers;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
internal static class BinaryMessageFormatter
public static class BinaryMessageFormatter
{
internal const byte TextTypeFlag = 0x00;
internal const byte BinaryTypeFlag = 0x01;
public static bool TryWriteMessage(Message message, IOutput output)
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output)
{
var typeIndicator = GetTypeIndicator(message.Type);
// Try to write the data
if (!output.TryWriteBigEndian((long)message.Payload.Length))
if (!output.TryWriteBigEndian((long)payload.Length))
{
return false;
}
if (!output.TryWriteBigEndian(typeIndicator))
{
return false;
}
if (!output.TryWrite(message.Payload))
if (!output.TryWrite(payload))
{
return false;
}
return true;
}
private static byte GetTypeIndicator(MessageType type)
{
switch (type)
{
case MessageType.Text:
return TextTypeFlag;
case MessageType.Binary:
return BinaryTypeFlag;
default:
throw new FormatException($"Invalid Message Type: {type}");
}
}
}
}

View File

@ -7,7 +7,7 @@ using System.Buffers;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
internal class BinaryMessageParser
public class BinaryMessageParser
{
private ParserState _state;
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
_state = default(ParserState);
}
public bool TryParseMessage(ref BytesReader buffer, out Message message)
public bool TryParseMessage(ref BytesReader buffer, out ReadOnlyBuffer<byte> payload)
{
if (_state.Length == null)
{
@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
if (lengthBuffer == null)
{
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
if (length.Length < sizeof(long))
{
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
@ -45,25 +45,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
_state.Length = (int)longLength;
}
if (_state.MessageType == null)
{
if (buffer.Unread.Length == 0)
{
message = default(Message);
return false;
}
var typeByte = buffer.Unread[0];
if (!TryParseType(typeByte, out var messageType))
{
throw new FormatException($"Unknown type value: 0x{typeByte:X}");
}
buffer.Advance(1);
_state.MessageType = messageType;
}
if (_state.Payload == null)
{
_state.Payload = new byte[_state.Length.Value];
@ -80,36 +61,19 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
if (_state.Read == _state.Payload.Length)
{
message = new Message(_state.Payload, _state.MessageType.Value);
payload = _state.Payload;
Reset();
return true;
}
// There's still more to read.
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
private static bool TryParseType(byte type, out MessageType messageType)
{
switch (type)
{
case BinaryMessageFormatter.TextTypeFlag:
messageType = MessageType.Text;
return true;
case BinaryMessageFormatter.BinaryTypeFlag:
messageType = MessageType.Binary;
return true;
default:
messageType = default(MessageType);
return false;
}
}
private struct ParserState
{
public int? Length;
public MessageType? MessageType;
public byte[] Payload;
public int Read;
}

View File

@ -1,31 +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;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
public class MessageFormatter
{
public static readonly char TextFormatIndicator = 'T';
public static readonly char BinaryFormatIndicator = 'B';
public static bool TryWriteMessage(Message message, IOutput output, MessageFormat format)
{
return format == MessageFormat.Text ?
TextMessageFormatter.TryWriteMessage(message, output) :
BinaryMessageFormatter.TryWriteMessage(message, output);
}
public static char GetFormatIndicator(MessageFormat messageFormat)
{
switch (messageFormat)
{
case MessageFormat.Text: return TextFormatIndicator;
case MessageFormat.Binary: return BinaryFormatIndicator;
default: throw new ArgumentException($"Invalid message format: {messageFormat}", nameof(messageFormat));
}
}
}
}

View File

@ -1,43 +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;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
public class MessageParser
{
private TextMessageParser _textParser = new TextMessageParser();
private BinaryMessageParser _binaryParser = new BinaryMessageParser();
public void Reset()
{
_textParser.Reset();
_binaryParser.Reset();
}
public bool TryParseMessage(ref BytesReader buffer, MessageFormat format, out Message message)
{
return format == MessageFormat.Text ?
_textParser.TryParseMessage(ref buffer, out message) :
_binaryParser.TryParseMessage(ref buffer, out message);
}
public static MessageFormat GetFormatFromIndicator(byte formatIndicator)
{
// Can't use switch because our "constants" are not consts, they're "static readonly" (which is good, because they are public)
if (formatIndicator == MessageFormatter.TextFormatIndicator)
{
return MessageFormat.Text;
}
if (formatIndicator == MessageFormatter.BinaryFormatIndicator)
{
return MessageFormat.Binary;
}
throw new ArgumentException($"Invalid message format: 0x{formatIndicator:X}", nameof(formatIndicator));
}
}
}

View File

@ -3,31 +3,21 @@
using System;
using System.Binary;
using System.Binary.Base64;
using System.Buffers;
using System.Text;
using System.Text.Formatting;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
internal static class TextMessageFormatter
public static class TextMessageFormatter
{
internal const char FieldDelimiter = ':';
internal const char MessageDelimiter = ';';
internal const char TextTypeFlag = 'T';
internal const char BinaryTypeFlag = 'B';
public static bool TryWriteMessage(Message message, IOutput output)
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output)
{
// Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary
var length = message.Payload.Length;
if (message.Type == MessageType.Binary)
{
length = Base64Encoder.ComputeEncodedLength(length);
}
// Get the type indicator
var typeIndicator = GetTypeIndicator(message.Type);
var length = payload.Length;
// Write the length as a string
output.Append(length, TextEncoder.Utf8);
@ -35,14 +25,8 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
// Write the field delimiter ':'
output.Append(FieldDelimiter, TextEncoder.Utf8);
// Write the type
output.Append(typeIndicator, TextEncoder.Utf8);
// Write the field delimiter ':'
output.Append(FieldDelimiter, TextEncoder.Utf8);
// Write the payload
if (!TryWritePayload(message, output, length))
if (!output.TryWrite(payload))
{
return false;
}
@ -51,31 +35,5 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
output.Append(MessageDelimiter, TextEncoder.Utf8);
return true;
}
private static bool TryWritePayload(Message message, IOutput output, int length)
{
// Payload
if (message.Type == MessageType.Binary)
{
// TODO: Base64 writer that works with IOutput would be amazing!
var arr = new byte[Base64Encoder.ComputeEncodedLength(message.Payload.Length)];
Base64.Encoder.Transform(message.Payload, arr, out _, out _);
return output.TryWrite(arr);
}
else
{
return output.TryWrite(message.Payload);
}
}
private static char GetTypeIndicator(MessageType type)
{
switch (type)
{
case MessageType.Text: return TextTypeFlag;
case MessageType.Binary: return BinaryTypeFlag;
default: throw new FormatException($"Invalid message type: {type}");
}
}
}
}

View File

@ -2,13 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Binary;
using System.Buffers;
using System.Text;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
internal class TextMessageParser
public class TextMessageParser
{
private ParserState _state;
@ -21,7 +20,7 @@ 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
/// exception if there is a format error in the provided data.
/// </summary>
public bool TryParseMessage(ref BytesReader buffer, out Message message)
public bool TryParseMessage(ref BytesReader buffer, out ReadOnlyBuffer<byte> payload)
{
while (buffer.Unread.Length > 0)
{
@ -30,34 +29,17 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
case ParsePhase.ReadingLength:
if (!TryReadLength(ref buffer))
{
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
break;
case ParsePhase.LengthComplete:
if (!TryReadDelimiter(ref buffer, TextMessageFormatter.FieldDelimiter, ParsePhase.ReadingType, "length"))
if (!TryReadDelimiter(ref buffer, TextMessageFormatter.FieldDelimiter, ParsePhase.ReadingPayload, "length"))
{
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
break;
case ParsePhase.ReadingType:
if (!TryReadType(ref buffer))
{
message = default(Message);
return false;
}
break;
case ParsePhase.TypeComplete:
if (!TryReadDelimiter(ref buffer, TextMessageFormatter.FieldDelimiter, ParsePhase.ReadingPayload, "type"))
{
message = default(Message);
return false;
}
break;
case ParsePhase.ReadingPayload:
ReadPayload(ref buffer);
@ -66,12 +48,12 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
case ParsePhase.PayloadComplete:
if (!TryReadDelimiter(ref buffer, TextMessageFormatter.MessageDelimiter, ParsePhase.ReadingPayload, "payload"))
{
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
// We're done!
message = new Message(_state.Payload, _state.MessageType);
payload = _state.Payload;
Reset();
return true;
default:
@ -79,7 +61,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
}
}
message = default(Message);
payload = default(ReadOnlyBuffer<byte>);
return false;
}
@ -129,23 +111,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
return true;
}
private bool TryReadType(ref BytesReader buffer)
{
if (buffer.Unread.Length == 0)
{
return false;
}
if (!TryParseType(buffer.Unread[0], out _state.MessageType))
{
throw new FormatException($"Unknown message type: '{(char)buffer.Unread[0]}'");
}
buffer.Advance(1);
_state.Phase = ParsePhase.TypeComplete;
return true;
}
private void ReadPayload(ref BytesReader buffer)
{
if (_state.Payload == null)
@ -155,11 +120,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
if (_state.Read == _state.Length)
{
if (_state.MessageType == MessageType.Binary)
{
_state.Payload = MessageFormatUtils.DecodePayload(_state.Payload);
}
_state.Phase = ParsePhase.PayloadComplete;
}
else
@ -172,27 +132,10 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
}
}
private static bool TryParseType(byte type, out MessageType messageType)
{
switch ((char)type)
{
case TextMessageFormatter.TextTypeFlag:
messageType = MessageType.Text;
return true;
case TextMessageFormatter.BinaryTypeFlag:
messageType = MessageType.Binary;
return true;
default:
messageType = default(MessageType);
return false;
}
}
private struct ParserState
{
public ParsePhase Phase;
public int Length;
public MessageType MessageType;
public byte[] Payload;
public int Read;
}
@ -201,8 +144,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
ReadingLength = 0,
LengthComplete,
ReadingType,
TypeComplete,
ReadingPayload,
PayloadComplete
}

View File

@ -5,7 +5,6 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -51,11 +50,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
var reader = new BytesReader(input.ToArray());
messages = new List<HubMessage>();
// This API has to change to return the amount consumed
foreach (var m in ParseSendBatch(ref reader, MessageFormat.Text))
var parser = new TextMessageParser();
while (parser.TryParseMessage(ref reader, out var payload))
{
// TODO: Need a span-native JSON parser!
using (var memoryStream = new MemoryStream(m.Payload))
using (var memoryStream = new MemoryStream(payload.ToArray()))
{
messages.Add(ParseMessage(memoryStream, binder));
}
@ -72,8 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
WriteMessage(message, memoryStream);
memoryStream.Flush();
var frame = new Message(memoryStream.ToArray(), MessageType.Text);
return MessageFormatter.TryWriteMessage(frame, output, MessageFormat.Text);
return TextMessageFormatter.TryWriteMessage(memoryStream.ToArray(), output);
}
}
@ -288,23 +286,5 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
return prop.Value<T>();
}
private List<Message> ParseSendBatch(ref BytesReader payload, MessageFormat messageFormat)
{
var messages = new List<Message>();
if (payload.Unread.Length == 0)
{
return messages;
}
// REVIEW: This needs a little work. We could probably new up exactly the right parser, if we tinkered with the inheritance hierarchy a bit.
var parser = new MessageParser();
while (parser.TryParseMessage(ref payload, messageFormat, out var message))
{
messages.Add(message);
}
return messages;
}
}
}

View File

@ -1,28 +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;
namespace Microsoft.AspNetCore.Sockets
{
public struct Message
{
public MessageType Type { get; }
// REVIEW: We need a better primitive to use here. Memory<byte> would be good,
// but @davidfowl has concerns about allocating OwnedMemory and how to dispose
// it properly
public byte[] Payload { get; }
public Message(byte[] payload, MessageType type)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
Type = type;
Payload = payload;
}
}
}

View File

@ -1,11 +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 Microsoft.AspNetCore.Sockets
{
public enum MessageType
{
Text,
Binary
}
}

View File

@ -83,7 +83,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
var request = new HttpRequestMessage(HttpMethod.Get, pollUrl);
request.Headers.UserAgent.Add(SendUtils.DefaultUserAgentHeader);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ContentTypes.BinaryContentType));
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();

View File

@ -56,7 +56,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
// Set the, now filled, stream as the content
request.Content = new StreamContent(memoryStream);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentTypes.GetContentType(MessageFormat.Binary));
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Sockets
{
public static class ContentTypes
{
public static readonly string TextContentType = "application/vnd.microsoft.aspnetcore.endpoint-messages.v1+text";
public static readonly string BinaryContentType = "application/vnd.microsoft.aspnetcore.endpoint-messages.v1+binary";
public static string GetContentType(MessageFormat messageFormat)
{
switch (messageFormat)
{
case MessageFormat.Text: return TextContentType;
case MessageFormat.Binary: return BinaryContentType;
default: throw new ArgumentException($"Invalid message format: {messageFormat}", nameof(messageFormat));
}
}
}
}

View File

@ -1,11 +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 Microsoft.AspNetCore.Sockets
{
public enum MessageFormat
{
Text,
Binary
}
}

View File

@ -35,11 +35,7 @@ namespace Microsoft.AspNetCore.Sockets.Transports
return;
}
var headers = context.Request.GetTypedHeaders();
var messageFormat = headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue(ContentTypes.BinaryContentType)) == true ?
MessageFormat.Binary :
MessageFormat.Text;
context.Response.ContentType = ContentTypes.GetContentType(messageFormat);
// REVIEW: What should the content type be?
var contentLength = 0;
var buffers = new List<byte[]>();

View File

@ -2,17 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Formatting;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.SignalR.Tests.Common;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
using Microsoft.Extensions.Logging;
using Moq;
using Moq.Protected;
@ -589,7 +585,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
{
content = "42";
}
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ContentTypes.TextContentType, content);
return ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -662,13 +658,5 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
}
}
}
private byte[] FormatMessageToArray(Message message, MessageFormat binary, int bufferSize = 1024)
{
var output = new ArrayOutput(bufferSize);
output.Append('B', TextEncoder.Utf8);
Assert.True(MessageFormatter.TryWriteMessage(message, output, binary));
return output.ToArray();
}
}
}

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
Assert.Equal("59:T:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage);
Assert.Equal("59:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage);
}
finally
{
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
Assert.Equal("59:T:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage);
Assert.Equal("59:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage);
// Complete the channel
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();

View File

@ -268,8 +268,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public int ParseCalls { get; private set; } = 0;
public int WriteCalls { get; private set; } = 0;
public MessageType MessageType => MessageType.Text;
public static MockHubProtocol ReturnOnParse(HubMessage parsed)
{
return new MockHubProtocol

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
@ -11,10 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Channels;
using Microsoft.AspNetCore.SignalR.Tests.Common;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.Extensions.Logging;
using Moq;
using Moq.Protected;
@ -227,7 +224,7 @@ namespace Microsoft.AspNetCore.Client.Tests
if (firstCall)
{
firstCall = false;
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ContentTypes.BinaryContentType, message1Payload);
return ResponseUtils.CreateResponse(HttpStatusCode.OK, message1Payload);
}
return ResponseUtils.CreateResponse(HttpStatusCode.NoContent);
@ -260,7 +257,6 @@ namespace Microsoft.AspNetCore.Client.Tests
// Check the provided request
Assert.Equal(2, sentRequests.Count);
Assert.Contains(ContentTypes.BinaryContentType, sentRequests[0].Headers.Accept.FirstOrDefault()?.ToString());
// Check the messages received
Assert.Equal(1, messages.Count);

View File

@ -4,24 +4,22 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Sockets;
namespace Microsoft.AspNetCore.Client.Tests
{
internal static class ResponseUtils
{
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) =>
CreateResponse(statusCode, ContentTypes.TextContentType, string.Empty);
CreateResponse(statusCode, string.Empty);
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string contentType, string payload) =>
CreateResponse(statusCode, contentType, new StringContent(payload));
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string payload) =>
CreateResponse(statusCode, new StringContent(payload));
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string contentType, byte[] payload) =>
CreateResponse(statusCode, contentType, new ByteArrayContent(payload));
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, byte[] payload) =>
CreateResponse(statusCode, new ByteArrayContent(payload));
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string contentType, HttpContent payload)
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload)
{
payload.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
return new HttpResponseMessage(statusCode)
{
Content = payload

View File

@ -10,7 +10,9 @@ using System.Threading.Tasks.Channels;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
@ -79,11 +81,18 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public Task ReceiveJsonMessage(object jsonObject)
{
var json = JsonConvert.SerializeObject(jsonObject, Formatting.None);
var bytes = Encoding.UTF8.GetBytes($"{json.Length}:T:{json};");
var bytes = FormatMessageToArray(Encoding.UTF8.GetBytes(json));
return _receivedMessages.Out.WriteAsync(bytes);
}
private byte[] FormatMessageToArray(byte[] message, int bufferSize = 1024)
{
var output = new ArrayOutput(1024);
Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
return output.ToArray();
}
private async Task ReceiveLoopAsync(CancellationToken token)
{
try

View File

@ -5,6 +5,8 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Xunit;
@ -134,10 +136,16 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
private static string Frame(string input)
{
input = $"{input.Length}:T:{input};";
return input;
var data = Encoding.UTF8.GetBytes(input);
return Encoding.UTF8.GetString(FormatMessageToArray(data));
}
private static byte[] FormatMessageToArray(byte[] message, int bufferSize = 1024)
{
var output = new ArrayOutput(1024);
Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
return output.ToArray();
}
private class CustomObject : IEquatable<CustomObject>
{

View File

@ -8,9 +8,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Protocol\**" />
<EmbeddedResource Remove="Protocol\**" />
<None Remove="Protocol\**" />
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -1,7 +1,6 @@
using System;
using System.Buffers;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests.Internal;
@ -11,9 +10,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
public class MessageParserBenchmark
{
private static readonly Random Random = new Random();
private readonly MessageParser _parser = new MessageParser();
private ReadOnlyBytes _input;
private byte[] _buffer;
private readonly TextMessageParser _textMessageParser = new TextMessageParser();
private readonly BinaryMessageParser _binaryMessageParser = new BinaryMessageParser();
private ReadOnlyBytes _binaryInput;
private ReadOnlyBytes _textInput;
[Params(32, 64)]
public int ChunkSize { get; set; }
@ -21,28 +21,45 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
[Params(64, 128)]
public int MessageLength { get; set; }
[Params(MessageFormat.Text, MessageFormat.Binary)]
public MessageFormat Format { get; set; }
[Setup]
public void Setup()
{
_buffer = new byte[MessageLength];
Random.NextBytes(_buffer);
var message = new Message(_buffer, MessageType.Binary);
var buffer = new byte[MessageLength];
Random.NextBytes(buffer);
var output = new ArrayOutput(MessageLength + 32);
if (!MessageFormatter.TryWriteMessage(message, output, Format))
if (!BinaryMessageFormatter.TryWriteMessage(buffer, output))
{
throw new InvalidOperationException("Failed to format message");
}
_input = output.ToArray().ToChunkedReadOnlyBytes(ChunkSize);
_binaryInput = output.ToArray().ToChunkedReadOnlyBytes(ChunkSize);
buffer = new byte[MessageLength];
Random.NextBytes(buffer);
output = new ArrayOutput(MessageLength + 32);
if (!TextMessageFormatter.TryWriteMessage(buffer, output))
{
throw new InvalidOperationException("Failed to format message");
}
_textInput = output.ToArray().ToChunkedReadOnlyBytes(ChunkSize);
}
[Benchmark]
public void SingleBinaryMessage()
{
var reader = new BytesReader(_input);
if (!_parser.TryParseMessage(ref reader, Format, out _))
var reader = new BytesReader(_binaryInput);
if (!_binaryMessageParser.TryParseMessage(ref reader, out _))
{
throw new InvalidOperationException("Failed to parse");
}
}
[Benchmark]
public void SingleTextMessage()
{
var reader = new BytesReader(_textInput);
if (!_textMessageParser.TryParseMessage(ref reader, out _))
{
throw new InvalidOperationException("Failed to parse");
}

View File

@ -3,7 +3,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Xunit;
@ -17,38 +17,35 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
var expectedEncoding = new byte[]
{
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* type: */ 0x01, // Binary
/* body: <empty> */
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E,
/* type: */ 0x00, // Text
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
};
var messages = new[]
{
MessageTestUtils.CreateMessage(new byte[0]),
MessageTestUtils.CreateMessage("Hello,\r\nWorld!",MessageType.Text)
new byte[0],
Encoding.UTF8.GetBytes("Hello,\r\nWorld!")
};
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
foreach (var message in messages)
{
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Binary));
Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output));
}
Assert.Equal(expectedEncoding, output.ToArray());
}
[Theory]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, new byte[0])]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(4, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, new byte[0])]
[InlineData(4, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, new byte[0])]
[InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(4, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])]
[InlineData(4, 8, 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, 0x00 }, new byte[0])]
[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)
{
var message = MessageTestUtils.CreateMessage(payload);
var output = new ArrayOutput(chunkSize);
if (offset > 0)
@ -56,20 +53,19 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
output.Advance(offset);
}
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Binary));
Assert.True(BinaryMessageFormatter.TryWriteMessage(payload, output));
Assert.Equal(encoded, output.ToArray().Slice(offset).ToArray());
}
[Theory]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, MessageType.Text, "")]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x41, 0x42, 0x43 }, MessageType.Text, "ABC")]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, MessageType.Text, "A\nR\rC\r\n;DEF")]
[InlineData(4, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, MessageType.Text, "")]
[InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, MessageType.Text, "")]
public void WriteTextMessage(int offset, int chunkSize, byte[] encoded, MessageType messageType, string payload)
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")]
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
[InlineData(4, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")]
[InlineData(0, 256, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")]
public void WriteTextMessage(int offset, int chunkSize, byte[] encoded, string payload)
{
var message = MessageTestUtils.CreateMessage(payload, messageType);
var message = Encoding.UTF8.GetBytes(payload);
var output = new ArrayOutput(chunkSize);
if (offset > 0)
@ -77,8 +73,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
output.Advance(offset);
}
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Binary));
Assert.True(BinaryMessageFormatter.TryWriteMessage(message, output));
Assert.Equal(encoded, output.ToArray().Slice(offset).ToArray());
}
}

View File

@ -4,8 +4,8 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests;
using Xunit;
namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
@ -13,30 +13,29 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
public class BinaryMessageParserTests
{
[Theory]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, MessageType.Text, "")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x41, 0x42, 0x43 }, MessageType.Text, "ABC")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, MessageType.Text, "A\nR\rC\r\n;DEF")]
public void ReadTextMessage(byte[] encoded, MessageType messageType, string payload)
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
public void ReadMessage(byte[] encoded, string payload)
{
var parser = new MessageParser();
var parser = new BinaryMessageParser();
var reader = new BytesReader(encoded);
Assert.True(parser.TryParseMessage(ref reader, MessageFormat.Binary, out var message));
Assert.True(parser.TryParseMessage(ref reader, out var message));
Assert.Equal(reader.Index, encoded.Length);
MessageTestUtils.AssertMessage(message, messageType, payload);
Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray());
}
[Theory]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, new byte[0])]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
public void ReadBinaryMessage(byte[] encoded, byte[] payload)
{
var parser = new MessageParser();
var parser = new BinaryMessageParser();
var reader = new BytesReader(encoded);
Assert.True(parser.TryParseMessage(ref reader, MessageFormat.Binary, out var message));
Assert.True(parser.TryParseMessage(ref reader, out var message));
Assert.Equal(reader.Index, encoded.Length);
MessageTestUtils.AssertMessage(message, MessageType.Binary, payload);
Assert.Equal(payload, message.ToArray());
}
[Theory]
@ -49,48 +48,35 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
var encoded = new byte[]
{
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* type: */ 0x01, // Binary
/* body: <empty> */
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E,
/* type: */ 0x00, // Text
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
};
var parser = new MessageParser();
var parser = new BinaryMessageParser();
var buffer = encoded.ToChunkedReadOnlyBytes(chunkSize);
var reader = new BytesReader(buffer);
var messages = new List<Message>();
while (parser.TryParseMessage(ref reader, MessageFormat.Binary, out var message))
var messages = new List<byte[]>();
while (parser.TryParseMessage(ref reader, out var message))
{
messages.Add(message);
messages.Add(message.ToArray());
}
Assert.Equal(encoded.Length, reader.Index);
Assert.Equal(2, messages.Count);
MessageTestUtils.AssertMessage(messages[0], MessageType.Binary, new byte[0]);
MessageTestUtils.AssertMessage(messages[1], MessageType.Text, "Hello,\r\nWorld!");
}
[Theory]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 }, "Unknown type value: 0x4")] // Invalid Type
public void ReadInvalidMessages(byte[] encoded, string message)
{
var parser = new MessageParser();
var reader = new BytesReader(new ReadOnlyBytes(encoded));
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, MessageFormat.Binary, out _));
Assert.Equal(message, ex.Message);
Assert.Equal(new byte[0], messages[0]);
Assert.Equal(Encoding.UTF8.GetBytes("Hello,\r\nWorld!"), messages[1]);
}
[Theory]
[InlineData(new byte[0])] // Empty
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })] // Just length
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00 })] // Not enough data for payload
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00 })] // Not enough data for payload
public void ReadIncompleteMessages(byte[] encoded)
{
var parser = new MessageParser();
var parser = new BinaryMessageParser();
var reader = new BytesReader(new ReadOnlyBytes(encoded));
Assert.False(parser.TryParseMessage(ref reader, MessageFormat.Binary, out var message));
Assert.False(parser.TryParseMessage(ref reader, out var message));
Assert.Equal(encoded.Length, reader.Index);
}
}

View File

@ -1,38 +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.Pipelines;
using System.Text;
using Xunit;
namespace Microsoft.AspNetCore.Sockets.Tests
{
internal static class MessageTestUtils
{
public static void AssertMessage(Message message, MessageType messageType, byte[] payload)
{
Assert.Equal(messageType, message.Type);
Assert.Equal(payload, message.Payload);
}
public static void AssertMessage(Message message, MessageType messageType, string payload)
{
Assert.Equal(messageType, message.Type);
Assert.Equal(payload, Encoding.UTF8.GetString(message.Payload));
}
public static Message CreateMessage(byte[] payload, MessageType type = MessageType.Binary)
{
return new Message(
payload,
type);
}
public static Message CreateMessage(string payload, MessageType type)
{
return new Message(
Encoding.UTF8.GetBytes(payload),
type);
}
}
}

View File

@ -214,14 +214,14 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{
get
{
yield return new object[] { "data: Shaolin\r\ndata: Fantastic\r\n\r\n", "Shaolin" + Environment.NewLine + " Fantastic", MessageType.Text };
yield return new object[] { "data: The\r\ndata: Get\r\ndata: Down\r\n\r\n", "The" + Environment.NewLine + "Get" + Environment.NewLine + "Down", MessageType.Text };
yield return new object[] { "data: Shaolin\r\ndata: Fantastic\r\n\r\n", "Shaolin" + Environment.NewLine + " Fantastic" };
yield return new object[] { "data: The\r\ndata: Get\r\ndata: Down\r\n\r\n", "The" + Environment.NewLine + "Get" + Environment.NewLine + "Down" };
}
}
[Theory]
[MemberData(nameof(MultilineMessages))]
public void ParseMessagesWithMultipleDataLines(string encodedMessage, string expectedMessage, MessageType expectedMessageType)
public void ParseMessagesWithMultipleDataLines(string encodedMessage, string expectedMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);

View File

@ -14,49 +14,33 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
[Fact]
public void WriteMultipleMessages()
{
const string expectedEncoding = "0:B:;14:T:Hello,\r\nWorld!;";
const string expectedEncoding = "0:;14:Hello,\r\nWorld!;";
var messages = new[]
{
MessageTestUtils.CreateMessage(new byte[0]),
MessageTestUtils.CreateMessage("Hello,\r\nWorld!",MessageType.Text),
new byte[0],
Encoding.UTF8.GetBytes("Hello,\r\nWorld!")
};
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
foreach (var message in messages)
{
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Text));
Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
}
Assert.Equal(expectedEncoding, Encoding.UTF8.GetString(output.ToArray()));
}
[Theory]
[InlineData(8, "0:B:;", new byte[0])]
[InlineData(8, "8:B:q83vEg==;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(8, "8:B:q83vEjQ=;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34 })]
[InlineData(8, "8:B:q83vEjRW;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56 })]
[InlineData(256, "8:B:q83vEjRW;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56 })]
public void WriteBinaryMessage(int chunkSize, string encoded, byte[] payload)
[InlineData(8, "0:;", "")]
[InlineData(8, "3:ABC;", "ABC")]
[InlineData(8, "11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")]
[InlineData(256, "11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")]
public void WriteMessage(int chunkSize, string encoded, string payload)
{
var message = MessageTestUtils.CreateMessage(payload);
var output = new ArrayOutput(chunkSize);
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Text));
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
}
[Theory]
[InlineData(8, "0:T:;", MessageType.Text, "")]
[InlineData(8, "3:T:ABC;", MessageType.Text, "ABC")]
[InlineData(8, "11:T:A\nR\rC\r\n;DEF;", MessageType.Text, "A\nR\rC\r\n;DEF")]
[InlineData(256, "11:T:A\nR\rC\r\n;DEF;", MessageType.Text, "A\nR\rC\r\n;DEF")]
public void WriteTextMessage(int chunkSize, string encoded, MessageType messageType, string payload)
{
var message = MessageTestUtils.CreateMessage(payload, messageType);
var message = Encoding.UTF8.GetBytes(payload);
var output = new ArrayOutput(chunkSize); // Use small chunks to test Advance/Enlarge and partial payload writing
Assert.True(MessageFormatter.TryWriteMessage(message, output, MessageFormat.Text));
Assert.True(TextMessageFormatter.TryWriteMessage(message, output));
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
}

View File

@ -6,7 +6,6 @@ using System.Buffers;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.AspNetCore.Sockets.Tests;
using Xunit;
namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
@ -14,37 +13,19 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
public class TextMessageParserTests
{
[Theory]
[InlineData(0, "0:T:;", MessageType.Text, "")]
[InlineData(0, "3:T:ABC;", MessageType.Text, "ABC")]
[InlineData(0, "11:T:A\nR\rC\r\n;DEF;", MessageType.Text, "A\nR\rC\r\n;DEF")]
[InlineData(4, "12:T:Hello, World;", MessageType.Text, "Hello, World")]
public void ReadTextMessage(int chunkSize, string encoded, MessageType messageType, string payload)
[InlineData(0, "0:;", "")]
[InlineData(0, "3:ABC;", "ABC")]
[InlineData(0, "11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")]
[InlineData(4, "12:Hello, World;", "Hello, World")]
public void ReadTextMessage(int chunkSize, string encoded, string payload)
{
var parser = new MessageParser();
var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer.ToChunkedReadOnlyBytes(chunkSize));
Assert.True(parser.TryParseMessage(ref reader, MessageFormat.Text, out var message));
Assert.True(parser.TryParseMessage(ref reader, out var message));
Assert.Equal(reader.Index, buffer.Length);
MessageTestUtils.AssertMessage(message, messageType, payload);
}
[Theory]
[InlineData("0:B:;", new byte[0])]
[InlineData("8:B:q83vEg==;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData("8:B:q83vEjQ=;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34 })]
[InlineData("8:B:q83vEjRW;", new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56 })]
public void ReadBinaryMessage(string encoded, byte[] payload)
{
var parser = new MessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer);
Assert.True(parser.TryParseMessage(ref reader, MessageFormat.Text, out var message));
Assert.Equal(reader.Index, buffer.Length);
MessageTestUtils.AssertMessage(message, MessageType.Binary, payload);
Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray());
}
[Theory]
@ -53,8 +34,8 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[InlineData(8)]
public void ReadMultipleMessages(int chunkSize)
{
const string encoded = "0:B:;14:T:Hello,\r\nWorld!;";
var parser = new MessageParser();
const string encoded = "0:;14:Hello,\r\nWorld!;";
var parser = new TextMessageParser();
var data = Encoding.UTF8.GetBytes(encoded);
var buffer = chunkSize > 0 ?
data.ToChunkedReadOnlyBytes(chunkSize) :
@ -62,17 +43,17 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
var reader = new BytesReader(buffer);
var messages = new List<Message>();
while (parser.TryParseMessage(ref reader, MessageFormat.Text, out var message))
var messages = new List<byte[]>();
while (parser.TryParseMessage(ref reader, out var message))
{
messages.Add(message);
messages.Add(message.ToArray());
}
Assert.Equal(reader.Index, Encoding.UTF8.GetByteCount(encoded));
Assert.Equal(2, messages.Count);
MessageTestUtils.AssertMessage(messages[0], MessageType.Binary, new byte[0]);
MessageTestUtils.AssertMessage(messages[1], MessageType.Text, "Hello,\r\nWorld!");
Assert.Equal(new byte[0], messages[0]);
Assert.Equal(Encoding.UTF8.GetBytes("Hello,\r\nWorld!"), messages[1]);
}
[Theory]
@ -81,45 +62,41 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[InlineData("1230450945")]
[InlineData("1:")]
[InlineData("10")]
[InlineData("5:T:A")]
[InlineData("5:T:ABCDE")]
[InlineData("5:A")]
[InlineData("5:ABCDE")]
public void ReadIncompleteMessages(string encoded)
{
var parser = new MessageParser();
var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer);
Assert.False(parser.TryParseMessage(ref reader, MessageFormat.Text, out _));
Assert.False(parser.TryParseMessage(ref reader, out _));
}
[Theory]
[InlineData("X:", "Invalid length: 'X'")]
[InlineData("5:X:ABCDEF", "Unknown message type: 'X'")]
[InlineData("1:asdf", "Unknown message type: 'a'")]
[InlineData("1::", "Unknown message type: ':'")]
[InlineData("1:AB:", "Unknown message type: 'A'")]
[InlineData("1:TA", "Missing delimiter ':' after type")]
[InlineData("1029348109238412903849023841290834901283409128349018239048102394:X:ABCDEF", "Invalid length: '1029348109238412903849023841290834901283409128349018239048102394'")]
[InlineData("1:asdf", "Missing delimiter ';' after payload")]
[InlineData("1029348109238412903849023841290834901283409128349018239048102394:ABCDEF", "Invalid length: '1029348109238412903849023841290834901283409128349018239048102394'")]
[InlineData("12ab34:", "Invalid length: '12ab34'")]
[InlineData("5:T:ABCDEF", "Missing delimiter ';' after payload")]
[InlineData("5:ABCDEF", "Missing delimiter ';' after payload")]
public void ReadInvalidMessages(string encoded, string expectedMessage)
{
var parser = new MessageParser();
var parser = new TextMessageParser();
var buffer = Encoding.UTF8.GetBytes(encoded);
var reader = new BytesReader(buffer);
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, MessageFormat.Text, out _));
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, out _));
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void ReadInvalidEncodedMessage()
{
var parser = new MessageParser();
var parser = new TextMessageParser();
// Invalid because first character is a UTF-8 "continuation" character
// We need to include the ':' so that
var buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' };
var reader = new BytesReader(buffer);
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, MessageFormat.Text, out _));
var ex = Assert.Throws<FormatException>(() => parser.TryParseMessage(ref reader, out _));
Assert.Equal("Invalid length", ex.Message);
}
}