Merge transport and hub protocols (#517)
* Merge transport and hub protocols - This change merges the transport and hub protocols into a single protocol. The idea being that sockets in a purely streaming layer that sends frames from the underlying transport. This makes things like TCP possible and doesn't impose a framing layer at the lowest level. This will make it possible to build servers like kestrel on top of the TCP layer. - The Message was removed from the lowest layer of the stack and pushed into the hubs layer. Hub invocations are framed with what was before the transport protocol. Connections also need to state upfront if they support binary or not. This will determine how data will be serialized to the specific connection. - Changed the SSE parser and writer to be strictly SSE without any of the transport protocol specific information. - To ensure we aren't using types in the wrong layers - Moved protocol logic into SignalR - Socket.Abstractions is now the root of the universe, Sockets.Common will likely be removed or turned into Sockets.Common.Http. - Move SSE parser to Sockets.Client and SSE writer into Sockets.Http - Moved tests into the appropriate test projects - Updated the spec
This commit is contained in:
parent
831fa72893
commit
38efde7b50
|
|
@ -52,8 +52,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Signal
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Sockets.Common", "src\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj", "{F3EFFD9F-DD85-48A2-9B11-83A133ECC099}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Sockets.Common.Tests", "test\Microsoft.AspNetCore.Sockets.Common.Tests\Microsoft.AspNetCore.Sockets.Common.Tests.csproj", "{B0D32729-48AA-4841-B52A-2A61B60EED61}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Client.TS", "client-ts\Microsoft.AspNetCore.SignalR.Client.TS\Microsoft.AspNetCore.SignalR.Client.TS.csproj", "{333526A4-633B-491A-AC45-CC62A0012D1C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{6CEC3DC2-5B01-45A8-8F0D-8531315DA90B}"
|
||||
|
|
@ -153,10 +151,6 @@ Global
|
|||
{F3EFFD9F-DD85-48A2-9B11-83A133ECC099}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3EFFD9F-DD85-48A2-9B11-83A133ECC099}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3EFFD9F-DD85-48A2-9B11-83A133ECC099}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B0D32729-48AA-4841-B52A-2A61B60EED61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B0D32729-48AA-4841-B52A-2A61B60EED61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B0D32729-48AA-4841-B52A-2A61B60EED61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B0D32729-48AA-4841-B52A-2A61B60EED61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{333526A4-633B-491A-AC45-CC62A0012D1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{333526A4-633B-491A-AC45-CC62A0012D1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{333526A4-633B-491A-AC45-CC62A0012D1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
@ -203,7 +197,6 @@ Global
|
|||
{354335AB-CEE9-4434-A641-78058F6EFE56} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{F3EFFD9F-DD85-48A2-9B11-83A133ECC099} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{B0D32729-48AA-4841-B52A-2A61B60EED61} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{333526A4-633B-491A-AC45-CC62A0012D1C} = {3A76C5A2-79ED-49BC-8BDC-6A3A766FFA1B}
|
||||
{6CEC3DC2-5B01-45A8-8F0D-8531315DA90B} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{96771B3F-4D18-41A7-A75B-FF38E76AAC89} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TextMessageFormat, ServerSentEventsFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters"
|
||||
import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters"
|
||||
import { Message, MessageType } from "../Microsoft.AspNetCore.SignalR.Client.TS/Message";
|
||||
|
||||
describe("Text Message Formatter", () => {
|
||||
|
|
@ -7,11 +7,8 @@ describe("Text Message Formatter", () => {
|
|||
expect(messages).toEqual([]);
|
||||
});
|
||||
([
|
||||
["T0:T:;", [new Message(MessageType.Text, "")]],
|
||||
["T0:C:;", [new Message(MessageType.Close, "")]],
|
||||
["T0:E:;", [new Message(MessageType.Error, "")]],
|
||||
["T5:T:Hello;", [new Message(MessageType.Text, "Hello")]],
|
||||
["T5:T:Hello;5:C:World;5:E:Error;", [new Message(MessageType.Text, "Hello"), new Message(MessageType.Close, "World"), new Message(MessageType.Error, "Error")]],
|
||||
["0:T:;", [new Message(MessageType.Text, "")]],
|
||||
["5:T:Hello;", [new Message(MessageType.Text, "Hello")]],
|
||||
] as [[string, Message[]]]).forEach(([payload, expected_messages]) => {
|
||||
it(`should parse '${payload}' correctly`, () => {
|
||||
let messages = TextMessageFormat.parse(payload);
|
||||
|
|
@ -20,46 +17,20 @@ describe("Text Message Formatter", () => {
|
|||
});
|
||||
|
||||
([
|
||||
["TABC", new Error("Invalid length: 'ABC'")],
|
||||
["X1:T:A", new Error("Unsupported message format: 'X'")],
|
||||
["T1:T:A;12ab34:", new Error("Invalid length: '12ab34'")],
|
||||
["T1:T:A;1:asdf:", new Error("Unknown type value: 'asdf'")],
|
||||
["T1:T:A;1::", new Error("Message is incomplete")],
|
||||
["T1:T:A;1:AB:", new Error("Message is incomplete")],
|
||||
["T1:T:A;5:T:A", new Error("Message is incomplete")],
|
||||
["T1:T:A;5:T:AB", new Error("Message is incomplete")],
|
||||
["T1:T:A;5:T:ABCDE", new Error("Message is incomplete")],
|
||||
["T1:T:A;5:X:ABCDE", new Error("Message is incomplete")],
|
||||
["T1:T:A;5:T:ABCDEF", new Error("Message missing trailer character")],
|
||||
["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")],
|
||||
] as [[string, Error]]).forEach(([payload, expected_error]) => {
|
||||
it(`should fail to parse '${payload}'`, () => {
|
||||
expect(() => TextMessageFormat.parse(payload)).toThrow(expected_error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Server-Sent Events Formatter", () => {
|
||||
([
|
||||
["", new Error("Message is missing header")],
|
||||
["A", new Error("Unknown type value: 'A'")],
|
||||
["BOO\r\nBlarg", new Error("Unknown type value: 'BOO'")]
|
||||
] as [string, Error][]).forEach(([payload, expected_error]) => {
|
||||
it(`should fail to parse '${payload}`, () => {
|
||||
expect(() => ServerSentEventsFormat.parse(payload)).toThrow(expected_error);
|
||||
});
|
||||
});
|
||||
|
||||
([
|
||||
["T\r\nTest", new Message(MessageType.Text, "Test")],
|
||||
["C\r\nTest", new Message(MessageType.Close, "Test")],
|
||||
["E\r\nTest", new Message(MessageType.Error, "Test")],
|
||||
["T", new Message(MessageType.Text, "")],
|
||||
["T\r\n", new Message(MessageType.Text, "")],
|
||||
["T\r\nFoo\r\nBar", new Message(MessageType.Text, "Foo\r\nBar")]
|
||||
] as [string, Message][]).forEach(([payload, expected_message]) => {
|
||||
it(`should parse '${payload}' correctly`, () => {
|
||||
let message = ServerSentEventsFormat.parse(payload);
|
||||
expect(message).toEqual(expected_message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,7 @@ import { HubConnection } from "../Microsoft.AspNetCore.SignalR.Client.TS/HubConn
|
|||
import { DataReceived, ConnectionClosed } from "../Microsoft.AspNetCore.SignalR.Client.TS/Common"
|
||||
import { TransportType, ITransport } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports"
|
||||
import { Observer } from "../Microsoft.AspNetCore.SignalR.Client.TS/Observable"
|
||||
import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters"
|
||||
|
||||
import { asyncit as it, captureException } from './JasmineUtils';
|
||||
|
||||
|
|
@ -215,12 +216,13 @@ class TestConnection implements IConnection {
|
|||
};
|
||||
|
||||
send(data: any): Promise<void> {
|
||||
this.lastInvocationId = JSON.parse(data).invocationId;
|
||||
var invocation = TextMessageFormat.parse(data)[0].content.toString();
|
||||
this.lastInvocationId = JSON.parse(invocation).invocationId;
|
||||
if (this.sentData) {
|
||||
this.sentData.push(data);
|
||||
this.sentData.push(invocation);
|
||||
}
|
||||
else {
|
||||
this.sentData = [data];
|
||||
this.sentData = [invocation];
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
|
@ -232,7 +234,8 @@ class TestConnection implements IConnection {
|
|||
};
|
||||
|
||||
receive(data: any): void {
|
||||
this.onDataReceived(JSON.stringify(data));
|
||||
var payload = JSON.stringify(data);
|
||||
this.onDataReceived(`${payload.length}:T:${payload};`);
|
||||
}
|
||||
|
||||
onDataReceived: DataReceived;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
let knownTypes = {
|
||||
"T": MessageType.Text,
|
||||
"B": MessageType.Binary,
|
||||
"C": MessageType.Close,
|
||||
"E": MessageType.Error
|
||||
"B": MessageType.Binary
|
||||
};
|
||||
|
||||
function splitAt(input: string, searchString: string, position: number): [string, number] {
|
||||
|
|
@ -16,42 +14,6 @@ function splitAt(input: string, searchString: string, position: number): [string
|
|||
return [left, index + searchString.length];
|
||||
}
|
||||
|
||||
export namespace ServerSentEventsFormat {
|
||||
export function parse(input: string): Message {
|
||||
// The SSE protocol is pretty simple. We just look at the first line for the type, and then process the remainder.
|
||||
// Binary messages require Base64-decoding and ArrayBuffer support, just like in the other formats below
|
||||
|
||||
if (input.length == 0) {
|
||||
throw new Error("Message is missing header");
|
||||
}
|
||||
|
||||
let [header, offset] = splitAt(input, "\n", 0);
|
||||
let payload = input.substring(offset);
|
||||
|
||||
// Just in case the header used CRLF as the line separator, carve it off
|
||||
if (header.endsWith('\r')) {
|
||||
header = header.substr(0, header.length - 1);
|
||||
}
|
||||
|
||||
// Parse the header
|
||||
var messageType = knownTypes[header];
|
||||
if (messageType === undefined) {
|
||||
throw new Error(`Unknown type value: '${header}'`);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// Create the message
|
||||
return new Message(messageType, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TextMessageFormat {
|
||||
const InvalidPayloadError = new Error("Invalid text message payload");
|
||||
const LengthRegex = /^[0-9]+$/;
|
||||
|
|
@ -114,12 +76,8 @@ export namespace TextMessageFormat {
|
|||
return []
|
||||
}
|
||||
|
||||
if (input[0] != 'T') {
|
||||
throw new Error(`Unsupported message format: '${input[0]}'`);
|
||||
}
|
||||
|
||||
let messages = [];
|
||||
var offset = 1;
|
||||
var offset = 0;
|
||||
while (offset < input.length) {
|
||||
let message;
|
||||
[offset, message] = parseMessage(input, offset);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { IConnection } from "./IConnection"
|
|||
import { Connection } from "./Connection"
|
||||
import { TransportType } from "./Transports"
|
||||
import { Subject, Observable } from "./Observable"
|
||||
import * as Formatters from "./Formatters";
|
||||
|
||||
const enum MessageType {
|
||||
Invocation = 1,
|
||||
|
|
@ -67,25 +68,32 @@ export class HubConnection {
|
|||
return;
|
||||
}
|
||||
|
||||
var message = JSON.parse(data);
|
||||
switch (message.type) {
|
||||
case MessageType.Invocation:
|
||||
this.InvokeClientMethod(<InvocationMessage>message);
|
||||
break;
|
||||
case MessageType.Result:
|
||||
case MessageType.Completion:
|
||||
let callback = this.callbacks.get(message.invocationId);
|
||||
if (callback != null) {
|
||||
callback(message);
|
||||
// Parse the messages
|
||||
let messages = Formatters.TextMessageFormat.parse(data);
|
||||
|
||||
if (message.type == MessageType.Completion) {
|
||||
this.callbacks.delete(message.invocationId);
|
||||
for (var i = 0; i < messages.length; ++i) {
|
||||
console.log(`Received message: ${messages[i].content}`);
|
||||
|
||||
var message = JSON.parse(messages[i].content.toString());
|
||||
switch (message.type) {
|
||||
case MessageType.Invocation:
|
||||
this.InvokeClientMethod(<InvocationMessage>message);
|
||||
break;
|
||||
case MessageType.Result:
|
||||
case MessageType.Completion:
|
||||
let callback = this.callbacks.get(message.invocationId);
|
||||
if (callback != null) {
|
||||
callback(message);
|
||||
|
||||
if (message.type == MessageType.Completion) {
|
||||
this.callbacks.delete(message.invocationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Invalid message type: " + data);
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
console.log("Invalid message type: " + data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +146,7 @@ export class HubConnection {
|
|||
if (completionMessage.error) {
|
||||
subject.error(new Error(completionMessage.error));
|
||||
}
|
||||
else if(completionMessage.result) {
|
||||
else if (completionMessage.result) {
|
||||
subject.error(new Error("Server provided a result in a completion response to a streamed invocation."));
|
||||
}
|
||||
else {
|
||||
|
|
@ -152,7 +160,10 @@ export class HubConnection {
|
|||
});
|
||||
|
||||
//TODO: separate conversion to enable different data formats
|
||||
this.connection.send(JSON.stringify(invocationDescriptor))
|
||||
let data = JSON.stringify(invocationDescriptor);
|
||||
let message = `${data.length}:T:${data};`;
|
||||
|
||||
this.connection.send(message)
|
||||
.catch(e => {
|
||||
subject.error(e);
|
||||
this.callbacks.delete(invocationDescriptor.invocationId);
|
||||
|
|
@ -180,8 +191,11 @@ export class HubConnection {
|
|||
}
|
||||
});
|
||||
|
||||
//TODO: separate conversion to enable different data formats
|
||||
this.connection.send(JSON.stringify(invocationDescriptor))
|
||||
// TODO: separate conversion to enable different data formats
|
||||
let data = JSON.stringify(invocationDescriptor);
|
||||
let message = `${data.length}:T:${data};`;
|
||||
|
||||
this.connection.send(message)
|
||||
.catch(e => {
|
||||
reject(e);
|
||||
this.callbacks.delete(invocationDescriptor.invocationId);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
export enum MessageType {
|
||||
Text,
|
||||
Binary,
|
||||
Close,
|
||||
Error
|
||||
}
|
||||
|
||||
export class Message {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { DataReceived, TransportClosed } from "./Common"
|
||||
import { IHttpClient } from "./HttpClient"
|
||||
import * as Formatters from "./Formatters";
|
||||
|
||||
export enum TransportType {
|
||||
WebSockets,
|
||||
|
|
@ -103,20 +102,15 @@ export class ServerSentEventsTransport implements ITransport {
|
|||
try {
|
||||
eventSource.onmessage = (e: MessageEvent) => {
|
||||
if (this.onDataReceived) {
|
||||
// Parse the message
|
||||
let message;
|
||||
try {
|
||||
message = Formatters.ServerSentEventsFormat.parse(e.data);
|
||||
console.log(`(SSE transport) data received: ${message.content}`);
|
||||
console.log(`(SSE transport) data received: ${e.data}`);
|
||||
this.onDataReceived(e.data);
|
||||
} catch (error) {
|
||||
if (this.onClosed) {
|
||||
this.onClosed(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: pass the whole message object along
|
||||
this.onDataReceived(message.content);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -187,22 +181,15 @@ export class LongPollingTransport implements ITransport {
|
|||
pollXhr.onload = () => {
|
||||
if (pollXhr.status == 200) {
|
||||
if (this.onDataReceived) {
|
||||
// Parse the messages
|
||||
let messages;
|
||||
try {
|
||||
messages = Formatters.TextMessageFormat.parse(pollXhr.response);
|
||||
console.log(`(LongPolling transport) data received: ${pollXhr.response}`);
|
||||
this.onDataReceived(pollXhr.response);
|
||||
} catch (error) {
|
||||
if (this.onClosed) {
|
||||
this.onClosed(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach((message) => {
|
||||
console.log(`(LongPolling transport) data received: ${message.content}`);
|
||||
// TODO: pass the whole message object along
|
||||
this.onDataReceived(message.content)
|
||||
});
|
||||
}
|
||||
this.poll(url);
|
||||
}
|
||||
|
|
@ -256,6 +243,5 @@ const headers = new Map<string, string>();
|
|||
headers.set("Content-Type", "application/vnd.microsoft.aspnetcore.endpoint-messages.v1+text");
|
||||
|
||||
async function send(httpClient: IHttpClient, url: string, data: any): Promise<void> {
|
||||
let message = `T${data.length.toString()}:T:${data};`;
|
||||
await httpClient.post(url, message, headers);
|
||||
await httpClient.post(url, data, headers);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ namespace ClientSample
|
|||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
connection.Received += (data, format) => Console.WriteLine($"{Encoding.UTF8.GetString(data)}");
|
||||
connection.Received += data => Console.WriteLine($"{Encoding.UTF8.GetString(data)}");
|
||||
connection.Closed += e => cts.Cancel();
|
||||
|
||||
await connection.StartAsync();
|
||||
|
|
@ -63,7 +63,7 @@ namespace ClientSample
|
|||
break;
|
||||
}
|
||||
|
||||
await connection.SendAsync(Encoding.UTF8.GetBytes(line), MessageType.Text, cts.Token);
|
||||
await connection.SendAsync(Encoding.UTF8.GetBytes(line), cts.Token);
|
||||
}
|
||||
}
|
||||
catch (AggregateException aex) when (aex.InnerExceptions.All(e => e is OperationCanceledException))
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace SocialWeather
|
|||
|
||||
public void OnConnectedAsync(ConnectionContext connection)
|
||||
{
|
||||
connection.Metadata[ConnectionMetadataNames.Format] = "json";
|
||||
connection.Metadata["format"] = "json";
|
||||
_connectionList.Add(connection);
|
||||
}
|
||||
|
||||
|
|
@ -35,17 +35,12 @@ namespace SocialWeather
|
|||
{
|
||||
foreach (var connection in _connectionList)
|
||||
{
|
||||
var formatter = _formatterResolver.GetFormatter<T>(connection.Metadata.Get<string>(ConnectionMetadataNames.Format));
|
||||
var context = connection.GetHttpContext();
|
||||
var formatter = _formatterResolver.GetFormatter<T>(connection.Metadata.Get<string>("format"));
|
||||
var ms = new MemoryStream();
|
||||
await formatter.WriteAsync(data, ms);
|
||||
|
||||
var context = (HttpContext)connection.Metadata[ConnectionMetadataNames.HttpContext];
|
||||
var format =
|
||||
string.Equals(context.Request.Query["format"], "binary", StringComparison.OrdinalIgnoreCase)
|
||||
? MessageType.Binary
|
||||
: MessageType.Text;
|
||||
|
||||
connection.Transport.Output.TryWrite(new Message(ms.ToArray(), format));
|
||||
connection.Transport.Output.TryWrite(ms.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ namespace SocialWeather
|
|||
|
||||
while (await connection.Transport.Input.WaitToReadAsync())
|
||||
{
|
||||
if (connection.Transport.Input.TryRead(out var message))
|
||||
if (connection.Transport.Input.TryRead(out var buffer))
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
await stream.WriteAsync(message.Payload, 0, message.Payload.Length);
|
||||
await stream.WriteAsync(buffer, 0, buffer.Length);
|
||||
stream.Position = 0;
|
||||
var weatherReport = await formatter.ReadAsync(stream);
|
||||
await _lifetimeManager.SendToAllAsync(weatherReport);
|
||||
|
|
|
|||
|
|
@ -23,13 +23,12 @@ namespace SocketsSample.EndPoints
|
|||
{
|
||||
while (await connection.Transport.Input.WaitToReadAsync())
|
||||
{
|
||||
Message message;
|
||||
if (connection.Transport.Input.TryRead(out message))
|
||||
if (connection.Transport.Input.TryRead(out var buffer))
|
||||
{
|
||||
// We can avoid the copy here but we'll deal with that later
|
||||
var text = Encoding.UTF8.GetString(message.Payload);
|
||||
var text = Encoding.UTF8.GetString(buffer);
|
||||
text = $"{connection.ConnectionId}: {text}";
|
||||
await Broadcast(Encoding.UTF8.GetBytes(text), message.Type);
|
||||
await Broadcast(Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,18 +42,16 @@ namespace SocketsSample.EndPoints
|
|||
|
||||
private Task Broadcast(string text)
|
||||
{
|
||||
return Broadcast(Encoding.UTF8.GetBytes(text), MessageType.Text);
|
||||
return Broadcast(Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
private Task Broadcast(byte[] payload, MessageType format)
|
||||
private Task Broadcast(byte[] payload)
|
||||
{
|
||||
var tasks = new List<Task>(Connections.Count);
|
||||
|
||||
foreach (var c in Connections)
|
||||
{
|
||||
tasks.Add(c.Transport.Output.WriteAsync(new Message(
|
||||
payload,
|
||||
format)));
|
||||
tasks.Add(c.Transport.Output.WriteAsync(payload));
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SocketsSample.EndPoints;
|
||||
using SocketsSample.Hubs;
|
||||
|
||||
|
|
|
|||
|
|
@ -321,6 +321,42 @@ Example - The following `Completion` message is a protocol error because it has
|
|||
|
||||
Items in the arguments array within the `Invocation` message type, as well as the `item` value of the `StreamItem` message and the `result` value of the `Completion` message, encode values which have meaning to each particular Binder. A general guideline for encoding/decoding these values is provided in the "Type Mapping" section at the end of this document, but Binders should provide configuration to applications to allow them to customize these mappings. These mappings need not be self-describing, because when decoding the value, the Binder is expected to know the destination type (by looking up the definition of the method indicated by the Target).
|
||||
|
||||
JSON payloads are wrapped in an outer message framing to support batching over various transports and to ease the parsing.
|
||||
|
||||
#### Text-based encoding
|
||||
|
||||
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]` - 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;`
|
||||
|
||||
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>>`
|
||||
|
||||
The encoding will be as follows
|
||||
|
||||
```
|
||||
T11:T:Hello
|
||||
World;4:B:AQI=;0:T:;
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Protocol Buffers (ProtoBuf) Encoding
|
||||
|
||||
In order to support ProtoBuf, an application must provide a [ProtoBuf service definition](https://developers.google.com/protocol-buffers/docs/proto3) for the Hub. However, implementations may automatically generate these definitions from reflection information, if the underlying platform supports this. For example, the .NET implementation will attempt to generate service definitions for methods that use only simple primitive and enumerated types. The service definition provides a description of how to encode the arguments and return value for the call. For example, consider the following C# method:
|
||||
|
|
@ -414,3 +450,33 @@ Below are some sample type mappings between JSON/ProtoBuf types and the .NET cli
|
|||
| `IEnumerable<T>` | `Array` | `repeated` |
|
||||
| custom `enum` | `Number` | `uint64` |
|
||||
| custom `struct` or `class` | `Object` | Requires an explicit .proto file definition |
|
||||
|
||||
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]` - 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.
|
||||
|
||||
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`
|
||||
|
||||
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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,24 +7,15 @@ This document describes the protocols used by the three ASP.NET Endpoint Transpo
|
|||
A transport is required to have the following attributes:
|
||||
|
||||
1. Duplex - Able to send messages from Server to Client and from Client to Server
|
||||
1. Frame-based - Messages have fixed length (as opposed to streaming, where data is just pushed throughout the connection)
|
||||
1. Frame Metadata - Able to encode one piece of Frame Metadata
|
||||
* `Type` - Either `Binary` or `Text`.
|
||||
1. Binary-safe - Able to transmit arbitrary binary data, regardless of content
|
||||
1. Text-safe - Able to transmit arbitrary text data, preserving the content. Line-endings must be preserved **but may be converted to a different format**. For example `\r\n` may be converted to `\n`. This is due to quirks in some transports (Server Sent Events). If the exact line-ending needs to be preserved, the data should be sent as a `Binary` message.
|
||||
|
||||
Multi-frame messages (where a message is split into multiple frames) may not overlap and all frames **must** have the same `Type` value (`Text` or `Binary`). It is an error to change the `Type` flag mid-message and the endpoint may terminate the connection if that occurs.
|
||||
|
||||
The only transport which fully implements the duplex requirement is WebSockets, the others are "half-transports" which implement one end of the duplex connection. They are used in combination to achieve a duplex connection.
|
||||
|
||||
Throughout this document, the term `[endpoint-base]` is used to refer to the route assigned to a particular end point. The term `[connection-id]` is used to refer to the connection ID provided by the `OPTIONS [endpoint-base]` request.
|
||||
|
||||
**NOTE on errors:** In all error cases, by default, the detailed exception message is **never** provided; a short description string may be provided. However, an application developer may elect to allow detailed exception messages to be emitted, which should only be used in the `Development` environment. Unexpected errors are communicated by HTTP `500 Server Error` status codes or WebSockets `1008 Policy Violation` close frames; in these cases the connection should be considered to be terminated.
|
||||
|
||||
## `Close` and `Error` frames
|
||||
|
||||
For the Long-Polling and Server-Sent events transports, there are two additional frame types: `Close` and `Error`. These are used to communicate completion and errors to the other end of the transport. Both can have optional **text** bodies, with descriptive text. Neither of these frame types are communicated to the application directly however, they manifest in terminating the channel and optionally (in the case of `Error`) yielding an exception.
|
||||
|
||||
## WebSockets (Full Duplex)
|
||||
|
||||
The WebSockets transport is unique in that it is full duplex, and a persistent connection that can be established in a single operation. As a result, the client is not required to use the `OPTIONS [endpoint-base]` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata.
|
||||
|
|
@ -71,140 +62,20 @@ foo: boz
|
|||
|
||||
In the first event, the value of `baz` would be `boz\nbiz\nflarg`, due to the concatenation behavior above. Full details can be found in the spec linked above.
|
||||
|
||||
In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata. The frame body starts on the **second** line of the `data` field value. The first line has the following format (Identifiers in square brackets `[]` indicate fields defined below):
|
||||
|
||||
```
|
||||
[Type]
|
||||
```
|
||||
|
||||
* `[Type]` is a single UTF-8 character representing the type of the frame; `T` indicates Text, `B` indicates Binary, `E` indicates an error, `C` indicates that the server is terminating its end of the connection.
|
||||
|
||||
If the `[Type]` field is `T`, the remaining lines of the `data` field contain the value, in UTF-8 text. If the `[Type]` field is `B`, the remaining lines of the `data` field contain Base64-encoded binary data. Any `\n` characters in Binary frames are removed before Base64-decoding. However, servers should avoid line breaks in the Base64-encoded data. If the `[Type]` field is `E`, the remaining lines of the `data` field may contain a textual description of the error. Also, the connection will be terminated immediately following an `E` frame. If the `[Type]` field is `C`, the remaining lines of the `data` field may contain a textual description of the reason for closing.
|
||||
|
||||
If the SSE response ends **without** a `C` or `E` frame, the client should immediately attempt to reestablish the connection. However, the protocol provides **no** guarantees that any messages that have not been seen will be retransmitted.
|
||||
In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata.
|
||||
|
||||
TBD: Keep Alive - Should it be done at this level?
|
||||
|
||||
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=`Close`, `<<no body>>`
|
||||
|
||||
The encoding will be as follows
|
||||
|
||||
```
|
||||
data: T
|
||||
data: Hello
|
||||
data: World
|
||||
|
||||
data: B
|
||||
data: AQI=
|
||||
|
||||
data: C
|
||||
|
||||
[Connection Terminated at this point]
|
||||
```
|
||||
|
||||
This transport will buffer incomplete frames sent by the server until the full message is available and then send the message in a single frame.
|
||||
|
||||
## Long Polling (Server-to-Client only)
|
||||
|
||||
Long Polling is a server-to-client half-transport, so it is always paired with HTTP Post. It requires a connection already be established using the `OPTIONS [endpoint-base]` request.
|
||||
|
||||
Long Polling requires that the client poll the server for new messages. Unlike traditional polling, if there are no messages available, the server will simply block the request waiting for messages to be dispatched. At some point, the server, client or an upstream proxy will likely terminate the connection, at which point the client should immediately re-send the request. Long Polling is the only transport that allows a "reconnection" where a new request can be received while the server believes an existing request is in process. This can happen because of a time out. When this happens, the existing request is immediately terminated with status code `204 No Content`. Any messages which have already been written to the existing request will be flushed and considered sent.
|
||||
|
||||
Since there is such a long round-trip-time for messages, given that the client must issue a request before the server can transmit a message back, Long Polling responses contain batches of multiple messages. Also, in order to support browsers which do not support XHR2, which provides the ability to read binary data, there are two different modes for the polling transport.
|
||||
Long Polling requires that the client poll the server for new messages. Unlike traditional polling, if there is no data available, the server will simply wait for messages to be dispatched. At some point, the server, client or an upstream proxy will likely terminate the connection, at which point the client should immediately re-send the request. Long Polling is the only transport that allows a "reconnection" where a new request can be received while the server believes an existing request is in process. This can happen because of a time out. When this happens, the existing request is immediately terminated with status code `204 No Content`. Any messages which have already been written to the existing request will be flushed and considered sent.
|
||||
|
||||
A Poll is established by sending an HTTP GET request to `[endpoint-base]` with the following query string parameters
|
||||
|
||||
* `connectionId` (Required) - The Connection ID of the destination connection.
|
||||
|
||||
The following headers are also supported:
|
||||
|
||||
* `Accept: application/vnd.microsoft.aspnetcore.endpoint-messages.v1+binary` - indicates if the client supports raw binary data in responses.
|
||||
* `Accept: application/vnd.microsoft.aspnetcore.endpoint-messages.v1+text` - indicates if the client only supports text in responses.
|
||||
|
||||
If the Accept header doesn't specify one of the above formats, `application/vnd.microsoft.aspnetcore.endpoint-messages.v1+text` is assumed.
|
||||
|
||||
When messages are available, the server responds with a body in one of the two formats below (depending upon the value of the `Accept` header). The response may be chunked, as per the chunked encoding part of the HTTP spec.
|
||||
When data is available, the server responds with a body in one of the two formats below (depending upon the value of the `Accept` header). The response may be chunked, as per the chunked encoding part of the HTTP spec.
|
||||
|
||||
If the `connectionId` parameter is missing, a `400 Bad Request` response is returned. If there is no connection with the ID specified in `connectionId`, a `404 Not Found` response is returned.
|
||||
|
||||
### Text-based encoding
|
||||
|
||||
The body will be formatted as below and encoded in UTF-8. The `Content-Type` response header is set to `application/vnd.microsoft.aspnetcore.endpoint-messages.v1+text`. Identifiers in square brackets `[]` indicate fields defined below, and parenthesis `()` indicate grouping.
|
||||
|
||||
```
|
||||
T([Length]:[Type]:[Body];)([Length]:[Type]:[Body];)... continues until end of the response body ...
|
||||
```
|
||||
|
||||
* `[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.
|
||||
* `E` - Indicates an error frame, the `[Body]` contains an optional UTF-8 encoded error description. The connection is immediately closed after processing this frame.
|
||||
* `C` - Indicates a close frame, the `[Body]` contains an optional UTF-8 encoded description of the reason for closing. The connection is immediately closed after processing this frame.
|
||||
|
||||
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` followed by a single close frame: `T1:T:A;0:C:;`
|
||||
|
||||
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=`Close`, `<<no body>>`
|
||||
|
||||
The encoding will be as follows
|
||||
|
||||
```
|
||||
T11:T:Hello
|
||||
World;4:B:AQI=;0:C:;
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
This transport will buffer incomplete frames sent by the server until the full message is available and then send the message in a single frame.
|
||||
|
||||
### Binary encoding
|
||||
|
||||
In JavaScript/Browser clients, this encoding requires XHR2 (or similar HTTP request functionality which allows binary data) and TypedArray support.
|
||||
|
||||
The body is encoded as follows. The `Content-Type` response header is set to `application/vnd.microsoft.aspnetcore.endpoint-messages.v1+binary`. Identifiers in square brackets `[]` indicate fields defined below, and parenthesis `()` indicate grouping. Other symbols indicate ASCII-encoded text in the stream
|
||||
|
||||
```
|
||||
B([Length][Type][Body])([Length][Type][Body])... continues until end of the response body ...
|
||||
```
|
||||
|
||||
* `[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
|
||||
* `0x02` => `Error` - `[Body]` is UTF-8 encoded error message
|
||||
* `0x03` => `Close` - `[Body]` is empty
|
||||
* 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` and `Error` frames are always encoded in UTF-8.
|
||||
|
||||
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=`Close`
|
||||
|
||||
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.
|
||||
```
|
||||
0x66 (ASCII 'B')
|
||||
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)
|
||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (start of frame; 64-bit integer value: 0)
|
||||
0x03 (Type = Close)
|
||||
```
|
||||
|
||||
This transport will buffer incomplete frames sent by the server until the full message is available and then send the message in a single frame.
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@
|
|||
// 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;
|
||||
|
|
@ -13,6 +19,7 @@ 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;
|
||||
|
|
@ -33,6 +40,7 @@ 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;
|
||||
|
||||
|
|
@ -162,7 +170,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
|
||||
_logger.LogInformation("Sending Invocation '{invocationId}'", invocationMessage.InvocationId);
|
||||
|
||||
await _connection.SendAsync(payload, _protocol.MessageType, irq.CancellationToken);
|
||||
await _connection.SendAsync(payload, irq.CancellationToken);
|
||||
_logger.LogInformation("Sending Invocation '{invocationId}' complete", invocationMessage.InvocationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -173,41 +181,45 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
}
|
||||
}
|
||||
|
||||
private void OnDataReceived(byte[] data, MessageType messageType)
|
||||
private void OnDataReceived(byte[] data)
|
||||
{
|
||||
var message = _protocol.ParseMessage(data, _binder);
|
||||
|
||||
InvocationRequest irq;
|
||||
switch (message)
|
||||
if (_protocol.TryParseMessages(data, _binder, out var messages))
|
||||
{
|
||||
case InvocationMessage invocation:
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
foreach (var message in messages)
|
||||
{
|
||||
InvocationRequest irq;
|
||||
switch (message)
|
||||
{
|
||||
var argsList = string.Join(", ", invocation.Arguments.Select(a => a.GetType().FullName));
|
||||
_logger.LogTrace("Received Invocation '{invocationId}': {methodName}({args})", invocation.InvocationId, invocation.Target, argsList);
|
||||
case InvocationMessage invocation:
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
var argsList = string.Join(", ", invocation.Arguments.Select(a => a.GetType().FullName));
|
||||
_logger.LogTrace("Received Invocation '{invocationId}': {methodName}({args})", invocation.InvocationId, invocation.Target, argsList);
|
||||
}
|
||||
DispatchInvocation(invocation, _connectionActive.Token);
|
||||
break;
|
||||
case CompletionMessage completion:
|
||||
if (!TryRemoveInvocation(completion.InvocationId, out irq))
|
||||
{
|
||||
_logger.LogWarning("Dropped unsolicited Completion message for invocation '{invocationId}'", completion.InvocationId);
|
||||
return;
|
||||
}
|
||||
DispatchInvocationCompletion(completion, irq);
|
||||
irq.Dispose();
|
||||
break;
|
||||
case StreamItemMessage streamItem:
|
||||
// Complete the invocation with an error, we don't support streaming (yet)
|
||||
if (!TryGetInvocation(streamItem.InvocationId, out irq))
|
||||
{
|
||||
_logger.LogWarning("Dropped unsolicited Stream Item message for invocation '{invocationId}'", streamItem.InvocationId);
|
||||
return;
|
||||
}
|
||||
DispatchInvocationStreamItemAsync(streamItem, irq);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown message type: {message.GetType().FullName}");
|
||||
}
|
||||
DispatchInvocation(invocation, _connectionActive.Token);
|
||||
break;
|
||||
case CompletionMessage completion:
|
||||
if (!TryRemoveInvocation(completion.InvocationId, out irq))
|
||||
{
|
||||
_logger.LogWarning("Dropped unsolicited Completion message for invocation '{invocationId}'", completion.InvocationId);
|
||||
return;
|
||||
}
|
||||
DispatchInvocationCompletion(completion, irq);
|
||||
irq.Dispose();
|
||||
break;
|
||||
case StreamItemMessage streamItem:
|
||||
// Complete the invocation with an error, we don't support streaming (yet)
|
||||
if (!TryGetInvocation(streamItem.InvocationId, out irq))
|
||||
{
|
||||
_logger.LogWarning("Dropped unsolicited Stream Item message for invocation '{invocationId}'", streamItem.InvocationId);
|
||||
return;
|
||||
}
|
||||
DispatchInvocationStreamItemAsync(streamItem, irq);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown message type: {message.GetType().FullName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<Description>Client for ASP.NET Core SignalR</Description>
|
||||
<TargetFramework>netstandard1.3</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;signalr</PackageTags>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
{
|
||||
internal const byte TextTypeFlag = 0x00;
|
||||
internal const byte BinaryTypeFlag = 0x01;
|
||||
internal const byte ErrorTypeFlag = 0x02;
|
||||
internal const byte CloseTypeFlag = 0x03;
|
||||
|
||||
public static bool TryWriteMessage(Message message, IOutput output)
|
||||
{
|
||||
|
|
@ -44,10 +42,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
return TextTypeFlag;
|
||||
case MessageType.Binary:
|
||||
return BinaryTypeFlag;
|
||||
case MessageType.Close:
|
||||
return CloseTypeFlag;
|
||||
case MessageType.Error:
|
||||
return ErrorTypeFlag;
|
||||
default:
|
||||
throw new FormatException($"Invalid Message Type: {type}");
|
||||
}
|
||||
|
|
@ -100,12 +100,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
case BinaryMessageFormatter.BinaryTypeFlag:
|
||||
messageType = MessageType.Binary;
|
||||
return true;
|
||||
case BinaryMessageFormatter.CloseTypeFlag:
|
||||
messageType = MessageType.Close;
|
||||
return true;
|
||||
case BinaryMessageFormatter.ErrorTypeFlag:
|
||||
messageType = MessageType.Error;
|
||||
return true;
|
||||
default:
|
||||
messageType = default(MessageType);
|
||||
return false;
|
||||
|
|
@ -11,9 +11,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
public static readonly char TextFormatIndicator = 'T';
|
||||
public static readonly char BinaryFormatIndicator = 'B';
|
||||
|
||||
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 bool TryWriteMessage(Message message, IOutput output, MessageFormat format)
|
||||
{
|
||||
return format == MessageFormat.Text ?
|
||||
|
|
@ -21,16 +18,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
BinaryMessageFormatter.TryWriteMessage(message, output);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public static char GetFormatIndicator(MessageFormat messageFormat)
|
||||
{
|
||||
switch (messageFormat)
|
||||
|
|
@ -39,21 +39,5 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
|
||||
throw new ArgumentException($"Invalid message format: 0x{formatIndicator:X}", nameof(formatIndicator));
|
||||
}
|
||||
|
||||
public static MessageFormat GetFormatFromContentType(string contentType)
|
||||
{
|
||||
// Can't use switch because our "constants" are not consts, they're "static readonly" (which is good, because they are public)
|
||||
if (string.Equals(contentType, MessageFormatter.TextContentType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MessageFormat.Text;
|
||||
}
|
||||
|
||||
if (string.Equals(contentType, MessageFormatter.BinaryContentType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MessageFormat.Binary;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Invalid Content-Type: '{contentType}'", nameof(contentType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
internal const char TextTypeFlag = 'T';
|
||||
internal const char BinaryTypeFlag = 'B';
|
||||
|
||||
internal const char CloseTypeFlag = 'C';
|
||||
internal const char ErrorTypeFlag = 'E';
|
||||
|
||||
public static bool TryWriteMessage(Message message, IOutput output)
|
||||
{
|
||||
// Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary
|
||||
|
|
@ -77,8 +74,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
{
|
||||
case MessageType.Text: return TextTypeFlag;
|
||||
case MessageType.Binary: return BinaryTypeFlag;
|
||||
case MessageType.Close: return CloseTypeFlag;
|
||||
case MessageType.Error: return ErrorTypeFlag;
|
||||
default: throw new FormatException($"Invalid message type: {type}");
|
||||
}
|
||||
}
|
||||
|
|
@ -182,12 +182,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
case TextMessageFormatter.BinaryTypeFlag:
|
||||
messageType = MessageType.Binary;
|
||||
return true;
|
||||
case TextMessageFormatter.CloseTypeFlag:
|
||||
messageType = MessageType.Close;
|
||||
return true;
|
||||
case TextMessageFormatter.ErrorTypeFlag:
|
||||
messageType = MessageType.Error;
|
||||
return true;
|
||||
default:
|
||||
messageType = default(MessageType);
|
||||
return false;
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||
{
|
||||
public interface IHubProtocol
|
||||
{
|
||||
MessageType MessageType { get; }
|
||||
|
||||
HubMessage ParseMessage(ReadOnlySpan<byte> input, IInvocationBinder binder);
|
||||
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages);
|
||||
|
||||
bool TryWriteMessage(HubMessage message, IOutput output);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
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;
|
||||
|
||||
|
|
@ -28,8 +30,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
// ONLY to be used for application payloads (args, return values, etc.)
|
||||
private JsonSerializer _payloadSerializer;
|
||||
|
||||
public MessageType MessageType => MessageType.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="JsonHubProtocol"/> using the specified <see cref="JsonSerializer"/>
|
||||
/// to serialize application payloads (arguments, results, etc.). The serialization of the outer protocol can
|
||||
|
|
@ -46,13 +46,22 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
_payloadSerializer = payloadSerializer;
|
||||
}
|
||||
|
||||
public HubMessage ParseMessage(ReadOnlySpan<byte> input, IInvocationBinder binder)
|
||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages)
|
||||
{
|
||||
// TODO: Need a span-native JSON parser!
|
||||
using (var memoryStream = new MemoryStream(input.ToArray()))
|
||||
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))
|
||||
{
|
||||
return ParseMessage(memoryStream, binder);
|
||||
// TODO: Need a span-native JSON parser!
|
||||
using (var memoryStream = new MemoryStream(m.Payload))
|
||||
{
|
||||
messages.Add(ParseMessage(memoryStream, binder));
|
||||
}
|
||||
}
|
||||
|
||||
return messages.Count > 0;
|
||||
}
|
||||
|
||||
public bool TryWriteMessage(HubMessage message, IOutput output)
|
||||
|
|
@ -63,7 +72,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
WriteMessage(message, memoryStream);
|
||||
memoryStream.Flush();
|
||||
|
||||
return output.TryWrite(memoryStream.ToArray());
|
||||
var frame = new Message(memoryStream.ToArray(), MessageType.Text);
|
||||
return MessageFormatter.TryWriteMessage(frame, output, MessageFormat.Text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,5 +288,23 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
public enum MessageType
|
||||
{
|
||||
Text,
|
||||
Binary,
|
||||
Close,
|
||||
Error
|
||||
Binary
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<Description>Common serialiation primitives for SignalR Clients Servers</Description>
|
||||
<TargetFramework>netstandard1.3</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;signalr</PackageTags>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ using System;
|
|||
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.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -301,11 +304,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
|
|||
{
|
||||
var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol);
|
||||
var data = await protocol.WriteToArrayAsync(hubMessage);
|
||||
var message = new Message(data, protocol.MessageType);
|
||||
|
||||
while (await connection.Transport.Output.WaitToWriteAsync())
|
||||
{
|
||||
if (connection.Transport.Output.TryWrite(message))
|
||||
if (connection.Transport.Output.TryWrite(data))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipelines.Text.Primitives;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
|
|
@ -110,11 +115,10 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
var protocol = connection.Metadata.Get<IHubProtocol>(HubConnectionMetadataNames.HubProtocol);
|
||||
var payload = await protocol.WriteToArrayAsync(hubMessage);
|
||||
var message = new Message(payload, protocol.MessageType);
|
||||
|
||||
while (await connection.Transport.Output.WaitToWriteAsync())
|
||||
{
|
||||
if (connection.Transport.Output.TryWrite(message))
|
||||
if (connection.Transport.Output.TryWrite(payload))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,21 @@
|
|||
// 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.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipelines.Text.Primitives;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Channels;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -157,27 +162,31 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
while (await connection.Transport.Input.WaitToReadAsync(cts.Token))
|
||||
{
|
||||
while (connection.Transport.Input.TryRead(out var incomingMessage))
|
||||
while (connection.Transport.Input.TryRead(out var buffer))
|
||||
{
|
||||
var hubMessage = protocol.ParseMessage(incomingMessage.Payload, this);
|
||||
|
||||
switch (hubMessage)
|
||||
if (protocol.TryParseMessages(buffer, this, out var hubMessages))
|
||||
{
|
||||
case InvocationMessage invocationMessage:
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
foreach (var hubMessage in hubMessages)
|
||||
{
|
||||
switch (hubMessage)
|
||||
{
|
||||
_logger.LogDebug("Received hub invocation: {invocation}", invocationMessage);
|
||||
case InvocationMessage invocationMessage:
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Received hub invocation: {invocation}", invocationMessage);
|
||||
}
|
||||
|
||||
// Don't wait on the result of execution, continue processing other
|
||||
// incoming messages on this connection.
|
||||
var ignore = ProcessInvocation(connection, protocol, invocationMessage, cts, completion);
|
||||
break;
|
||||
|
||||
// Other kind of message we weren't expecting
|
||||
default:
|
||||
_logger.LogError("Received unsupported message of type '{messageType}'", hubMessage.GetType().FullName);
|
||||
throw new NotSupportedException($"Received unsupported message: {hubMessage}");
|
||||
}
|
||||
|
||||
// Don't wait on the result of execution, continue processing other
|
||||
// incoming messages on this connection.
|
||||
var ignore = ProcessInvocation(connection, protocol, invocationMessage, cts, completion);
|
||||
break;
|
||||
|
||||
// Other kind of message we weren't expecting
|
||||
default:
|
||||
_logger.LogError("Received unsupported message of type '{messageType}'", hubMessage.GetType().FullName);
|
||||
throw new NotSupportedException($"Received unsupported message: {hubMessage}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,8 +222,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
|
||||
private async Task Execute(ConnectionContext connection, IHubProtocol protocol, InvocationMessage invocationMessage)
|
||||
{
|
||||
HubMethodDescriptor descriptor;
|
||||
if (!_methods.TryGetValue(invocationMessage.Target, out descriptor))
|
||||
if (!_methods.TryGetValue(invocationMessage.Target, out var descriptor))
|
||||
{
|
||||
// Send an error to the client. Then let the normal completion process occur
|
||||
_logger.LogError("Unknown hub method '{method}'", invocationMessage.Target);
|
||||
|
|
@ -229,11 +237,10 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
private async Task SendMessageAsync(ConnectionContext connection, IHubProtocol protocol, HubMessage hubMessage)
|
||||
{
|
||||
var payload = await protocol.WriteToArrayAsync(hubMessage);
|
||||
var message = new Message(payload, protocol.MessageType);
|
||||
|
||||
while (await connection.Transport.Output.WaitToWriteAsync())
|
||||
{
|
||||
if (connection.Transport.Output.TryWrite(message))
|
||||
if (connection.Transport.Output.TryWrite(payload))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<PackageTags>aspnetcore;signalr</PackageTags>
|
||||
<EnableApiCheck>false</EnableApiCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
public abstract ConnectionMetadata Metadata { get; }
|
||||
|
||||
// TEMPORARY
|
||||
public abstract IChannelConnection<Message> Transport { get; set; }
|
||||
public abstract IChannelConnection<byte[]> Transport { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
// on the same task
|
||||
private TaskCompletionSource<object> _disposeTcs = new TaskCompletionSource<object>();
|
||||
|
||||
public DefaultConnectionContext(string id, IChannelConnection<Message> transport, IChannelConnection<Message> application)
|
||||
public DefaultConnectionContext(string id, IChannelConnection<byte[]> transport, IChannelConnection<byte[]> application)
|
||||
{
|
||||
Transport = transport;
|
||||
Application = application;
|
||||
|
|
@ -43,9 +43,9 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
|
||||
public override ConnectionMetadata Metadata { get; } = new ConnectionMetadata();
|
||||
|
||||
public IChannelConnection<Message> Application { get; }
|
||||
public IChannelConnection<byte[]> Application { get; }
|
||||
|
||||
public override IChannelConnection<Message> Transport { get; set; }
|
||||
public override IChannelConnection<byte[]> Transport { get; set; }
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,9 +15,4 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -13,26 +13,26 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
public class Connection: IConnection
|
||||
public class Connection : IConnection
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private volatile int _connectionState = ConnectionState.Initial;
|
||||
private volatile IChannelConnection<Message, SendMessage> _transportChannel;
|
||||
private volatile IChannelConnection<byte[], SendMessage> _transportChannel;
|
||||
private HttpClient _httpClient;
|
||||
private volatile ITransport _transport;
|
||||
private volatile Task _receiveLoopTask;
|
||||
private TaskCompletionSource<object> _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private TaskQueue _eventQueue = new TaskQueue();
|
||||
|
||||
private ReadableChannel<Message> Input => _transportChannel.Input;
|
||||
private ReadableChannel<byte[]> Input => _transportChannel.Input;
|
||||
private WritableChannel<SendMessage> Output => _transportChannel.Output;
|
||||
|
||||
public Uri Url { get; }
|
||||
|
||||
public event Action Connected;
|
||||
public event Action<byte[], MessageType> Received;
|
||||
public event Action<byte[]> Received;
|
||||
public event Action<Exception> Closed;
|
||||
|
||||
public Connection(Uri url)
|
||||
|
|
@ -208,10 +208,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private async Task StartTransport(Uri connectUrl)
|
||||
{
|
||||
var applicationToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationSide = new ChannelConnection<SendMessage, Message>(applicationToTransport, transportToApplication);
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationSide = new ChannelConnection<SendMessage, byte[]>(applicationToTransport, transportToApplication);
|
||||
|
||||
_transportChannel = new ChannelConnection<Message, SendMessage>(transportToApplication, applicationToTransport);
|
||||
_transportChannel = new ChannelConnection<byte[], SendMessage>(transportToApplication, applicationToTransport);
|
||||
|
||||
// Start the transport, giving it one end of the pipeline
|
||||
try
|
||||
|
|
@ -237,11 +237,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
_logger.LogDebug("Message received but connection is not connected. Skipping raising Received event.");
|
||||
// drain
|
||||
Input.TryRead(out Message ignore);
|
||||
Input.TryRead(out _);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Input.TryRead(out Message message))
|
||||
if (Input.TryRead(out var buffer))
|
||||
{
|
||||
_logger.LogDebug("Scheduling raising Received event.");
|
||||
var ignore = _eventQueue.Enqueue(() =>
|
||||
|
|
@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
var receivedEventHandler = Received;
|
||||
if (receivedEventHandler != null)
|
||||
{
|
||||
receivedEventHandler(message.Payload, message.Type);
|
||||
receivedEventHandler(buffer);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -275,12 +275,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger.LogTrace("Ending receive loop");
|
||||
}
|
||||
|
||||
public Task SendAsync(byte[] data, MessageType type)
|
||||
{
|
||||
return SendAsync(data, type, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken)
|
||||
public async Task SendAsync(byte[] data, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
|
|
@ -298,7 +293,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
// TaskCompletionSource result. This way we prevent from user's code blocking our channel
|
||||
// send loop.
|
||||
var sendTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var message = new SendMessage(data, type, sendTcs);
|
||||
var message = new SendMessage(data, sendTcs);
|
||||
|
||||
_logger.LogDebug("Sending message");
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
public interface IConnection
|
||||
{
|
||||
Task StartAsync(ITransportFactory transportFactory, HttpClient httpClient);
|
||||
Task SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken);
|
||||
Task SendAsync(byte[] data, CancellationToken cancellationToken);
|
||||
Task DisposeAsync();
|
||||
|
||||
event Action Connected;
|
||||
event Action<byte[], MessageType> Received;
|
||||
event Action<byte[]> Received;
|
||||
event Action<Exception> Closed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
public interface ITransport
|
||||
{
|
||||
Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application);
|
||||
Task StartAsync(Uri url, IChannelConnection<SendMessage, byte[]> application);
|
||||
Task StopAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
|
@ -19,10 +17,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private IChannelConnection<SendMessage, Message> _application;
|
||||
private IChannelConnection<SendMessage, byte[]> _application;
|
||||
private Task _sender;
|
||||
private Task _poller;
|
||||
private MessageParser _parser = new MessageParser();
|
||||
|
||||
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
||||
|
||||
|
|
@ -38,7 +35,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
|
||||
public Task StartAsync(Uri url, IChannelConnection<SendMessage, byte[]> application)
|
||||
{
|
||||
_logger.LogInformation("Starting {0}", nameof(LongPollingTransport));
|
||||
|
||||
|
|
@ -86,7 +83,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, pollUrl);
|
||||
request.Headers.UserAgent.Add(SendUtils.DefaultUserAgentHeader);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MessageFormatter.BinaryContentType));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ContentTypes.BinaryContentType));
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
|
@ -102,24 +99,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
_logger.LogDebug("Received messages from the server");
|
||||
|
||||
var messageFormat = MessageParser.GetFormatFromContentType(response.Content.Headers.ContentType.ToString());
|
||||
|
||||
// Until Pipeline starts natively supporting BytesReader, this is the easiest way to do this.
|
||||
var payload = await response.Content.ReadAsByteArrayAsync();
|
||||
if (payload.Length > 0)
|
||||
{
|
||||
var messages = ParsePayload(payload, messageFormat);
|
||||
|
||||
foreach (var message in messages)
|
||||
while (!_application.Output.TryWrite(payload))
|
||||
{
|
||||
while (!_application.Output.TryWrite(message))
|
||||
if (cancellationToken.IsCancellationRequested || !await _application.Output.WaitToWriteAsync(cancellationToken))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested || !await _application.Output.WaitToWriteAsync(cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,33 +131,5 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger.LogInformation("Receive loop stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private IList<Message> ParsePayload(byte[] payload, MessageFormat messageFormat)
|
||||
{
|
||||
var reader = new BytesReader(payload);
|
||||
if (messageFormat != MessageParser.GetFormatFromIndicator(reader.Unread[0]))
|
||||
{
|
||||
throw new FormatException($"Format indicator '{(char)reader.Unread[0]}' does not match format determined by Content-Type '{MessageFormatter.GetContentType(messageFormat)}'");
|
||||
}
|
||||
reader.Advance(1);
|
||||
|
||||
_parser.Reset();
|
||||
var messages = new List<Message>();
|
||||
while (_parser.TryParseMessage(ref reader, messageFormat, out var message))
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
// Since we pre-read the whole payload, we know that when this fails we have read everything.
|
||||
// Once Pipelines natively support BytesReader, we could get into situations where the data for
|
||||
// a message just isn't available yet.
|
||||
|
||||
// If there's still data, we hit an incomplete message
|
||||
if (reader.Unread.Length > 0)
|
||||
{
|
||||
throw new FormatException("Incomplete message");
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<Description>Client for ASP.NET Core SignalR</Description>
|
||||
<TargetFramework>netstandard1.3</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;signalr</PackageTags>
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -20,7 +21,6 @@
|
|||
<PackageReference Include="System.Text.Formatting" Version="$(CoreFxLabsVersion)" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsVersion)" />
|
||||
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="$(CoreFxVersion)" />
|
||||
<PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +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.Client
|
||||
{
|
||||
public class ReceiveData
|
||||
{
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
public MessageType MessageType { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
public struct SendMessage
|
||||
{
|
||||
public MessageType Type { get; }
|
||||
public byte[] Payload { get; }
|
||||
public TaskCompletionSource<object> SendResult { get; }
|
||||
|
||||
public SendMessage(byte[] payload, MessageType type, TaskCompletionSource<object> result)
|
||||
public SendMessage(byte[] payload, TaskCompletionSource<object> result)
|
||||
{
|
||||
Type = type;
|
||||
Payload = payload;
|
||||
SendResult = result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipelines.Text.Primitives;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Formatting;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
|
|
@ -22,7 +17,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly string DefaultUserAgent = "Microsoft.AspNetCore.SignalR.Client/0.0.0";
|
||||
public static readonly ProductInfoHeaderValue DefaultUserAgentHeader = ProductInfoHeaderValue.Parse(DefaultUserAgent);
|
||||
|
||||
public static async Task SendMessages(Uri sendUrl, IChannelConnection<SendMessage, Message> application, HttpClient httpClient, CancellationTokenSource transportCts, ILogger logger)
|
||||
public static async Task SendMessages(Uri sendUrl, IChannelConnection<SendMessage, byte[]> application, HttpClient httpClient, CancellationTokenSource transportCts, ILogger logger)
|
||||
{
|
||||
logger.LogInformation("Starting the send loop");
|
||||
IList<SendMessage> messages = null;
|
||||
|
|
@ -49,17 +44,19 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
// But where do we get the pool from? ArrayBufferPool.Instance?
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
// Write the messages to the stream
|
||||
var pipe = memoryStream.AsPipelineWriter();
|
||||
var output = new PipelineTextOutput(pipe, TextEncoder.Utf8); // We don't need the Encoder, but it's harmless to set.
|
||||
await WriteMessagesAsync(messages, output, MessageFormat.Binary, logger);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Payload != null)
|
||||
{
|
||||
memoryStream.Write(message.Payload, 0, message.Payload.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Seek back to the start
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
// Set the, now filled, stream as the content
|
||||
request.Content = new StreamContent(memoryStream);
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MessageFormatter.GetContentType(MessageFormat.Binary));
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentTypes.GetContentType(MessageFormat.Binary));
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
|
@ -109,23 +106,5 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
logger.LogInformation("Send loop stopped");
|
||||
}
|
||||
|
||||
private static async Task WriteMessagesAsync(IList<SendMessage> messages, PipelineTextOutput output, MessageFormat format, ILogger logger)
|
||||
{
|
||||
output.Append(MessageFormatter.GetFormatIndicator(format), TextEncoder.Utf8);
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
logger.LogDebug("Writing '{0}' message to the server", message.Type);
|
||||
|
||||
var payload = message.Payload ?? Array.Empty<byte>();
|
||||
if (!MessageFormatter.TryWriteMessage(new Message(payload, message.Type), output, format))
|
||||
{
|
||||
// We didn't get any more memory!
|
||||
throw new InvalidOperationException("Unable to write message to pipeline");
|
||||
}
|
||||
await output.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,26 +14,19 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
private const byte ByteCR = (byte)'\r';
|
||||
private const byte ByteLF = (byte)'\n';
|
||||
private const byte ByteColon = (byte)':';
|
||||
private const byte ByteT = (byte)'T';
|
||||
private const byte ByteB = (byte)'B';
|
||||
private const byte ByteC = (byte)'C';
|
||||
private const byte ByteE = (byte)'E';
|
||||
|
||||
private static byte[] _dataPrefix = Encoding.UTF8.GetBytes("data: ");
|
||||
private static byte[] _sseLineEnding = Encoding.UTF8.GetBytes("\r\n");
|
||||
private static byte[] _newLine = Encoding.UTF8.GetBytes(Environment.NewLine);
|
||||
|
||||
private readonly static int _messageTypeLineLength = "data: X\r\n".Length;
|
||||
|
||||
private InternalParseState _internalParserState = InternalParseState.ReadMessageType;
|
||||
private InternalParseState _internalParserState = InternalParseState.ReadMessagePayload;
|
||||
private List<byte[]> _data = new List<byte[]>();
|
||||
private MessageType _messageType = MessageType.Text;
|
||||
|
||||
public ParseResult ParseMessage(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out Message message)
|
||||
public ParseResult ParseMessage(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out byte[] message)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
message = new Message();
|
||||
message = null;
|
||||
var reader = new ReadableBufferReader(buffer);
|
||||
|
||||
var start = consumed;
|
||||
|
|
@ -46,7 +39,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
// For the case of data: Foo\r\n\r\<Anytine except \n>
|
||||
if (_internalParserState == InternalParseState.ReadEndOfMessage)
|
||||
{
|
||||
if(ConvertBufferToSpan(buffer.Slice(start, buffer.End)).Length > 1)
|
||||
if (ConvertBufferToSpan(buffer.Slice(start, buffer.End)).Length > 1)
|
||||
{
|
||||
throw new FormatException("Expected a \\r\\n frame ending");
|
||||
}
|
||||
|
|
@ -95,17 +88,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
var payload = Array.Empty<byte>();
|
||||
switch (_internalParserState)
|
||||
{
|
||||
case InternalParseState.ReadMessageType:
|
||||
EnsureStartsWithDataPrefix(line);
|
||||
|
||||
|
||||
_messageType = ParseMessageType(line);
|
||||
|
||||
_internalParserState = InternalParseState.ReadMessagePayload;
|
||||
|
||||
start = lineEnd;
|
||||
consumed = lineEnd;
|
||||
break;
|
||||
case InternalParseState.ReadMessagePayload:
|
||||
EnsureStartsWithDataPrefix(line);
|
||||
|
||||
|
|
@ -131,25 +113,18 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
payloadSize += dataLine.Length;
|
||||
}
|
||||
|
||||
if (_messageType != MessageType.Binary)
|
||||
{
|
||||
payloadSize += _newLine.Length*_data.Count;
|
||||
payloadSize += _newLine.Length * _data.Count;
|
||||
|
||||
// Allocate space in the paylod buffer for the data and the new lines.
|
||||
// Subtract newLine length because we don't want a trailing newline.
|
||||
payload = new byte[payloadSize - _newLine.Length];
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = new byte[payloadSize];
|
||||
}
|
||||
// Allocate space in the payload buffer for the data and the new lines.
|
||||
// Subtract newLine length because we don't want a trailing newline.
|
||||
payload = new byte[payloadSize - _newLine.Length];
|
||||
|
||||
var offset = 0;
|
||||
foreach (var dataLine in _data)
|
||||
{
|
||||
dataLine.CopyTo(payload, offset);
|
||||
offset += dataLine.Length;
|
||||
if (offset < payload.Length && _messageType != MessageType.Binary)
|
||||
if (offset < payload.Length)
|
||||
{
|
||||
_newLine.CopyTo(payload, offset);
|
||||
offset += _newLine.Length;
|
||||
|
|
@ -157,12 +132,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
if (_messageType == MessageType.Binary)
|
||||
{
|
||||
payload = MessageFormatUtils.DecodePayload(payload);
|
||||
}
|
||||
|
||||
message = new Message(payload, _messageType);
|
||||
message = payload;
|
||||
consumed = lineEnd;
|
||||
examined = consumed;
|
||||
return ParseResult.Completed;
|
||||
|
|
@ -188,7 +158,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
|
||||
public void Reset()
|
||||
{
|
||||
_internalParserState = InternalParseState.ReadMessageType;
|
||||
_internalParserState = InternalParseState.ReadMessagePayload;
|
||||
_data.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -205,30 +175,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
return line.Length == _sseLineEnding.Length && line.SequenceEqual(_sseLineEnding);
|
||||
}
|
||||
|
||||
private MessageType ParseMessageType(ReadOnlySpan<byte> line)
|
||||
{
|
||||
if (line.Length != _messageTypeLineLength)
|
||||
{
|
||||
throw new FormatException("Expected a data format message of the form 'data: <MesssageType>'");
|
||||
}
|
||||
|
||||
// Skip the "data: " part of the line
|
||||
var type = line[_dataPrefix.Length];
|
||||
switch (type)
|
||||
{
|
||||
case ByteT:
|
||||
return MessageType.Text;
|
||||
case ByteB:
|
||||
return MessageType.Binary;
|
||||
case ByteC:
|
||||
return MessageType.Close;
|
||||
case ByteE:
|
||||
return MessageType.Error;
|
||||
default:
|
||||
throw new FormatException($"Unknown message type: '{(char)type}'");
|
||||
}
|
||||
}
|
||||
|
||||
public enum ParseResult
|
||||
{
|
||||
Completed,
|
||||
|
|
@ -237,7 +183,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
|||
|
||||
private enum InternalParseState
|
||||
{
|
||||
ReadMessageType,
|
||||
ReadMessagePayload,
|
||||
ReadEndOfMessage,
|
||||
Error
|
||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
||||
private readonly ServerSentEventsMessageParser _parser = new ServerSentEventsMessageParser();
|
||||
|
||||
private IChannelConnection<SendMessage, Message> _application;
|
||||
private IChannelConnection<SendMessage, byte[]> _application;
|
||||
|
||||
public Task Running { get; private set; } = Task.CompletedTask;
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>();
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
|
||||
public Task StartAsync(Uri url, IChannelConnection<SendMessage, byte[]> application)
|
||||
{
|
||||
_logger.LogInformation("Starting {transportName}", nameof(ServerSentEventsTransport));
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OpenConnection(IChannelConnection<SendMessage, Message> application, Uri url, CancellationToken cancellationToken)
|
||||
private async Task OpenConnection(IChannelConnection<SendMessage, byte[]> application, Uri url, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting receive loop");
|
||||
|
||||
|
|
@ -89,12 +89,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
break;
|
||||
}
|
||||
|
||||
var parseResult = _parser.ParseMessage(input, out consumed, out examined, out var message);
|
||||
var parseResult = _parser.ParseMessage(input, out consumed, out examined, out var buffer);
|
||||
|
||||
switch (parseResult)
|
||||
{
|
||||
case ServerSentEventsMessageParser.ParseResult.Completed:
|
||||
_application.Output.TryWrite(message);
|
||||
_application.Output.TryWrite(buffer);
|
||||
_parser.Reset();
|
||||
break;
|
||||
case ServerSentEventsMessageParser.ParseResult.Incomplete:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
public class WebSocketsTransport : ITransport
|
||||
{
|
||||
private readonly ClientWebSocket _webSocket = new ClientWebSocket();
|
||||
private IChannelConnection<SendMessage, Message> _application;
|
||||
private IChannelConnection<SendMessage, byte[]> _application;
|
||||
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
public Task Running { get; private set; } = Task.CompletedTask;
|
||||
|
||||
public async Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
|
||||
public async Task StartAsync(Uri url, IChannelConnection<SendMessage, byte[]> application)
|
||||
{
|
||||
_logger.LogInformation("Starting {0}", nameof(WebSocketsTransport));
|
||||
|
||||
|
|
@ -103,31 +103,25 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
//Making sure the message type is either text or binary
|
||||
Debug.Assert((receiveResult.MessageType == WebSocketMessageType.Binary || receiveResult.MessageType == WebSocketMessageType.Text), "Unexpected message type");
|
||||
|
||||
Message message;
|
||||
var messageType = receiveResult.MessageType == WebSocketMessageType.Binary ? MessageType.Binary : MessageType.Text;
|
||||
var messageBuffer = new byte[totalBytes];
|
||||
if (incomingMessage.Count > 1)
|
||||
{
|
||||
var messageBuffer = new byte[totalBytes];
|
||||
var offset = 0;
|
||||
for (var i = 0; i < incomingMessage.Count; i++)
|
||||
{
|
||||
Buffer.BlockCopy(incomingMessage[i].Array, 0, messageBuffer, offset, incomingMessage[i].Count);
|
||||
offset += incomingMessage[i].Count;
|
||||
}
|
||||
|
||||
message = new Message(messageBuffer, messageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = new byte[incomingMessage[0].Count];
|
||||
Buffer.BlockCopy(incomingMessage[0].Array, incomingMessage[0].Offset, buffer, 0, incomingMessage[0].Count);
|
||||
message = new Message(buffer, messageType);
|
||||
Buffer.BlockCopy(incomingMessage[0].Array, incomingMessage[0].Offset, messageBuffer, 0, incomingMessage[0].Count);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Passing message to application. Payload size: {0}", message.Payload.Length);
|
||||
_logger.LogInformation("Passing message to application. Payload size: {0}", messageBuffer.Length);
|
||||
while (await _application.Output.WaitToWriteAsync(_transportCts.Token))
|
||||
{
|
||||
if (_application.Output.TryWrite(message))
|
||||
if (_application.Output.TryWrite(messageBuffer))
|
||||
{
|
||||
incomingMessage.Clear();
|
||||
break;
|
||||
|
|
@ -157,12 +151,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Received message from application. Message type {0}. Payload size: {1}",
|
||||
message.Type, message.Payload.Length);
|
||||
_logger.LogDebug("Received message from application. Payload size: {1}", message.Payload.Length);
|
||||
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(message.Payload),
|
||||
message.Type == MessageType.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary,
|
||||
true, _transportCts.Token);
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(message.Payload), WebSocketMessageType.Text, true, _transportCts.Token);
|
||||
|
||||
message.SendResult.SetResult(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +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.Binary;
|
||||
using System.Binary.Base64;
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using System.Text.Formatting;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
||||
{
|
||||
public static class ServerSentEventsMessageFormatter
|
||||
{
|
||||
private static readonly byte[] DataPrefix = new byte[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)':', (byte)' ' };
|
||||
private static readonly byte[] Newline = new byte[] { (byte)'\r', (byte)'\n' };
|
||||
|
||||
private const byte LineFeed = (byte)'\n';
|
||||
private const char TextTypeFlag = 'T';
|
||||
private const char BinaryTypeFlag = 'B';
|
||||
private const char CloseTypeFlag = 'C';
|
||||
private const char ErrorTypeFlag = 'E';
|
||||
|
||||
public static bool TryWriteMessage(Message message, IOutput output)
|
||||
{
|
||||
var typeIndicator = GetTypeIndicator(message.Type);
|
||||
|
||||
// Write the Data Prefix
|
||||
if (!output.TryWrite(DataPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the type indicator
|
||||
output.Append(typeIndicator, TextEncoder.Utf8);
|
||||
|
||||
if (!output.TryWrite(Newline))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the payload
|
||||
if (!TryWritePayload(message.Payload, message.Type, output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(Newline))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWritePayload(ReadOnlySpan<byte> payload, MessageType type, IOutput output)
|
||||
{
|
||||
// Short-cut for empty payload
|
||||
if (payload.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == MessageType.Binary)
|
||||
{
|
||||
// TODO: Base64 writer that works with IOutput would be amazing!
|
||||
var arr = new byte[Base64Encoder.ComputeEncodedLength(payload.Length)];
|
||||
Base64.Encoder.Transform(payload, arr, out _, out _);
|
||||
return TryWriteLine(arr, output);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't just use while(payload.Length > 0) because we need to write a blank final "data: " line
|
||||
// if the payload ends in a newline. For example, consider the following payload:
|
||||
// "Hello\n"
|
||||
// It needs to be written as:
|
||||
// data: Hello\r\n
|
||||
// data: \r\n
|
||||
// \r\n
|
||||
// Since we slice past the newline when we find it, after writing "Hello" in the previous example, we'll
|
||||
// end up with an empty payload buffer, BUT we need to write it as an empty 'data:' line, so we need
|
||||
// to use a condition that ensure the only time we stop writing is when we write the slice after the final
|
||||
// newline.
|
||||
var keepWriting = true;
|
||||
while (keepWriting)
|
||||
{
|
||||
// Seek to the end of buffer or newline
|
||||
var sliceEnd = payload.IndexOf(LineFeed);
|
||||
var nextSliceStart = sliceEnd + 1;
|
||||
if (sliceEnd < 0)
|
||||
{
|
||||
sliceEnd = payload.Length;
|
||||
nextSliceStart = sliceEnd + 1;
|
||||
|
||||
// This is the last span
|
||||
keepWriting = false;
|
||||
}
|
||||
if (sliceEnd > 0 && payload[sliceEnd - 1] == '\r')
|
||||
{
|
||||
sliceEnd--;
|
||||
}
|
||||
|
||||
var slice = payload.Slice(0, sliceEnd);
|
||||
|
||||
if (nextSliceStart >= payload.Length)
|
||||
{
|
||||
payload = Span<byte>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = payload.Slice(nextSliceStart);
|
||||
}
|
||||
|
||||
if (!TryWriteLine(slice, output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteLine(ReadOnlySpan<byte> line, IOutput output)
|
||||
{
|
||||
if (!output.TryWrite(DataPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(Newline))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static char GetTypeIndicator(MessageType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MessageType.Text: return TextTypeFlag;
|
||||
case MessageType.Binary: return BinaryTypeFlag;
|
||||
case MessageType.Close: return CloseTypeFlag;
|
||||
case MessageType.Error: return ErrorTypeFlag;
|
||||
default: throw new FormatException($"Invalid Message Type: {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,6 @@
|
|||
<EnableApiCheck>false</EnableApiCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../Common/IOutputExtensions.cs" Link="IOutputExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Binary.Base64" Version="$(CoreFxLabsVersion)" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsVersion)" />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
{
|
||||
public static class ConnectionMetadataNames
|
||||
{
|
||||
public static readonly string Format = nameof(Format);
|
||||
public static readonly string Transport = nameof(Transport);
|
||||
public static readonly string HttpContext = nameof(HttpContext);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// 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.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
|
|
@ -11,7 +9,6 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Microsoft.AspNetCore.Sockets.Transports;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
|
@ -59,7 +56,7 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteEndpointAsync(HttpContext context, SocketDelegate socketDelegate, HttpSocketOptions options)
|
||||
private async Task ExecuteEndpointAsync(HttpContext context, SocketDelegate socketDelegate, HttpSocketOptions options)
|
||||
{
|
||||
var supportedTransports = options.Transports;
|
||||
|
||||
|
|
@ -339,8 +336,8 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
return;
|
||||
}
|
||||
|
||||
// Read the entire payload to a byte array for now because Pipelines and ReadOnlyBytes
|
||||
// don't play well with each other yet.
|
||||
// TODO: Use a pool here
|
||||
|
||||
byte[] buffer;
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
|
|
@ -349,35 +346,11 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
buffer = stream.ToArray();
|
||||
}
|
||||
|
||||
MessageFormat messageFormat;
|
||||
if (string.Equals(context.Request.ContentType, MessageFormatter.TextContentType, StringComparison.OrdinalIgnoreCase))
|
||||
while (!connection.Application.Output.TryWrite(buffer))
|
||||
{
|
||||
messageFormat = MessageFormat.Text;
|
||||
}
|
||||
else if (string.Equals(context.Request.ContentType, MessageFormatter.BinaryContentType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
messageFormat = MessageFormat.Binary;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await context.Response.WriteAsync($"'{context.Request.ContentType}' is not a valid Content-Type for send requests.");
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new BytesReader(buffer);
|
||||
var messages = ParseSendBatch(ref reader, messageFormat);
|
||||
|
||||
// REVIEW: Do we want to return a specific status code here if the connection has ended?
|
||||
_logger.LogDebug("Received batch of {count} message(s)", messages.Count);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
while (!connection.Application.Output.TryWrite(message))
|
||||
if (!await connection.Application.Output.WaitToWriteAsync())
|
||||
{
|
||||
if (!await connection.Application.Output.WaitToWriteAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -405,10 +378,8 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
}
|
||||
|
||||
// Setup the connection state from the http context
|
||||
var format = (string)context.Request.Query[ConnectionMetadataNames.Format];
|
||||
connection.User = context.User;
|
||||
connection.Metadata[ConnectionMetadataNames.HttpContext] = context;
|
||||
connection.Metadata[ConnectionMetadataNames.Format] = string.IsNullOrEmpty(format) ? "json" : format;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -456,30 +427,5 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private List<Message> ParseSendBatch(ref BytesReader payload, MessageFormat messageFormat)
|
||||
{
|
||||
var messages = new List<Message>();
|
||||
|
||||
if (payload.Unread.Length == 0)
|
||||
{
|
||||
return messages;
|
||||
}
|
||||
|
||||
if (payload.Unread[0] != MessageFormatter.GetFormatIndicator(messageFormat))
|
||||
{
|
||||
throw new FormatException($"Format indicator '{(char)payload.Unread[0]}' does not match format determined by Content-Type '{MessageFormatter.GetContentType(messageFormat)}'");
|
||||
}
|
||||
|
||||
payload.Advance(1);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
<EnableApiCheck>false</EnableApiCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../Common/IOutputExtensions.cs" Link="IOutputExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipelines.Text.Primitives;
|
||||
using System.Text;
|
||||
using System.Text.Formatting;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Channels;
|
||||
|
|
@ -17,10 +14,10 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
{
|
||||
public class LongPollingTransport : IHttpTransport
|
||||
{
|
||||
private readonly ReadableChannel<Message> _application;
|
||||
private readonly ReadableChannel<byte[]> _application;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LongPollingTransport(ReadableChannel<Message> application, ILoggerFactory loggerFactory)
|
||||
public LongPollingTransport(ReadableChannel<byte[]> application, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_application = application;
|
||||
_logger = loggerFactory.CreateLogger<LongPollingTransport>();
|
||||
|
|
@ -39,35 +36,28 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
}
|
||||
|
||||
var headers = context.Request.GetTypedHeaders();
|
||||
var messageFormat = headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue(MessageFormatter.BinaryContentType)) == true ?
|
||||
var messageFormat = headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue(ContentTypes.BinaryContentType)) == true ?
|
||||
MessageFormat.Binary :
|
||||
MessageFormat.Text;
|
||||
context.Response.ContentType = MessageFormatter.GetContentType(messageFormat);
|
||||
|
||||
var writer = context.Response.Body.AsPipelineWriter();
|
||||
var output = new PipelineTextOutput(writer, TextEncoder.Utf8); // We don't need the Encoder, but it's harmless to set.
|
||||
|
||||
output.Append(MessageFormatter.GetFormatIndicator(messageFormat));
|
||||
context.Response.ContentType = ContentTypes.GetContentType(messageFormat);
|
||||
|
||||
var contentLength = 0;
|
||||
var buffers = new List<byte[]>();
|
||||
// We're intentionally not checking cancellation here because we need to drain messages we've got so far,
|
||||
// but it's too late to emit the 204 required by being cancelled.
|
||||
while (_application.TryRead(out var message))
|
||||
while (_application.TryRead(out var buffer))
|
||||
{
|
||||
_logger.LogDebug("Writing {0} byte message to response", message.Payload.Length);
|
||||
contentLength += buffer.Length;
|
||||
buffers.Add(buffer);
|
||||
|
||||
if (!MessageFormatter.TryWriteMessage(message, output, messageFormat))
|
||||
{
|
||||
// We ran out of space to write, even after trying to enlarge.
|
||||
// This should only happen in a significant lack-of-memory scenario.
|
||||
_logger.LogDebug("Writing {0} byte message to response", buffer.Length);
|
||||
}
|
||||
|
||||
// IOutput doesn't really have a way to write incremental
|
||||
context.Response.ContentLength = contentLength;
|
||||
|
||||
// Throwing InvalidOperationException here, but it's not quite an invalid operation...
|
||||
throw new InvalidOperationException("Ran out of space to format messages!");
|
||||
}
|
||||
|
||||
// REVIEW: Flushing after each message? Good? Bad? We can't access Commit because it's hidden inside PipelineTextOutput
|
||||
await output.FlushAsync();
|
||||
foreach (var buffer in buffers)
|
||||
{
|
||||
await context.Response.Body.WriteAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
// 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.Binary;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
||||
{
|
||||
public static class ServerSentEventsMessageFormatter
|
||||
{
|
||||
private static readonly byte[] DataPrefix = new byte[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)':', (byte)' ' };
|
||||
private static readonly byte[] Newline = new byte[] { (byte)'\r', (byte)'\n' };
|
||||
|
||||
private const byte LineFeed = (byte)'\n';
|
||||
|
||||
public static bool TryWriteMessage(ReadOnlySpan<byte> payload, IOutput output)
|
||||
{
|
||||
// Write the payload
|
||||
if (!TryWritePayload(payload, output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(Newline))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWritePayload(ReadOnlySpan<byte> payload, IOutput output)
|
||||
{
|
||||
// Short-cut for empty payload
|
||||
if (payload.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can't just use while(payload.Length > 0) because we need to write a blank final "data: " line
|
||||
// if the payload ends in a newline. For example, consider the following payload:
|
||||
// "Hello\n"
|
||||
// It needs to be written as:
|
||||
// data: Hello\r\n
|
||||
// data: \r\n
|
||||
// \r\n
|
||||
// Since we slice past the newline when we find it, after writing "Hello" in the previous example, we'll
|
||||
// end up with an empty payload buffer, BUT we need to write it as an empty 'data:' line, so we need
|
||||
// to use a condition that ensure the only time we stop writing is when we write the slice after the final
|
||||
// newline.
|
||||
var keepWriting = true;
|
||||
while (keepWriting)
|
||||
{
|
||||
// Seek to the end of buffer or newline
|
||||
var sliceEnd = payload.IndexOf(LineFeed);
|
||||
var nextSliceStart = sliceEnd + 1;
|
||||
if (sliceEnd < 0)
|
||||
{
|
||||
sliceEnd = payload.Length;
|
||||
nextSliceStart = sliceEnd + 1;
|
||||
|
||||
// This is the last span
|
||||
keepWriting = false;
|
||||
}
|
||||
if (sliceEnd > 0 && payload[sliceEnd - 1] == '\r')
|
||||
{
|
||||
sliceEnd--;
|
||||
}
|
||||
|
||||
var slice = payload.Slice(0, sliceEnd);
|
||||
|
||||
if (nextSliceStart >= payload.Length)
|
||||
{
|
||||
payload = Span<byte>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = payload.Slice(nextSliceStart);
|
||||
}
|
||||
|
||||
if (!TryWriteLine(slice, output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteLine(ReadOnlySpan<byte> line, IOutput output)
|
||||
{
|
||||
if (!output.TryWrite(DataPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!output.TryWrite(Newline))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,10 +17,10 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
{
|
||||
public class ServerSentEventsTransport : IHttpTransport
|
||||
{
|
||||
private readonly ReadableChannel<Message> _application;
|
||||
private readonly ReadableChannel<byte[]> _application;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ServerSentEventsTransport(ReadableChannel<Message> application, ILoggerFactory loggerFactory)
|
||||
public ServerSentEventsTransport(ReadableChannel<byte[]> application, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_application = application;
|
||||
_logger = loggerFactory.CreateLogger<ServerSentEventsTransport>();
|
||||
|
|
@ -49,9 +49,9 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
{
|
||||
while (await _application.WaitToReadAsync(token))
|
||||
{
|
||||
while (_application.TryRead(out var message))
|
||||
while (_application.TryRead(out var buffer))
|
||||
{
|
||||
if (!ServerSentEventsMessageFormatter.TryWriteMessage(message, output))
|
||||
if (!ServerSentEventsMessageFormatter.TryWriteMessage(buffer, output))
|
||||
{
|
||||
// We ran out of space to write, even after trying to enlarge.
|
||||
// This should only happen in a significant lack-of-memory scenario.
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
{
|
||||
private readonly WebSocketOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IChannelConnection<Message> _application;
|
||||
private readonly IChannelConnection<byte[]> _application;
|
||||
|
||||
public WebSocketsTransport(WebSocketOptions options, IChannelConnection<Message> application, ILoggerFactory loggerFactory)
|
||||
public WebSocketsTransport(WebSocketOptions options, IChannelConnection<byte[]> application, ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
|
|
@ -149,31 +149,30 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
// Making sure the message type is either text or binary
|
||||
Debug.Assert((receiveResult.MessageType == WebSocketMessageType.Binary || receiveResult.MessageType == WebSocketMessageType.Text), "Unexpected message type");
|
||||
|
||||
Message message;
|
||||
var messageType = receiveResult.MessageType == WebSocketMessageType.Binary ? MessageType.Binary : MessageType.Text;
|
||||
// TODO: Check received message type against the _options.WebSocketMessageType
|
||||
|
||||
byte[] messageBuffer = null;
|
||||
|
||||
if (incomingMessage.Count > 1)
|
||||
{
|
||||
var messageBuffer = new byte[totalBytes];
|
||||
messageBuffer = new byte[totalBytes];
|
||||
var offset = 0;
|
||||
for (var i = 0; i < incomingMessage.Count; i++)
|
||||
{
|
||||
Buffer.BlockCopy(incomingMessage[i].Array, 0, messageBuffer, offset, incomingMessage[i].Count);
|
||||
offset += incomingMessage[i].Count;
|
||||
}
|
||||
|
||||
message = new Message(messageBuffer, messageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = new byte[incomingMessage[0].Count];
|
||||
Buffer.BlockCopy(incomingMessage[0].Array, incomingMessage[0].Offset, buffer, 0, incomingMessage[0].Count);
|
||||
message = new Message(buffer, messageType);
|
||||
messageBuffer = new byte[incomingMessage[0].Count];
|
||||
Buffer.BlockCopy(incomingMessage[0].Array, incomingMessage[0].Offset, messageBuffer, 0, incomingMessage[0].Count);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Passing message to application. Payload size: {length}", message.Payload.Length);
|
||||
_logger.LogInformation("Passing message to application. Payload size: {length}", messageBuffer.Length);
|
||||
while (await _application.Output.WaitToWriteAsync())
|
||||
{
|
||||
if (_application.Output.TryWrite(message))
|
||||
if (_application.Output.TryWrite(messageBuffer))
|
||||
{
|
||||
incomingMessage.Clear();
|
||||
break;
|
||||
|
|
@ -187,23 +186,18 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
|||
while (await _application.Input.WaitToReadAsync())
|
||||
{
|
||||
// Get a frame from the application
|
||||
while (_application.Input.TryRead(out var message))
|
||||
while (_application.Input.TryRead(out var buffer))
|
||||
{
|
||||
if (message.Payload.Length > 0)
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messageType = message.Type == MessageType.Binary ?
|
||||
WebSocketMessageType.Binary :
|
||||
WebSocketMessageType.Text;
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Sending Type: {messageType}, size: {size}",
|
||||
message.Type, message.Payload.Length);
|
||||
_logger.LogDebug("Sending payload: {size}", buffer.Length);
|
||||
}
|
||||
|
||||
await ws.SendAsync(new ArraySegment<byte>(message.Payload), messageType, endOfMessage: true, cancellationToken: CancellationToken.None);
|
||||
await ws.SendAsync(new ArraySegment<byte>(buffer), _options.WebSocketMessageType, endOfMessage: true, cancellationToken: CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets
|
||||
{
|
||||
public class WebSocketOptions
|
||||
{
|
||||
public TimeSpan CloseTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
public WebSocketMessageType WebSocketMessageType { get; set; } = WebSocketMessageType.Text;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,11 +50,11 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
{
|
||||
var id = MakeNewConnectionId();
|
||||
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
var connection = new DefaultConnectionContext(id, applicationSide, transportSide);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
releaseDisposeTcs.SetResult(null);
|
||||
await disposeTask.OrTimeout();
|
||||
|
||||
transport.Verify(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()), Times.Never);
|
||||
transport.Verify(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, byte[]>>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
{
|
||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
await connection.DisposeAsync();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
});
|
||||
|
||||
var mockTransport = new Mock<ITransport>();
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()))
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, byte[]>>()))
|
||||
.Returns(Task.FromException(new InvalidOperationException("Transport failed to start")));
|
||||
|
||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||
|
|
@ -336,9 +336,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
});
|
||||
|
||||
var mockTransport = new Mock<ITransport>();
|
||||
IChannelConnection<SendMessage, Message> channel = null;
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()))
|
||||
.Returns<Uri, IChannelConnection<SendMessage, Message>>((url, c) =>
|
||||
IChannelConnection<SendMessage, byte[]> channel = null;
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, byte[]>>()))
|
||||
.Returns<Uri, IChannelConnection<SendMessage, byte[]>>((url, c) =>
|
||||
{
|
||||
channel = c;
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -348,7 +348,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
{
|
||||
// The connection is now in the Disconnected state so the Received event for
|
||||
// this message should not be raised
|
||||
channel.Output.TryWrite(new Message());
|
||||
channel.Output.TryWrite(Array.Empty<byte>());
|
||||
channel.Output.TryComplete();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
|
@ -357,7 +357,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
{
|
||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||
var receivedInvoked = false;
|
||||
connection.Received += (m, t) => receivedInvoked = true;
|
||||
connection.Received += (m) => receivedInvoked = true;
|
||||
|
||||
await connection.StartAsync(new TestTransportFactory(mockTransport.Object), httpClient);
|
||||
await connection.DisposeAsync();
|
||||
|
|
@ -378,9 +378,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
});
|
||||
|
||||
var mockTransport = new Mock<ITransport>();
|
||||
IChannelConnection<SendMessage, Message> channel = null;
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()))
|
||||
.Returns<Uri, IChannelConnection<SendMessage, Message>>((url, c) =>
|
||||
IChannelConnection<SendMessage, byte[]> channel = null;
|
||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, byte[]>>()))
|
||||
.Returns<Uri, IChannelConnection<SendMessage, byte[]>>((url, c) =>
|
||||
{
|
||||
channel = c;
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -400,7 +400,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
|
||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||
connection.Received +=
|
||||
async (m, t) =>
|
||||
async (m) =>
|
||||
{
|
||||
if (Interlocked.Increment(ref receivedInvocationCount) == 2)
|
||||
{
|
||||
|
|
@ -411,8 +411,8 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
connection.Closed += e => closedTcs.SetResult(null);
|
||||
|
||||
await connection.StartAsync(new TestTransportFactory(mockTransport.Object), httpClient);
|
||||
channel.Output.TryWrite(new Message());
|
||||
channel.Output.TryWrite(new Message());
|
||||
channel.Output.TryWrite(Array.Empty<byte>());
|
||||
channel.Output.TryWrite(Array.Empty<byte>());
|
||||
await allowDisposeTcs.Task.OrTimeout();
|
||||
await connection.DisposeAsync();
|
||||
Assert.Equal(2, receivedInvocationCount);
|
||||
|
|
@ -469,8 +469,6 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
public async Task CanSendData()
|
||||
{
|
||||
var data = new byte[] { 1, 1, 2, 3, 5, 8 };
|
||||
var message = new Message(data, MessageType.Binary);
|
||||
var expectedPayload = FormatMessageToArray(message, MessageFormat.Binary);
|
||||
|
||||
var sendTcs = new TaskCompletionSource<byte[]>();
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
|
|
@ -493,9 +491,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
{
|
||||
await connection.StartAsync(TransportType.LongPolling, httpClient);
|
||||
|
||||
await connection.SendAsync(data, MessageType.Binary);
|
||||
await connection.SendAsync(data);
|
||||
|
||||
Assert.Equal(expectedPayload, await sendTcs.Task.OrTimeout());
|
||||
Assert.Equal(data, await sendTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -509,7 +507,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
{
|
||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
}
|
||||
|
|
@ -540,7 +538,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
await connection.DisposeAsync();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
}
|
||||
|
|
@ -569,7 +567,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
await connection.StartAsync(TransportType.LongPolling, httpClient);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<HttpRequestException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
|
|
@ -589,9 +587,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
|
||||
if (request.Method == HttpMethod.Get)
|
||||
{
|
||||
content = "T2:T:42;";
|
||||
content = "42";
|
||||
}
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, MessageFormatter.TextContentType, content);
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ContentTypes.TextContentType, content);
|
||||
});
|
||||
|
||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||
|
|
@ -600,7 +598,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
try
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
connection.Received += (data, format) => receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
connection.Received += (data) => receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
connection.Closed += e =>
|
||||
{
|
||||
if (e != null)
|
||||
|
|
@ -654,7 +652,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
|||
await closeTcs.Task.OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||
async () => await connection.SendAsync(new byte[0]));
|
||||
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||
Assert.Equal("59:T:{\"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("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||
Assert.Equal("59:T:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage);
|
||||
|
||||
// Complete the channel
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -254,7 +255,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var hubConnection = new HubConnection(mockConnection.Object, mockProtocol, null);
|
||||
await hubConnection.StartAsync(new TestTransportFactory(Mock.Of<ITransport>()), httpClient: null);
|
||||
|
||||
mockConnection.Raise(c => c.Received += null, new object[] { new byte[] { }, MessageType.Text });
|
||||
mockConnection.Raise(c => c.Received += null, new object[] { new byte[] { } });
|
||||
Assert.Equal(1, mockProtocol.ParseCalls);
|
||||
}
|
||||
|
||||
|
|
@ -285,8 +286,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
};
|
||||
}
|
||||
|
||||
public HubMessage ParseMessage(ReadOnlySpan<byte> input, IInvocationBinder binder)
|
||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, out IList<HubMessage> messages)
|
||||
{
|
||||
messages = new List<HubMessage>();
|
||||
|
||||
ParseCalls += 1;
|
||||
if (_error != null)
|
||||
{
|
||||
|
|
@ -294,7 +297,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
if (_parsed != null)
|
||||
{
|
||||
return _parsed;
|
||||
messages.Add(_parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No Parsed Message provided");
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
transportActiveTask = longPollingTransport.Running;
|
||||
|
|
@ -81,8 +81,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
await longPollingTransport.Running.OrTimeout();
|
||||
|
|
@ -113,8 +113,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
var exception =
|
||||
|
|
@ -149,8 +149,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
await connectionToTransport.Out.WriteAsync(new SendMessage());
|
||||
|
|
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
await Assert.ThrowsAsync<HttpRequestException>(async () => await longPollingTransport.Running.OrTimeout());
|
||||
|
||||
// The channel needs to be drained for the Completion task to be completed
|
||||
while (transportToConnection.In.TryRead(out Message message))
|
||||
while (transportToConnection.In.TryRead(out var message))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -190,8 +190,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
connectionToTransport.Out.Complete();
|
||||
|
|
@ -208,63 +208,10 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LongPollingTransportThrowsIfFormatIndicatorDoesNotMatchContentType()
|
||||
{
|
||||
var encoded = new byte[] { (byte)'T' };
|
||||
|
||||
var firstCall = true;
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
var sentRequests = new List<HttpRequestMessage>();
|
||||
mockHttpHandler.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||
{
|
||||
sentRequests.Add(request);
|
||||
|
||||
await Task.Yield();
|
||||
|
||||
if (firstCall)
|
||||
{
|
||||
firstCall = false;
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, MessageFormatter.BinaryContentType, encoded);
|
||||
}
|
||||
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.NoContent);
|
||||
});
|
||||
|
||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||
{
|
||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
|
||||
// Start the transport
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
||||
// Transport should fail
|
||||
var ex = await Assert.ThrowsAsync<FormatException>(() => longPollingTransport.Running.OrTimeout());
|
||||
Assert.Equal($"Format indicator 'T' does not match format determined by Content-Type '{MessageFormatter.BinaryContentType}'", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await longPollingTransport.StopAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LongPollingTransportDispatchesMessagesReceivedFromPoll()
|
||||
{
|
||||
var message1Payload = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' };
|
||||
var message2Payload = new byte[] { (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d' };
|
||||
var encoded = Enumerable.SelectMany(new[] {
|
||||
new byte[] { (byte)'B', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00 }, message1Payload,
|
||||
new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01 }, message2Payload
|
||||
}, b => b).ToArray();
|
||||
|
||||
var firstCall = true;
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
|
|
@ -280,7 +227,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
if (firstCall)
|
||||
{
|
||||
firstCall = false;
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, MessageFormatter.BinaryContentType, encoded);
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ContentTypes.BinaryContentType, message1Payload);
|
||||
}
|
||||
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.NoContent);
|
||||
|
|
@ -292,8 +239,8 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
|
||||
// Start the transport
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
|
@ -302,7 +249,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
await longPollingTransport.Running.OrTimeout();
|
||||
|
||||
// Pull Messages out of the channel
|
||||
var messages = new List<Message>();
|
||||
var messages = new List<byte[]>();
|
||||
while (await transportToConnection.In.WaitToReadAsync())
|
||||
{
|
||||
while (transportToConnection.In.TryRead(out var message))
|
||||
|
|
@ -313,14 +260,11 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
|
||||
// Check the provided request
|
||||
Assert.Equal(2, sentRequests.Count);
|
||||
Assert.Contains(MessageFormatter.BinaryContentType, sentRequests[0].Headers.Accept.FirstOrDefault()?.ToString());
|
||||
Assert.Contains(ContentTypes.BinaryContentType, sentRequests[0].Headers.Accept.FirstOrDefault()?.ToString());
|
||||
|
||||
// Check the messages received
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.Equal(MessageType.Text, messages[0].Type);
|
||||
Assert.Equal(message1Payload, messages[0].Payload);
|
||||
Assert.Equal(MessageType.Binary, messages[1].Type);
|
||||
Assert.Equal(message2Payload, messages[1].Payload);
|
||||
Assert.Equal(1, messages.Count);
|
||||
Assert.Equal(message1Payload, messages[0]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -354,15 +298,15 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
|
||||
var tcs1 = new TaskCompletionSource<object>();
|
||||
var tcs2 = new TaskCompletionSource<object>();
|
||||
|
||||
// Pre-queue some messages
|
||||
await connectionToTransport.Out.WriteAsync(new SendMessage(Encoding.UTF8.GetBytes("Hello"), MessageType.Text, tcs1)).OrTimeout();
|
||||
await connectionToTransport.Out.WriteAsync(new SendMessage(Encoding.UTF8.GetBytes("World"), MessageType.Binary, tcs2)).OrTimeout();
|
||||
await connectionToTransport.Out.WriteAsync(new SendMessage(Encoding.UTF8.GetBytes("Hello"), tcs1)).OrTimeout();
|
||||
await connectionToTransport.Out.WriteAsync(new SendMessage(Encoding.UTF8.GetBytes("World"), tcs2)).OrTimeout();
|
||||
|
||||
// Start the transport
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||
|
|
@ -373,10 +317,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
await connectionToTransport.In.Completion.OrTimeout();
|
||||
|
||||
Assert.Equal(1, sentRequests.Count);
|
||||
Assert.Equal(new byte[] {
|
||||
(byte)'B',
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o',
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d'
|
||||
Assert.Equal(new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d'
|
||||
}, sentRequests[0]);
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
|
||||
namespace Microsoft.AspNetCore.Client.Tests
|
||||
{
|
||||
internal static class ResponseUtils
|
||||
{
|
||||
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) =>
|
||||
CreateResponse(statusCode, MessageFormatter.TextContentType, string.Empty);
|
||||
CreateResponse(statusCode, ContentTypes.TextContentType, string.Empty);
|
||||
|
||||
public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string contentType, string payload) =>
|
||||
CreateResponse(statusCode, contentType, new StringContent(payload));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||
using System.Threading.Tasks.Channels;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
|
|
@ -18,20 +19,20 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
private TaskCompletionSource<object> _started = new TaskCompletionSource<object>();
|
||||
private TaskCompletionSource<object> _disposed = new TaskCompletionSource<object>();
|
||||
|
||||
private Channel<Message> _sentMessages = Channel.CreateUnbounded<Message>();
|
||||
private Channel<Message> _receivedMessages = Channel.CreateUnbounded<Message>();
|
||||
private Channel<byte[]> _sentMessages = Channel.CreateUnbounded<byte[]>();
|
||||
private Channel<byte[]> _receivedMessages = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource();
|
||||
private Task _receiveLoop;
|
||||
|
||||
public event Action Connected;
|
||||
public event Action<byte[], MessageType> Received;
|
||||
public event Action<byte[]> Received;
|
||||
public event Action<Exception> Closed;
|
||||
|
||||
public Task Started => _started.Task;
|
||||
public Task Disposed => _disposed.Task;
|
||||
public ReadableChannel<Message> SentMessages => _sentMessages.In;
|
||||
public WritableChannel<Message> ReceivedMessages => _receivedMessages.Out;
|
||||
public ReadableChannel<byte[]> SentMessages => _sentMessages.In;
|
||||
public WritableChannel<byte[]> ReceivedMessages => _receivedMessages.Out;
|
||||
|
||||
public TestConnection()
|
||||
{
|
||||
|
|
@ -45,17 +46,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return _receiveLoop;
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken)
|
||||
public async Task SendAsync(byte[] data, CancellationToken cancellationToken)
|
||||
{
|
||||
if(!_started.Task.IsCompleted)
|
||||
if (!_started.Task.IsCompleted)
|
||||
{
|
||||
throw new InvalidOperationException("Connection must be started before SendAsync can be called");
|
||||
}
|
||||
|
||||
var message = new Message(data, type);
|
||||
while (await _sentMessages.Out.WaitToWriteAsync(cancellationToken))
|
||||
{
|
||||
if (_sentMessages.Out.TryWrite(message))
|
||||
if (_sentMessages.Out.TryWrite(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -73,20 +73,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
public async Task<string> ReadSentTextMessageAsync()
|
||||
{
|
||||
var message = await SentMessages.ReadAsync();
|
||||
if (message.Type != MessageType.Text)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected message of type: {message.Type}");
|
||||
}
|
||||
return Encoding.UTF8.GetString(message.Payload);
|
||||
return Encoding.UTF8.GetString(message);
|
||||
}
|
||||
|
||||
public Task ReceiveJsonMessage(object jsonObject)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(jsonObject, Formatting.None);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var message = new Message(bytes, MessageType.Text);
|
||||
var bytes = Encoding.UTF8.GetBytes($"{json.Length}:T:{json};");
|
||||
|
||||
return _receivedMessages.Out.WriteAsync(message);
|
||||
return _receivedMessages.Out.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken token)
|
||||
|
|
@ -99,7 +94,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
while (_receivedMessages.In.TryRead(out var message))
|
||||
{
|
||||
Received?.Invoke(message.Payload, message.Type);
|
||||
Received?.Invoke(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[MemberData(nameof(ProtocolTestData))]
|
||||
public async Task WriteMessage(HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string expectedOutput)
|
||||
{
|
||||
expectedOutput = Frame(expectedOutput);
|
||||
|
||||
var jsonSerializer = new JsonSerializer
|
||||
{
|
||||
NullValueHandling = nullValueHandling,
|
||||
|
|
@ -67,6 +69,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[MemberData(nameof(ProtocolTestData))]
|
||||
public void ParseMessage(HubMessage expectedMessage, bool camelCase, NullValueHandling nullValueHandling, string input)
|
||||
{
|
||||
input = Frame(input);
|
||||
|
||||
var jsonSerializer = new JsonSerializer
|
||||
{
|
||||
NullValueHandling = nullValueHandling,
|
||||
|
|
@ -75,9 +79,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
|
||||
var binder = new TestBinder(expectedMessage);
|
||||
var protocol = new JsonHubProtocol(jsonSerializer);
|
||||
var message = protocol.ParseMessage(Encoding.UTF8.GetBytes(input), binder);
|
||||
protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, out var messages);
|
||||
|
||||
Assert.Equal(expectedMessage, message, TestEqualityComparer.Instance);
|
||||
Assert.Equal(expectedMessage, messages[0], TestEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -106,9 +110,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[InlineData("{'type':'foo'}", "Expected 'type' to be of type Integer.")]
|
||||
public void InvalidMessages(string input, string expectedMessage)
|
||||
{
|
||||
input = Frame(input);
|
||||
|
||||
var binder = new TestBinder();
|
||||
var protocol = new JsonHubProtocol(new JsonSerializer());
|
||||
var ex = Assert.Throws<FormatException>(() => protocol.ParseMessage(Encoding.UTF8.GetBytes(input), binder));
|
||||
var ex = Assert.Throws<FormatException>(() => protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, out var messages));
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
|
|
@ -118,12 +124,21 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[InlineData("{'type':3,'invocationId':'42','error':'foo','result':true}", "The 'error' and 'result' properties are mutually exclusive.")]
|
||||
public void InvalidMessagesWithBinder(string input, string expectedMessage)
|
||||
{
|
||||
input = Frame(input);
|
||||
|
||||
var binder = new TestBinder(paramTypes: new[] { typeof(int), typeof(string) }, returnType: typeof(bool));
|
||||
var protocol = new JsonHubProtocol(new JsonSerializer());
|
||||
var ex = Assert.Throws<FormatException>(() => protocol.ParseMessage(Encoding.UTF8.GetBytes(input), binder));
|
||||
var ex = Assert.Throws<FormatException>(() => protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, out var messages));
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
private static string Frame(string input)
|
||||
{
|
||||
input = $"{input.Length}:T:{input};";
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
private class CustomObject : IEquatable<CustomObject>
|
||||
{
|
||||
// Not intended to be a full set of things, just a smattering of sample serializations
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.10.3" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
var closeTcs = new TaskCompletionSource<object>();
|
||||
connection.Received += (data, format) =>
|
||||
connection.Received += data =>
|
||||
{
|
||||
logger.LogInformation("Received {length} byte message", data.Length);
|
||||
receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
|
|
@ -119,11 +119,9 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
logger.LogInformation("Sending {length} byte message", bytes.Length);
|
||||
await connection.SendAsync(bytes, MessageType.Text).OrTimeout();
|
||||
await connection.SendAsync(bytes).OrTimeout();
|
||||
logger.LogInformation("Sent message", bytes.Length);
|
||||
|
||||
var receiveData = new ReceiveData();
|
||||
|
||||
logger.LogInformation("Receiving message");
|
||||
Assert.Equal(message, await receiveTcs.Task.OrTimeout());
|
||||
logger.LogInformation("Completed receive");
|
||||
|
|
@ -162,7 +160,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
try
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<byte[]>();
|
||||
connection.Received += (data, messageType) =>
|
||||
connection.Received += data =>
|
||||
{
|
||||
logger.LogInformation("Received {length} byte message", data.Length);
|
||||
receiveTcs.TrySetResult(data);
|
||||
|
|
@ -174,11 +172,9 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
logger.LogInformation("Sending {length} byte message", bytes.Length);
|
||||
await connection.SendAsync(bytes, MessageType.Text).OrTimeout();
|
||||
await connection.SendAsync(bytes).OrTimeout();
|
||||
logger.LogInformation("Sent message", bytes.Length);
|
||||
|
||||
var receiveData = new ReceiveData();
|
||||
|
||||
logger.LogInformation("Receiving message");
|
||||
var receivedData = await receiveTcs.Task.OrTimeout();
|
||||
Assert.Equal(message, Encoding.UTF8.GetString(receivedData));
|
||||
|
|
|
|||
|
|
@ -22,20 +22,12 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
|||
/* 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,
|
||||
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
/* type: */ 0x03, // Close
|
||||
/* body: */ 0x41,
|
||||
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
|
||||
/* type: */ 0x02, // Error
|
||||
/* body: */ 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72
|
||||
};
|
||||
|
||||
var messages = new[]
|
||||
{
|
||||
MessageTestUtils.CreateMessage(new byte[0]),
|
||||
MessageTestUtils.CreateMessage("Hello,\r\nWorld!",MessageType.Text),
|
||||
MessageTestUtils.CreateMessage("A", MessageType.Close),
|
||||
MessageTestUtils.CreateMessage("Server Error", MessageType.Error)
|
||||
MessageTestUtils.CreateMessage("Hello,\r\nWorld!",MessageType.Text)
|
||||
};
|
||||
|
||||
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
|
||||
|
|
@ -73,10 +65,6 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
|||
[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(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03 }, MessageType.Close, "")]
|
||||
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x03, 0x43, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x64 }, MessageType.Close, "Connection Closed")]
|
||||
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 }, MessageType.Error, "")]
|
||||
[InlineData(0, 8, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72 }, MessageType.Error, "Server Error")]
|
||||
[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)
|
||||
|
|
@ -16,10 +16,6 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
[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")]
|
||||
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03 }, MessageType.Close, "")]
|
||||
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x03, 0x43, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x64 }, MessageType.Close, "Connection Closed")]
|
||||
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 }, MessageType.Error, "")]
|
||||
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72 }, MessageType.Error, "Server Error")]
|
||||
public void ReadTextMessage(byte[] encoded, MessageType messageType, string payload)
|
||||
{
|
||||
var parser = new MessageParser();
|
||||
|
|
@ -58,12 +54,6 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
/* 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,
|
||||
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
/* type: */ 0x03, // Close
|
||||
/* body: */ 0x41,
|
||||
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
|
||||
/* type: */ 0x02, // Error
|
||||
/* body: */ 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72
|
||||
};
|
||||
var parser = new MessageParser();
|
||||
var buffer = encoded.ToChunkedReadOnlyBytes(chunkSize);
|
||||
|
|
@ -77,11 +67,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
|
||||
Assert.Equal(encoded.Length, reader.Index);
|
||||
|
||||
Assert.Equal(4, messages.Count);
|
||||
Assert.Equal(2, messages.Count);
|
||||
MessageTestUtils.AssertMessage(messages[0], MessageType.Binary, new byte[0]);
|
||||
MessageTestUtils.AssertMessage(messages[1], MessageType.Text, "Hello,\r\nWorld!");
|
||||
MessageTestUtils.AssertMessage(messages[2], MessageType.Close, "A");
|
||||
MessageTestUtils.AssertMessage(messages[3], MessageType.Error, "Server Error");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -35,4 +35,4 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,62 +14,53 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
public class ServerSentEventsParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("data: T\r\n\r\n", "", MessageType.Text)]
|
||||
[InlineData("data: B\r\n\r\n", "", MessageType.Binary)]
|
||||
[InlineData("data: T\r\n\r\n:\r\n", "", MessageType.Text)]
|
||||
[InlineData("data: T\r\n\r\n:comment\r\n", "", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)]
|
||||
[InlineData("data: T\r\n:comment\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: A\rB\r\n\r\n", "A\rB", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\ndata: ", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\ndata: ", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\n", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n:comment\r\n\r\n", "Hello, World", MessageType.Text)]
|
||||
[InlineData("data: B\r\ndata: SGVsbG8sIFdvcmxk\r\n\r\n", "Hello, World", MessageType.Binary)]
|
||||
[InlineData("data: B\r\ndata: SGVsbG8g\r\ndata: V29ybGQ=\r\n\r\n", "Hello World", MessageType.Binary)]
|
||||
public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage, MessageType messageType)
|
||||
[InlineData("\r\n", "")]
|
||||
[InlineData("\r\n", "")]
|
||||
[InlineData("\r\n:\r\n", "")]
|
||||
[InlineData("\r\n:comment\r\n", "")]
|
||||
[InlineData("data: \r\r\n\r\n", "\r")]
|
||||
[InlineData(":comment\r\ndata: \r\r\n\r\n", "\r")]
|
||||
[InlineData("data: A\rB\r\n\r\n", "A\rB")]
|
||||
[InlineData("data: Hello, World\r\n\r\n", "Hello, World")]
|
||||
[InlineData("data: Hello, World\r\n\r\ndata: ", "Hello, World")]
|
||||
[InlineData("data: Hello, World\r\n\r\n:comment\r\ndata: ", "Hello, World")]
|
||||
[InlineData("data: Hello, World\r\n\r\n:comment", "Hello, World")]
|
||||
[InlineData("data: Hello, World\r\n\r\n:comment\r\n", "Hello, World")]
|
||||
[InlineData("data: Hello, World\r\n:comment\r\n\r\n", "Hello, World")]
|
||||
[InlineData("data: SGVsbG8sIFdvcmxk\r\n\r\n", "SGVsbG8sIFdvcmxk")]
|
||||
public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage)
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
|
||||
var readableBuffer = ReadableBuffer.Create(buffer);
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out var message);
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
|
||||
Assert.Equal(messageType, message.Type);
|
||||
Assert.Equal(consumed, examined);
|
||||
|
||||
var result = Encoding.UTF8.GetString(message.Payload);
|
||||
var result = Encoding.UTF8.GetString(message);
|
||||
Assert.Equal(expectedMessage, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("data: X\r\n", "Unknown message type: 'X'")]
|
||||
[InlineData("data: T\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data: X\r\n\r\n", "Unknown message type: 'X'")]
|
||||
[InlineData("data: Not the message type\r\n\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\r\n\n", "There was an error in the frame format")]
|
||||
[InlineData("data: Not the message type\r\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: T\r\ndata: Hello, World\n\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data: T\r\nfoo: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("foo: T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("food: T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\n", "There was an error in the frame format")]
|
||||
[InlineData("data: T\r\ndata: Hello\n, World\r\n\r\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data: data: \r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: T\r\ndata: Major\r\ndata: Key\rndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: T\r\ndata: Major\r\ndata: Key\r\ndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: This is not a message type\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: B\r\n SGVsbG8sIFdvcmxk\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("data: Hello, World\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: Major\r\ndata: Key\rndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: Major\r\ndata: Key\r\ndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
public void ParseSSEMessageFailureCases(string encodedMessage, string expectedExceptionMessage)
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
|
||||
var readableBuffer = ReadableBuffer.Create(buffer);
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
|
||||
var ex = Assert.Throws<FormatException>(() => { parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message); });
|
||||
var ex = Assert.Throws<FormatException>(() => { parser.ParseMessage(readableBuffer, out var consumed, out var examined, out var message); });
|
||||
Assert.Equal(expectedExceptionMessage, ex.Message);
|
||||
}
|
||||
|
||||
|
|
@ -97,33 +88,32 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
var readableBuffer = ReadableBuffer.Create(buffer);
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out var message);
|
||||
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[] { "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T", "\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r", "\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\n", "data: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\nd", "ata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\ndata: ", "Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\ndata: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", "\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: ", "T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { ":", "comment", "\r\n", "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\n", ":comment", "\r\n", "data: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", ":comment\r\n", "\r\n" }, "Hello, World", MessageType.Text)]
|
||||
[InlineData(new[] { "data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\r\n" }, "Hello, World", MessageType.Binary)]
|
||||
public async Task ParseMessageAcrossMultipleReadsSuccess(string[] messageParts, string expectedMessage, MessageType expectedMessageType)
|
||||
[InlineData(new[] { "d", "ata: Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "da", "ta: Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "dat", "a: Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data", ": Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data:", " Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data: ", "Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data: Hello, World", "\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data: Hello, World\r\n", "\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data: ", "Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { ":", "comment", "\r\n", "d", "ata: Hello, World\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { ":comment", "\r\n", "data: Hello, World", "\r\n\r\n" }, "Hello, World")]
|
||||
[InlineData(new[] { "data: Hello, World\r\n", ":comment\r\n", "\r\n" }, "Hello, World")]
|
||||
public async Task ParseMessageAcrossMultipleReadsSuccess(string[] messageParts, string expectedMessage)
|
||||
{
|
||||
using (var pipeFactory = new PipeFactory())
|
||||
{
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
var pipe = pipeFactory.Create();
|
||||
|
||||
Message message = default(Message);
|
||||
byte[] message = null;
|
||||
ReadCursor consumed = default(ReadCursor), examined = default(ReadCursor);
|
||||
|
||||
for (var i = 0; i < messageParts.Length; i++)
|
||||
|
|
@ -144,32 +134,24 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
Assert.Equal(expectedResult, parseResult);
|
||||
}
|
||||
|
||||
Assert.Equal(expectedMessageType, message.Type);
|
||||
Assert.Equal(consumed, examined);
|
||||
|
||||
var resultMessage = Encoding.UTF8.GetString(message.Payload);
|
||||
var resultMessage = Encoding.UTF8.GetString(message);
|
||||
Assert.Equal(expectedMessage, resultMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("data: ", "X\r\n", "Unknown message type: 'X'")]
|
||||
[InlineData("data: T", "\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data: ", "X\r\n\r\n", "Unknown message type: 'X'")]
|
||||
[InlineData("data: ", "Not the message type\r\n\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: T\r\n", "data: Hello, World\r\r\n\n", "There was an error in the frame format")]
|
||||
[InlineData("data:", " Not the message type\r\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: T\r\n", "data: Hello, World\n\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data: T\r\nf", "oo: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("foo", ": T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("food:", " T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
|
||||
[InlineData("data: T\r\ndata: Hello, W", "orld\r\n\n", "There was an error in the frame format")]
|
||||
[InlineData("data: T\r\nda", "ta: Hello\n, World\r\n\r\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
|
||||
[InlineData("data:", " data: \r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: ", "T\r\ndata: Major\r\ndata: Key\r\ndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
|
||||
[InlineData("data: ", "This is not a message type\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
[InlineData("data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\n\n", "There was an error in the frame format")]
|
||||
[InlineData("data: T", "his is not a message type\r\n", "Expected a data format message of the form 'data: <MesssageType>'")]
|
||||
public async Task ParseMessageAcrossMultipleReadsFailure(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage)
|
||||
{
|
||||
using (var pipeFactory = new PipeFactory())
|
||||
|
|
@ -182,7 +164,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
var result = await pipe.Reader.ReadAsync();
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
|
||||
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out Message message);
|
||||
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out var buffer);
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
|
||||
|
||||
pipe.Reader.Advance(consumed, examined);
|
||||
|
|
@ -191,16 +173,15 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart2));
|
||||
result = await pipe.Reader.ReadAsync();
|
||||
|
||||
var ex = Assert.Throws<FormatException>(() => parser.ParseMessage(result.Buffer, out consumed, out examined, out message));
|
||||
var ex = Assert.Throws<FormatException>(() => parser.ParseMessage(result.Buffer, out consumed, out examined, out buffer));
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("data: T\r\ndata: foo\r\n\r\n", "data: T\r\ndata: bar\r\n\r\n", MessageType.Text)]
|
||||
[InlineData("data: B\r\ndata: Zm9v\r\n\r\n", "data: B\r\ndata: YmFy\r\n\r\n", MessageType.Binary)]
|
||||
public async Task ParseMultipleMessagesText(string message1, string message2, MessageType expectedMessageType)
|
||||
[InlineData("data: foo\r\n\r\n", "data: bar\r\n\r\n")]
|
||||
public async Task ParseMultipleMessagesText(string message1, string message2)
|
||||
{
|
||||
using (var pipeFactory = new PipeFactory())
|
||||
{
|
||||
|
|
@ -214,8 +195,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
|
||||
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out var message);
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
|
||||
Assert.Equal(expectedMessageType, message.Type);
|
||||
Assert.Equal("foo", Encoding.UTF8.GetString(message.Payload));
|
||||
Assert.Equal("foo", Encoding.UTF8.GetString(message));
|
||||
Assert.Equal(consumed, result.Buffer.Move(result.Buffer.Start, message1.Length));
|
||||
pipe.Reader.Advance(consumed, examined);
|
||||
Assert.Equal(consumed, examined);
|
||||
|
|
@ -225,8 +205,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
result = await pipe.Reader.ReadAsync();
|
||||
parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
|
||||
Assert.Equal(expectedMessageType, message.Type);
|
||||
Assert.Equal("bar", Encoding.UTF8.GetString(message.Payload));
|
||||
Assert.Equal("bar", Encoding.UTF8.GetString(message));
|
||||
pipe.Reader.Advance(consumed, examined);
|
||||
}
|
||||
}
|
||||
|
|
@ -235,8 +214,8 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { "data: T\r\ndata: Shaolin\r\ndata: Fantastic\r\n\r\n", "Shaolin" + Environment.NewLine + " Fantastic", MessageType.Text };
|
||||
yield return new object[] { "data: T\r\ndata: 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", 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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,12 +227,11 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
var readableBuffer = ReadableBuffer.Create(buffer);
|
||||
var parser = new ServerSentEventsMessageParser();
|
||||
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
|
||||
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out var message);
|
||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
|
||||
Assert.Equal(expectedMessageType, message.Type);
|
||||
Assert.Equal(consumed, examined);
|
||||
|
||||
var result = Encoding.UTF8.GetString(message.Payload);
|
||||
var result = Encoding.UTF8.GetString(message);
|
||||
Assert.Equal(expectedMessage, result);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
|||
[Fact]
|
||||
public void WriteMultipleMessages()
|
||||
{
|
||||
const string expectedEncoding = "0:B:;14:T:Hello,\r\nWorld!;1:C:A;12:E:Server Error;";
|
||||
const string expectedEncoding = "0:B:;14:T:Hello,\r\nWorld!;";
|
||||
var messages = new[]
|
||||
{
|
||||
MessageTestUtils.CreateMessage(new byte[0]),
|
||||
MessageTestUtils.CreateMessage("Hello,\r\nWorld!",MessageType.Text),
|
||||
MessageTestUtils.CreateMessage("A", MessageType.Close),
|
||||
MessageTestUtils.CreateMessage("Server Error", MessageType.Error)
|
||||
};
|
||||
|
||||
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
|
||||
|
|
@ -52,10 +50,6 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
|||
[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(8, "0:C:;", MessageType.Close, "")]
|
||||
[InlineData(8, "17:C:Connection Closed;", MessageType.Close, "Connection Closed")]
|
||||
[InlineData(8, "0:E:;", MessageType.Error, "")]
|
||||
[InlineData(8, "12:E:Server Error;", MessageType.Error, "Server Error")]
|
||||
[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)
|
||||
{
|
||||
|
|
@ -17,10 +17,6 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
[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(0, "0:C:;", MessageType.Close, "")]
|
||||
[InlineData(0, "17:C:Connection Closed;", MessageType.Close, "Connection Closed")]
|
||||
[InlineData(0, "0:E:;", MessageType.Error, "")]
|
||||
[InlineData(0, "12:E:Server Error;", MessageType.Error, "Server Error")]
|
||||
[InlineData(4, "12:T:Hello, World;", MessageType.Text, "Hello, World")]
|
||||
public void ReadTextMessage(int chunkSize, string encoded, MessageType messageType, string payload)
|
||||
{
|
||||
|
|
@ -57,7 +53,7 @@ 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!;1:C:A;12:E:Server Error;";
|
||||
const string encoded = "0:B:;14:T:Hello,\r\nWorld!;";
|
||||
var parser = new MessageParser();
|
||||
var data = Encoding.UTF8.GetBytes(encoded);
|
||||
var buffer = chunkSize > 0 ?
|
||||
|
|
@ -74,11 +70,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
|||
|
||||
Assert.Equal(reader.Index, Encoding.UTF8.GetByteCount(encoded));
|
||||
|
||||
Assert.Equal(4, messages.Count);
|
||||
Assert.Equal(2, messages.Count);
|
||||
MessageTestUtils.AssertMessage(messages[0], MessageType.Binary, new byte[0]);
|
||||
MessageTestUtils.AssertMessage(messages[1], MessageType.Text, "Hello,\r\nWorld!");
|
||||
MessageTestUtils.AssertMessage(messages[2], MessageType.Close, "A");
|
||||
MessageTestUtils.AssertMessage(messages[3], MessageType.Error, "Server Error");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
private CancellationTokenSource _cts;
|
||||
|
||||
public DefaultConnectionContext Connection { get; }
|
||||
public IChannelConnection<Message> Application { get; }
|
||||
public IChannelConnection<byte[]> Application { get; }
|
||||
public Task Connected => Connection.Metadata.Get<TaskCompletionSource<bool>>("ConnectedTask").Task;
|
||||
|
||||
public TestClient()
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
Application = ChannelConnection.Create<Message>(input: applicationToTransport, output: transportToApplication);
|
||||
var transport = ChannelConnection.Create<Message>(input: transportToApplication, output: applicationToTransport);
|
||||
Application = ChannelConnection.Create<byte[]>(input: applicationToTransport, output: transportToApplication);
|
||||
var transport = ChannelConnection.Create<byte[]>(input: transportToApplication, output: applicationToTransport);
|
||||
|
||||
Connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), transport, Application);
|
||||
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, Interlocked.Increment(ref _id).ToString()) }));
|
||||
|
|
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var invocationId = GetInvocationId();
|
||||
var payload = await _protocol.WriteToArrayAsync(new InvocationMessage(invocationId, nonBlocking: false, target: methodName, arguments: args));
|
||||
|
||||
await Application.Output.WriteAsync(new Message(payload, _protocol.MessageType));
|
||||
await Application.Output.WriteAsync(payload);
|
||||
|
||||
return invocationId;
|
||||
}
|
||||
|
|
@ -137,9 +137,10 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
public HubMessage TryRead()
|
||||
{
|
||||
if (Application.Input.TryRead(out var message))
|
||||
if (Application.Input.TryRead(out var buffer) &&
|
||||
_protocol.TryParseMessages(buffer, this, out var messages))
|
||||
{
|
||||
return _protocol.ParseMessage(message.Payload, this);
|
||||
return messages[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using System;
|
|||
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.Testing.xunit;
|
||||
|
|
@ -33,8 +32,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
public async Task WebSocketsTransportStopsSendAndReceiveLoopsWhenTransportIsStopped()
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
|
||||
var webSocketsTransport = new WebSocketsTransport();
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), channelConnection);
|
||||
|
|
@ -47,8 +46,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
public async Task WebSocketsTransportStopsWhenConnectionChannelClosed()
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
|
||||
var webSocketsTransport = new WebSocketsTransport();
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), channelConnection);
|
||||
|
|
@ -61,20 +60,20 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer()
|
||||
{
|
||||
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||
var transportToConnection = Channel.CreateUnbounded<byte[]>();
|
||||
var channelConnection = new ChannelConnection<SendMessage, byte[]>(connectionToTransport, transportToConnection);
|
||||
|
||||
var webSocketsTransport = new WebSocketsTransport();
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), channelConnection);
|
||||
|
||||
var sendTcs = new TaskCompletionSource<object>();
|
||||
connectionToTransport.Out.TryWrite(new SendMessage(new byte[] { 0x42 }, MessageType.Binary, sendTcs));
|
||||
connectionToTransport.Out.TryWrite(new SendMessage(new byte[] { 0x42 }, sendTcs));
|
||||
await sendTcs.Task;
|
||||
// The echo endpoint close the connection immediately after sending response which should stop the transport
|
||||
await webSocketsTransport.Running.OrTimeout();
|
||||
|
||||
Assert.True(transportToConnection.In.TryRead(out var message));
|
||||
Assert.Equal(new byte[] { 0x42 }, message.Payload);
|
||||
Assert.True(transportToConnection.In.TryRead(out var buffer));
|
||||
Assert.Equal(new byte[] { 0x42 }, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +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.Text;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
||||
{
|
||||
public class ServerSentEventsMessageFormatterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("data: T\r\n\r\n", MessageType.Text, "")]
|
||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", MessageType.Text, "Hello, World")]
|
||||
[InlineData("data: T\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Text, "Hello\r\nWorld")]
|
||||
[InlineData("data: T\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Text, "Hello\nWorld")]
|
||||
[InlineData("data: T\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Text, "Hello\n")]
|
||||
[InlineData("data: T\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Text, "Hello\r\n")]
|
||||
[InlineData("data: C\r\n\r\n", MessageType.Close, "")]
|
||||
[InlineData("data: C\r\ndata: Hello, World\r\n\r\n", MessageType.Close, "Hello, World")]
|
||||
[InlineData("data: C\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Close, "Hello\r\nWorld")]
|
||||
[InlineData("data: C\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Close, "Hello\nWorld")]
|
||||
[InlineData("data: C\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Close, "Hello\n")]
|
||||
[InlineData("data: C\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Close, "Hello\r\n")]
|
||||
[InlineData("data: E\r\n\r\n", MessageType.Error, "")]
|
||||
[InlineData("data: E\r\ndata: Hello, World\r\n\r\n", MessageType.Error, "Hello, World")]
|
||||
[InlineData("data: E\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Error, "Hello\r\nWorld")]
|
||||
[InlineData("data: E\r\ndata: Hello\r\ndata: World\r\n\r\n", MessageType.Error, "Hello\nWorld")]
|
||||
[InlineData("data: E\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Error, "Hello\n")]
|
||||
[InlineData("data: E\r\ndata: Hello\r\ndata: \r\n\r\n", MessageType.Error, "Hello\r\n")]
|
||||
public void WriteTextMessage(string encoded, MessageType messageType, string payload)
|
||||
{
|
||||
var message = MessageTestUtils.CreateMessage(payload, messageType);
|
||||
|
||||
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
|
||||
Assert.True(ServerSentEventsMessageFormatter.TryWriteMessage(message, output));
|
||||
|
||||
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("data: B\r\n\r\n", new byte[0])]
|
||||
[InlineData("data: B\r\ndata: q83v\r\n\r\n", new byte[] { 0xAB, 0xCD, 0xEF })]
|
||||
public void WriteBinaryMessage(string encoded, byte[] payload)
|
||||
{
|
||||
var message = MessageTestUtils.CreateMessage(payload);
|
||||
|
||||
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
|
||||
Assert.True(ServerSentEventsMessageFormatter.TryWriteMessage(message, output));
|
||||
|
||||
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\build\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
|
||||
<Compile Include="..\Common\ByteArrayExtensions.cs" Link="ByteArrayExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets.Common\Microsoft.AspNetCore.Sockets.Common.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -176,35 +176,6 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendRequestsWithInvalidContentTypeAreRejected()
|
||||
{
|
||||
var manager = CreateConnectionManager();
|
||||
var connection = manager.CreateConnection();
|
||||
var dispatcher = new HttpConnectionDispatcher(manager, new LoggerFactory());
|
||||
using (var strm = new MemoryStream())
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
var services = new ServiceCollection();
|
||||
services.AddOptions();
|
||||
services.AddEndPoint<TestEndPoint>();
|
||||
context.Request.Path = "/foo";
|
||||
context.Request.Method = "POST";
|
||||
context.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
|
||||
context.Request.ContentType = "text/plain";
|
||||
context.Response.Body = strm;
|
||||
|
||||
var builder = new SocketBuilder(services.BuildServiceProvider());
|
||||
builder.UseEndPoint<TestEndPoint>();
|
||||
var app = builder.Build();
|
||||
await dispatcher.ExecuteAsync(context, new HttpSocketOptions(), app);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
await strm.FlushAsync();
|
||||
Assert.Equal("'text/plain' is not a valid Content-Type for send requests.", Encoding.UTF8.GetString(strm.ToArray()));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransportType.LongPolling, 204)]
|
||||
[InlineData(TransportType.WebSockets, 404)]
|
||||
|
|
@ -453,7 +424,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
|
||||
// Write to the transport so the poll yields
|
||||
await connection.Transport.Output.WriteAsync(new Message(buffer, MessageType.Text));
|
||||
await connection.Transport.Output.WriteAsync(buffer);
|
||||
|
||||
await task;
|
||||
|
||||
|
|
@ -485,7 +456,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
|
||||
// Write to the application
|
||||
await connection.Application.Output.WriteAsync(new Message(buffer, MessageType.Text));
|
||||
await connection.Application.Output.WriteAsync(buffer);
|
||||
|
||||
await task;
|
||||
|
||||
|
|
@ -515,7 +486,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
|
||||
// Write to the application
|
||||
await connection.Application.Output.WriteAsync(new Message(buffer, MessageType.Text));
|
||||
await connection.Application.Output.WriteAsync(buffer);
|
||||
|
||||
await task;
|
||||
|
||||
|
|
@ -548,7 +519,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
await task1.OrTimeout();
|
||||
|
||||
// Send a message from the app to complete Task 2
|
||||
await connection.Transport.Output.WriteAsync(new Message(Encoding.UTF8.GetBytes("Hello, World"), MessageType.Text));
|
||||
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World"));
|
||||
|
||||
await task2.OrTimeout();
|
||||
|
||||
|
|
@ -556,43 +527,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
Assert.Equal(StatusCodes.Status204NoContent, context1.Response.StatusCode);
|
||||
Assert.Equal(string.Empty, GetContentAsString(context1.Response.Body));
|
||||
Assert.Equal(StatusCodes.Status200OK, context2.Response.StatusCode);
|
||||
Assert.Equal("T12:T:Hello, World;", GetContentAsString(context2.Response.Body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TextContentType, null, "T12:T:Hello, World;", "Hello, World", MessageType.Text)]
|
||||
[InlineData(TextContentType, null, "T16:B:SGVsbG8sIFdvcmxk;", "Hello, World", MessageType.Binary)]
|
||||
[InlineData(TextContentType, null, "T12:E:Hello, World;", "Hello, World", MessageType.Error)]
|
||||
[InlineData(TextContentType, null, "T12:C:Hello, World;", "Hello, World", MessageType.Close)]
|
||||
[InlineData(BinaryContentType, null, "QgAAAAAAAAAMAEhlbGxvLCBXb3JsZA==", "Hello, World", MessageType.Text)]
|
||||
[InlineData(BinaryContentType, null, "QgAAAAAAAAAMAUhlbGxvLCBXb3JsZA==", "Hello, World", MessageType.Binary)]
|
||||
[InlineData(BinaryContentType, null, "QgAAAAAAAAAMAkhlbGxvLCBXb3JsZA==", "Hello, World", MessageType.Error)]
|
||||
[InlineData(BinaryContentType, null, "QgAAAAAAAAAMA0hlbGxvLCBXb3JsZA==", "Hello, World", MessageType.Close)]
|
||||
public async Task SendPutsPayloadsInTheChannel(string contentType, string format, string encoded, string payload, MessageType type)
|
||||
{
|
||||
var messages = await RunSendTest(contentType, encoded, format);
|
||||
|
||||
Assert.Equal(1, messages.Count);
|
||||
Assert.Equal(payload, Encoding.UTF8.GetString(messages[0].Payload));
|
||||
Assert.Equal(type, messages[0].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TextContentType, "T12:T:Hello, World;16:B:SGVsbG8sIFdvcmxk;5:E:Error;6:C:Closed;")]
|
||||
[InlineData(BinaryContentType, "QgAAAAAAAAAMAEhlbGxvLCBXb3JsZAAAAAAAAAAMAUhlbGxvLCBXb3JsZAAAAAAAAAAFAkVycm9yAAAAAAAAAAYDQ2xvc2Vk")]
|
||||
public async Task SendAllowsMultipleMessages(string contentType, string encoded)
|
||||
{
|
||||
var messages = await RunSendTest(contentType, encoded, format: null);
|
||||
|
||||
Assert.Equal(4, messages.Count);
|
||||
Assert.Equal("Hello, World", Encoding.UTF8.GetString(messages[0].Payload));
|
||||
Assert.Equal(MessageType.Text, messages[0].Type);
|
||||
Assert.Equal("Hello, World", Encoding.UTF8.GetString(messages[1].Payload));
|
||||
Assert.Equal(MessageType.Binary, messages[1].Type);
|
||||
Assert.Equal("Error", Encoding.UTF8.GetString(messages[2].Payload));
|
||||
Assert.Equal(MessageType.Error, messages[2].Type);
|
||||
Assert.Equal("Closed", Encoding.UTF8.GetString(messages[3].Payload));
|
||||
Assert.Equal(MessageType.Close, messages[3].Type);
|
||||
Assert.Equal("Hello, World", GetContentAsString(context2.Response.Body));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -713,12 +648,12 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") }));
|
||||
|
||||
var endPointTask = dispatcher.ExecuteAsync(context, options, app);
|
||||
await connection.Transport.Output.WriteAsync(new Message(Encoding.UTF8.GetBytes("Hello, World"), MessageType.Text)).OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).OrTimeout();
|
||||
|
||||
await endPointTask.OrTimeout();
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
Assert.Equal("T12:T:Hello, World;", GetContentAsString(context.Response.Body));
|
||||
Assert.Equal("Hello, World", GetContentAsString(context.Response.Body));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -763,12 +698,12 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") }));
|
||||
|
||||
var endPointTask = dispatcher.ExecuteAsync(context, options, app);
|
||||
await connection.Transport.Output.WriteAsync(new Message(Encoding.UTF8.GetBytes("Hello, World"), MessageType.Text)).OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).OrTimeout();
|
||||
|
||||
await endPointTask.OrTimeout();
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
Assert.Equal("T12:T:Hello, World;", GetContentAsString(context.Response.Body));
|
||||
Assert.Equal("Hello, World", GetContentAsString(context.Response.Body));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -909,40 +844,6 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task<List<Message>> RunSendTest(string contentType, string encoded, string format)
|
||||
{
|
||||
var manager = CreateConnectionManager();
|
||||
var connection = manager.CreateConnection();
|
||||
|
||||
var dispatcher = new HttpConnectionDispatcher(manager, new LoggerFactory());
|
||||
|
||||
var context = MakeRequest("/foo", connection, format);
|
||||
context.Request.Method = "POST";
|
||||
context.Request.ContentType = contentType;
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddEndPoint<TestEndPoint>();
|
||||
var builder = new SocketBuilder(services.BuildServiceProvider());
|
||||
builder.UseEndPoint<TestEndPoint>();
|
||||
var app = builder.Build();
|
||||
|
||||
var buffer = contentType == BinaryContentType ?
|
||||
Convert.FromBase64String(encoded) :
|
||||
Encoding.UTF8.GetBytes(encoded);
|
||||
var messages = new List<Message>();
|
||||
using (context.Request.Body = new MemoryStream(buffer, writable: false))
|
||||
{
|
||||
await dispatcher.ExecuteAsync(context, new HttpSocketOptions(), app).OrTimeout();
|
||||
}
|
||||
|
||||
while (connection.Transport.Input.TryRead(out var message))
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static DefaultHttpContext MakeRequest(string path, DefaultConnectionContext connection, string format = null)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task Set204StatusCodeWhenChannelComplete()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
var poll = new LongPollingTransport(channel, new LoggerFactory());
|
||||
|
||||
|
|
@ -34,48 +34,35 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task FrameSentAsSingleResponse()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
var poll = new LongPollingTransport(channel, new LoggerFactory());
|
||||
var ms = new MemoryStream();
|
||||
context.Response.Body = ms;
|
||||
|
||||
await channel.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes("Hello World"),
|
||||
MessageType.Text));
|
||||
await channel.Out.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
|
||||
|
||||
Assert.True(channel.Out.TryComplete());
|
||||
|
||||
await poll.ProcessRequestAsync(context, context.RequestAborted);
|
||||
|
||||
Assert.Equal(200, context.Response.StatusCode);
|
||||
Assert.Equal("T11:T:Hello World;", Encoding.UTF8.GetString(ms.ToArray()));
|
||||
Assert.Equal("Hello World", Encoding.UTF8.GetString(ms.ToArray()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MessageFormat.Text, "T5:T:Hello;1:T: ;5:T:World;")]
|
||||
[InlineData(MessageFormat.Binary, "QgAAAAAAAAAFAEhlbGxvAAAAAAAAAAEAIAAAAAAAAAAFAFdvcmxk")]
|
||||
public async Task MultipleFramesSentAsSingleResponse(MessageFormat format, string expectedPayload)
|
||||
[Fact]
|
||||
public async Task MultipleFramesSentAsSingleResponse()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
if (format == MessageFormat.Binary)
|
||||
{
|
||||
context.Request.Headers["Accept"] = MessageFormatter.BinaryContentType;
|
||||
}
|
||||
|
||||
var poll = new LongPollingTransport(channel, new LoggerFactory());
|
||||
var ms = new MemoryStream();
|
||||
context.Response.Body = ms;
|
||||
|
||||
await channel.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes("Hello"),
|
||||
MessageType.Text));
|
||||
await channel.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes(" "),
|
||||
MessageType.Text));
|
||||
await channel.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes("World"),
|
||||
MessageType.Text));
|
||||
await channel.Out.WriteAsync(Encoding.UTF8.GetBytes("Hello"));
|
||||
await channel.Out.WriteAsync(Encoding.UTF8.GetBytes(" "));
|
||||
await channel.Out.WriteAsync(Encoding.UTF8.GetBytes("World"));
|
||||
|
||||
Assert.True(channel.Out.TryComplete());
|
||||
|
||||
|
|
@ -84,10 +71,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
Assert.Equal(200, context.Response.StatusCode);
|
||||
|
||||
var payload = ms.ToArray();
|
||||
var encoded = format == MessageFormat.Binary ?
|
||||
Convert.ToBase64String(payload) :
|
||||
Encoding.UTF8.GetString(payload);
|
||||
Assert.Equal(expectedPayload, encoded);
|
||||
Assert.Equal("Hello World", Encoding.UTF8.GetString(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Common\ArrayOutput.cs" Link="ArrayOutput.cs" />
|
||||
<Compile Include="..\Common\TaskExtensions.cs" Link="TaskExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
// 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.Text;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
||||
{
|
||||
public class ServerSentEventsMessageFormatterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("\r\n", "")]
|
||||
[InlineData("data: Hello, World\r\n\r\n", "Hello, World")]
|
||||
[InlineData("data: Hello\r\ndata: World\r\n\r\n", "Hello\r\nWorld")]
|
||||
[InlineData("data: Hello\r\ndata: World\r\n\r\n", "Hello\nWorld")]
|
||||
[InlineData("data: Hello\r\ndata: \r\n\r\n", "Hello\n")]
|
||||
[InlineData("data: Hello\r\ndata: \r\n\r\n", "Hello\r\n")]
|
||||
public void WriteTextMessage(string encoded, string payload)
|
||||
{
|
||||
var output = new ArrayOutput(chunkSize: 8); // Use small chunks to test Advance/Enlarge and partial payload writing
|
||||
Assert.True(ServerSentEventsMessageFormatter.TryWriteMessage(Encoding.UTF8.GetBytes(payload), output));
|
||||
|
||||
Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task SSESetsContentType()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
var sse = new ServerSentEventsTransport(channel, new LoggerFactory());
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task SSETurnsResponseBufferingOff()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
var feature = new HttpBufferingFeature();
|
||||
context.Features.Set<IHttpBufferingFeature>(feature);
|
||||
|
|
@ -49,20 +49,18 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Hello World", ":\r\ndata: T\r\ndata: Hello World\r\n\r\n")]
|
||||
[InlineData("Hello\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
|
||||
[InlineData("Hello\r\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
|
||||
[InlineData("Hello World", ":\r\ndata: Hello World\r\n\r\n")]
|
||||
[InlineData("Hello\nWorld", ":\r\ndata: Hello\r\ndata: World\r\n\r\n")]
|
||||
[InlineData("Hello\r\nWorld", ":\r\ndata: Hello\r\ndata: World\r\n\r\n")]
|
||||
public async Task SSEAddsAppropriateFraming(string message, string expected)
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<Message>();
|
||||
var channel = Channel.CreateUnbounded<byte[]>();
|
||||
var context = new DefaultHttpContext();
|
||||
var sse = new ServerSentEventsTransport(channel, new LoggerFactory());
|
||||
var ms = new MemoryStream();
|
||||
context.Response.Body = ms;
|
||||
|
||||
await channel.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes(message),
|
||||
MessageType.Text));
|
||||
await channel.Out.WriteAsync(Encoding.UTF8.GetBytes(message));
|
||||
|
||||
Assert.True(channel.Out.TryComplete());
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
|
||||
public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
|
||||
{
|
||||
await _output.WriteAsync(new WebSocketMessage
|
||||
await SendMessageAsync(new WebSocketMessage
|
||||
{
|
||||
CloseStatus = closeStatus,
|
||||
CloseStatusDescription = statusDescription,
|
||||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
|
||||
public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
|
||||
{
|
||||
await _output.WriteAsync(new WebSocketMessage
|
||||
await SendMessageAsync(new WebSocketMessage
|
||||
{
|
||||
CloseStatus = closeStatus,
|
||||
CloseStatusDescription = statusDescription,
|
||||
|
|
@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
{
|
||||
var copy = new byte[buffer.Count];
|
||||
Buffer.BlockCopy(buffer.Array, buffer.Offset, copy, 0, buffer.Count);
|
||||
return _output.WriteAsync(new WebSocketMessage
|
||||
return SendMessageAsync(new WebSocketMessage
|
||||
{
|
||||
Buffer = copy,
|
||||
MessageType = messageType,
|
||||
|
|
@ -151,6 +151,17 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
_closeStatus = WebSocketCloseStatus.InternalServerError;
|
||||
return new WebSocketConnectionSummary(frames, new WebSocketReceiveResult(0, WebSocketMessageType.Close, endOfMessage: true, closeStatus: WebSocketCloseStatus.InternalServerError, closeStatusDescription: ""));
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(WebSocketMessage webSocketMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _output.WaitToWriteAsync(cancellationToken))
|
||||
{
|
||||
if (_output.TryWrite(webSocketMessage))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WebSocketConnectionSummary
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
public class WebSocketsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(MessageType.Text, WebSocketMessageType.Text)]
|
||||
[InlineData(MessageType.Binary, WebSocketMessageType.Binary)]
|
||||
public async Task ReceivedFramesAreWrittenToChannel(MessageType format, WebSocketMessageType webSocketMessageType)
|
||||
[InlineData(WebSocketMessageType.Text)]
|
||||
[InlineData(WebSocketMessageType.Binary)]
|
||||
public async Task ReceivedFramesAreWrittenToChannel(WebSocketMessageType webSocketMessageType)
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
|
|
@ -46,9 +46,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
cancellationToken: CancellationToken.None);
|
||||
await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
|
||||
var message = await applicationSide.Input.In.ReadAsync();
|
||||
Assert.Equal(format, message.Type);
|
||||
Assert.Equal("Hello", Encoding.UTF8.GetString(message.Payload));
|
||||
var buffer = await applicationSide.Input.In.ReadAsync();
|
||||
Assert.Equal("Hello", Encoding.UTF8.GetString(buffer));
|
||||
|
||||
Assert.True(applicationSide.Output.Out.TryComplete());
|
||||
|
||||
|
|
@ -63,19 +62,19 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MessageType.Text, WebSocketMessageType.Text)]
|
||||
[InlineData(MessageType.Binary, WebSocketMessageType.Binary)]
|
||||
public async Task DataWrittenToOutputPipelineAreSentAsFrames(MessageType format, WebSocketMessageType webSocketMessageType)
|
||||
[InlineData(WebSocketMessageType.Text)]
|
||||
[InlineData(WebSocketMessageType.Binary)]
|
||||
public async Task DataWrittenToOutputPipelineAreSentAsFrames(WebSocketMessageType webSocketMessageType)
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
var ws = new WebSocketsTransport(new WebSocketOptions(), transportSide, new LoggerFactory());
|
||||
var ws = new WebSocketsTransport(new WebSocketOptions() { WebSocketMessageType = webSocketMessageType }, transportSide, new LoggerFactory());
|
||||
|
||||
// Give the server socket to the transport and run it
|
||||
var transport = ws.ProcessSocketAsync(await feature.AcceptAsync());
|
||||
|
|
@ -84,9 +83,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
var client = feature.Client.ExecuteAndCaptureFramesAsync();
|
||||
|
||||
// Write to the output channel, and then complete it
|
||||
await applicationSide.Output.Out.WriteAsync(new Message(
|
||||
Encoding.UTF8.GetBytes("Hello"),
|
||||
format));
|
||||
await applicationSide.Output.Out.WriteAsync(Encoding.UTF8.GetBytes("Hello"));
|
||||
Assert.True(applicationSide.Output.Out.TryComplete());
|
||||
|
||||
// The client should finish now, as should the server
|
||||
|
|
@ -102,19 +99,19 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MessageType.Text, WebSocketMessageType.Text)]
|
||||
[InlineData(MessageType.Binary, WebSocketMessageType.Binary)]
|
||||
public async Task FrameReceivedAfterServerCloseSent(MessageType format, WebSocketMessageType webSocketMessageType)
|
||||
[InlineData(WebSocketMessageType.Text)]
|
||||
[InlineData(WebSocketMessageType.Binary)]
|
||||
public async Task FrameReceivedAfterServerCloseSent(WebSocketMessageType webSocketMessageType)
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
var ws = new WebSocketsTransport(new WebSocketOptions(), transportSide, new LoggerFactory());
|
||||
var ws = new WebSocketsTransport(new WebSocketOptions() { WebSocketMessageType = webSocketMessageType }, transportSide, new LoggerFactory());
|
||||
|
||||
// Give the server socket to the transport and run it
|
||||
var transport = ws.ProcessSocketAsync(await feature.AcceptAsync());
|
||||
|
|
@ -135,9 +132,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
|
||||
// Read that frame from the input
|
||||
var message = await applicationSide.Input.In.ReadAsync();
|
||||
Assert.Equal(format, message.Type);
|
||||
Assert.Equal("Hello", Encoding.UTF8.GetString(message.Payload));
|
||||
var buffer = await applicationSide.Input.In.ReadAsync();
|
||||
Assert.Equal("Hello", Encoding.UTF8.GetString(buffer));
|
||||
|
||||
await transport;
|
||||
}
|
||||
|
|
@ -146,11 +142,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task TransportFailsWhenClientDisconnectsAbnormally()
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
|
|
@ -173,11 +169,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task ClientReceivesInternalServerErrorWhenTheApplicationFails()
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
|
|
@ -204,11 +200,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[Fact]
|
||||
public async Task TransportClosesOnCloseTimeoutIfClientDoesNotSendCloseFrame()
|
||||
{
|
||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||
var transportToApplication = Channel.CreateUnbounded<byte[]>();
|
||||
var applicationToTransport = Channel.CreateUnbounded<byte[]>();
|
||||
|
||||
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||
var transportSide = new ChannelConnection<byte[]>(applicationToTransport, transportToApplication);
|
||||
var applicationSide = new ChannelConnection<byte[]>(transportToApplication, applicationToTransport);
|
||||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
|
|
@ -227,7 +223,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
applicationSide.Dispose();
|
||||
|
||||
await transport.OrTimeout(TimeSpan.FromSeconds(10));
|
||||
|
||||
|
||||
// Now we're closed
|
||||
Assert.Equal(WebSocketState.Aborted, serverSocket.State);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue