Remove support for binary over SSE and add transfer format to negotiation (#1564)

This commit is contained in:
Andrew Stanton-Nurse 2018-03-13 14:29:32 -07:00 committed by GitHub
parent adb760210d
commit fb6121399c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 937 additions and 1320 deletions

View File

@ -5,7 +5,6 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
@ -40,14 +39,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
protocol = new MessagePackHubProtocol();
}
var encoder = new PassThroughEncoder();
for (var i = 0; i < Connections; ++i)
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport);
var hubConnection = new HubConnectionContext(connection, Timeout.InfiniteTimeSpan, NullLoggerFactory.Instance);
hubConnection.ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, encoder);
hubConnection.Protocol = protocol;
_hubLifetimeManager.OnConnectedAsync(hubConnection).Wait();
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -11,12 +11,11 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using DefaultConnectionContext = Microsoft.AspNetCore.Sockets.DefaultConnectionContext;
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
@ -47,13 +46,13 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
_connectionContext = new NoErrorHubConnectionContext(connection, TimeSpan.Zero, NullLoggerFactory.Instance);
_connectionContext.ProtocolReaderWriter = new HubProtocolReaderWriter(new FakeHubProtocol(), new FakeDataEncoder());
_connectionContext.Protocol = new FakeHubProtocol();
}
public class FakeHubProtocol : IHubProtocol
{
public string Name { get; }
public ProtocolType Type { get; }
public TransferFormat TransferFormat { get; }
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{
@ -65,19 +64,6 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
}
}
public class FakeDataEncoder : IDataEncoder
{
public byte[] Encode(byte[] payload)
{
return null;
}
public bool TryDecode(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> data)
{
return false;
}
}
public class NoErrorHubConnectionContext : HubConnectionContext
{
public NoErrorHubConnectionContext(ConnectionContext connectionContext, TimeSpan keepAliveInterval, ILoggerFactory loggerFactory) : base(connectionContext, keepAliveInterval, loggerFactory)
@ -232,4 +218,4 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
return _dispatcher.DispatchMessageAsync(_connectionContext, new StreamInvocationMessage("123", "StreamChannelReaderValueTaskAsync", null));
}
}
}
}

View File

@ -2,16 +2,17 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
{
public class HubProtocolBenchmark
{
private HubProtocolReaderWriter _hubProtocolReaderWriter;
private IHubProtocol _hubProtocol;
private byte[] _binaryInput;
private TestBinder _binder;
private HubMessage _hubMessage;
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
switch (HubProtocol)
{
case Protocol.MsgPack:
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new MessagePackHubProtocol(), new PassThroughEncoder());
_hubProtocol = new MessagePackHubProtocol();
break;
case Protocol.Json:
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder());
_hubProtocol = new JsonHubProtocol();
break;
}
@ -51,14 +52,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
break;
}
_binaryInput = GetBytes(_hubMessage);
_binaryInput = _hubProtocol.WriteToArray(_hubMessage);
_binder = new TestBinder(_hubMessage);
}
[Benchmark]
public void ReadSingleMessage()
{
if (!_hubProtocolReaderWriter.ReadMessages(_binaryInput, _binder, out var _))
var messages = new List<HubMessage>();
if (!_hubProtocol.TryParseMessages(_binaryInput, _binder, messages))
{
throw new InvalidOperationException("Failed to read message");
}
@ -67,7 +69,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
[Benchmark]
public void WriteSingleMessage()
{
if (_hubProtocolReaderWriter.WriteMessage(_hubMessage).Length != _binaryInput.Length)
var bytes = _hubProtocol.WriteToArray(_hubMessage);
if (bytes.Length != _binaryInput.Length)
{
throw new InvalidOperationException("Failed to write message");
}
@ -86,10 +89,5 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
ManyArguments = 2,
LargeArguments = 3
}
private byte[] GetBytes(HubMessage hubMessage)
{
return _hubProtocolReaderWriter.WriteMessage(_hubMessage);
}
}
}

View File

@ -521,6 +521,42 @@ describe("hubConnection", () => {
});
});
if (typeof EventSource !== "undefined") {
it("allows Server-Sent Events when negotiating for JSON protocol", async (done) => {
const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, {
logger: LogLevel.Trace,
protocol: new JsonHubProtocol(),
});
try {
await hubConnection.start();
// Check what transport was used by asking the server to tell us.
expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("ServerSentEvents");
done();
} catch (e) {
fail(e);
}
});
}
it("skips Server-Sent Events when negotiating for MsgPack protocol", async (done) => {
const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, {
logger: LogLevel.Trace,
protocol: new MessagePackHubProtocol(),
});
try {
await hubConnection.start();
// Check what transport was used by asking the server to tell us.
expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("LongPolling");
done();
} catch (e) {
fail(e);
}
});
function getJwtToken(url): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

View File

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
-->
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
</system.webServer>
</configuration>
</configuration>

View File

@ -1,9 +1,18 @@
{
"name": "@aspnet/signalr-protocol-msgpack",
"version": "1.0.0-preview2-t000",
"version": "1.0.0-preview1-t000",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@aspnet/signalr": {
"version": "file:../signalr",
"dependencies": {
"es6-promise": {
"version": "4.2.2",
"bundled": true
}
}
},
"@types/bl": {
"version": "0.8.31",
"resolved": "https://registry.npmjs.org/@types/bl/-/bl-0.8.31.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr-protocol-msgpack",
"version": "1.0.0-preview1-t000",
"version": "1.0.0-preview2-t000",
"description": "MsgPack Protocol support for ASP.NET Core SignalR",
"main": "./dist/cjs/index.js",
"browser": "./dist/browser/signalr-protocol-msgpack.js",
@ -18,8 +18,8 @@
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5",
"build:browser": "node ../node_modules/rollup/bin/rollup -c",
"build:uglify": "node ../node_modules/uglify-js/bin/uglifyjs --source-map \"url='signalr-protocol-msgpack.min.js.map',content='./dist/browser/signalr-protocol-msgpack.js.map'\" --comments -o ./dist/browser/signalr-protocol-msgpack.min.js ./dist/browser/signalr-protocol-msgpack.js",
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json",
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/signalr-protocol-msgpack/spec/**/*.spec.js"
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json && cd ./spec/obj/src && npm init -y && npm install ../../../../signalr",
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
},
"keywords": [
"signalr",

View File

@ -8,7 +8,7 @@
"lib": [ "es2015", "dom" ],
"baseUrl": ".",
"paths": {
"@aspnet/signalr": [ "../../signalr/src/index" ]
"@aspnet/*": [ "../../*" ]
}
},
"include": [

View File

@ -1,7 +1,7 @@
// 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.
import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "@aspnet/signalr";
import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, StreamInvocationMessage, StreamItemMessage, TransferFormat } from "@aspnet/signalr";
import { Buffer } from "buffer";
import * as msgpack5 from "msgpack5";
import { BinaryMessageFormat } from "./BinaryMessageFormat";
@ -10,7 +10,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
public readonly name: string = "messagepack";
public readonly type: ProtocolType = ProtocolType.Binary;
public readonly transferFormat: TransferFormat = TransferFormat.Binary;
public parseMessages(input: ArrayBuffer): HubMessage[] {
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m));

View File

@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr",
"version": "1.0.0-preview2-t000",
"version": "1.0.0-preview1-t000",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr",
"version": "1.0.0-preview1-t000",
"version": "1.0.0-preview2-t000",
"description": "ASP.NET Core SignalR Client",
"main": "./dist/cjs/index.js",
"browser": "./dist/browser/signalr.js",

View File

@ -1,74 +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.
import { Base64EncodedHubProtocol } from "../src/Base64EncodedHubProtocol";
import { HubMessage, IHubProtocol, ProtocolType } from "../src/IHubProtocol";
class FakeHubProtocol implements IHubProtocol {
public name: "fakehubprotocol";
public type: ProtocolType;
public parseMessages(input: any): HubMessage[] {
let s = "";
new Uint8Array(input).forEach((item: any) => {
s += String.fromCharCode(item);
});
return JSON.parse(s);
}
public writeMessage(message: HubMessage): any {
const s = JSON.stringify(message);
const payload = new Uint8Array(s.length);
for (let i = 0; i < payload.length; i++) {
payload[i] = s.charCodeAt(i);
}
return payload;
}
}
describe("Base64EncodedHubProtocol", () => {
([
["ABC", new Error("Invalid payload.")],
["3:ABC", new Error("Invalid payload.")],
[":;", new Error("Invalid length: ''")],
["1.0:A;", new Error("Invalid length: '1.0'")],
["2:A;", new Error("Invalid message size.")],
["2:ABC;", new Error("Invalid message size.")],
] as Array<[string, Error]>).forEach(([payload, expectedError]) => {
it(`should fail to parse '${payload}'`, () => {
expect(() => new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload)).toThrow(expectedError);
});
});
([
["2:{};", {}],
] as [[string, any]]).forEach(([payload, message]) => {
it(`should be able to parse '${payload}'`, () => {
const globalAny: any = global;
globalAny.atob = (input: any) => input;
const result = new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload);
expect(result).toEqual(message);
delete globalAny.atob;
});
});
([
[{}, "2:{};"],
] as Array<[any, string]>).forEach(([message, payload]) => {
it(`should be able to write '${JSON.stringify(message)}'`, () => {
const globalAny: any = global;
globalAny.btoa = (input: any) => input;
const result = new Base64EncodedHubProtocol(new FakeHubProtocol()).writeMessage(message);
expect(result).toEqual(payload);
delete globalAny.btoa;
});
});
});

View File

@ -5,7 +5,7 @@ import { DataReceived, TransportClosed } from "../src/Common";
import { HttpConnection } from "../src/HttpConnection";
import { IHttpConnectionOptions } from "../src/HttpConnection";
import { HttpResponse } from "../src/index";
import { ITransport, TransferMode, TransportType } from "../src/Transports";
import { ITransport, TransferFormat, TransportType } from "../src/Transports";
import { eachEndpointUrl, eachTransport } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
@ -15,13 +15,13 @@ const commonOptions: IHttpConnectionOptions = {
describe("HttpConnection", () => {
it("cannot be created with relative url if document object is not present", () => {
expect(() => new HttpConnection("/test", commonOptions))
expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions))
.toThrow(new Error("Cannot resolve '/test'."));
});
it("cannot be created with relative url if window object is not present", () => {
(global as any).window = {};
expect(() => new HttpConnection("/test", commonOptions))
expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions))
.toThrow(new Error("Cannot resolve '/test'."));
delete (global as any).window;
});
@ -34,7 +34,7 @@ describe("HttpConnection", () => {
.on("GET", (r) => ""),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
@ -64,7 +64,7 @@ describe("HttpConnection", () => {
}),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
@ -86,7 +86,7 @@ describe("HttpConnection", () => {
.on("GET", (r) => ""),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
@ -117,7 +117,7 @@ describe("HttpConnection", () => {
}),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
@ -129,17 +129,17 @@ describe("HttpConnection", () => {
});
it("can stop a non-started connection", async (done) => {
const connection = new HttpConnection("http://tempuri.org", commonOptions);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, commonOptions);
await connection.stop();
done();
});
it("preserves users connection string", async (done) => {
it("preserves user's query string", async (done) => {
let connectUrl: string;
const fakeTransport: ITransport = {
connect(url: string): Promise<TransferMode> {
connect(url: string): Promise<void> {
connectUrl = url;
return Promise.reject(TransferMode.Text);
return Promise.reject("");
},
send(data: any): Promise<void> {
return Promise.reject("");
@ -159,7 +159,7 @@ describe("HttpConnection", () => {
transport: fakeTransport,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
const connection = new HttpConnection("http://tempuri.org?q=myData", TransferFormat.Text, options);
try {
await connection.start();
@ -190,7 +190,7 @@ describe("HttpConnection", () => {
}),
} as IHttpConnectionOptions;
connection = new HttpConnection(givenUrl, options);
connection = new HttpConnection(givenUrl, TransferFormat.Text, options);
try {
await connection.start();
@ -213,18 +213,18 @@ describe("HttpConnection", () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
.on("GET", (r) => ""),
transport: requestedTransport,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
fail();
done();
} catch (e) {
expect(e.message).toBe("No available transports found.");
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
});
@ -234,17 +234,17 @@ describe("HttpConnection", () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
.on("GET", (r) => ""),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
fail();
done();
} catch (e) {
expect(e.message).toBe("No available transports found.");
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
});
@ -256,7 +256,7 @@ describe("HttpConnection", () => {
transport: TransportType.WebSockets,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try {
await connection.start();
fail();
@ -264,42 +264,24 @@ describe("HttpConnection", () => {
} catch (e) {
// WebSocket is created when the transport is connecting which happens after
// negotiate request would be sent. No better/easier way to test this.
expect(e.message).toBe("WebSocket is not defined");
expect(e.message).toBe("'WebSocket' is not supported in your environment.");
done();
}
});
[
[TransferMode.Text, TransferMode.Text],
[TransferMode.Text, TransferMode.Binary],
[TransferMode.Binary, TransferMode.Text],
[TransferMode.Binary, TransferMode.Binary],
].forEach(([requestedTransferMode, transportTransferMode]) => {
it(`connection returns ${transportTransferMode} transfer mode when ${requestedTransferMode} transfer mode is requested`, async () => {
const fakeTransport = {
// mode: TransferMode : TransferMode.Text
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> { return Promise.resolve(transportTransferMode); },
mode: transportTransferMode,
onclose: null,
onreceive: null,
send(data: any): Promise<void> { return Promise.resolve(); },
stop(): Promise<void> { return Promise.resolve(); },
} as ITransport;
describe(".constructor", () => {
it("throws if no Url is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :)
expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
});
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("GET", (r) => ""),
transport: fakeTransport,
} as IHttpConnectionOptions;
it("throws if no TransferFormat is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :)
expect(() => new (HttpConnection as any)("http://tempuri.org")).toThrowError("The 'transferFormat' argument is required.");
});
const connection = new HttpConnection("https://tempuri.org", options);
connection.features.transferMode = requestedTransferMode;
await connection.start();
const actualTransferMode = connection.features.transferMode;
expect(actualTransferMode).toBe(transportTransferMode);
it("throws if an unsupported TransferFormat is provided", async () => {
expect(() => new HttpConnection("http://tempuri.org", 42)).toThrowError("Unknown transferFormat value: 42.");
});
});
});

View File

@ -8,7 +8,7 @@ import { MessageType } from "../src/IHubProtocol";
import { ILogger, LogLevel } from "../src/ILogger";
import { Observer } from "../src/Observable";
import { TextMessageFormat } from "../src/TextMessageFormat";
import { ITransport, TransferMode, TransportType } from "../src/Transports";
import { ITransport, TransferFormat, TransportType } from "../src/Transports";
import { IHubConnectionOptions } from "../src/HubConnection";
import { asyncit as it, captureException, delay, PromiseSource } from "./Utils";

View File

@ -3,7 +3,7 @@
import { HttpClient, HttpRequest, HttpResponse } from "../src/HttpClient";
type TestHttpHandlerResult = HttpResponse | string;
type TestHttpHandlerResult = any;
export type TestHttpHandler = (request: HttpRequest, next?: (request: HttpRequest) => Promise<HttpResponse>) => Promise<TestHttpHandlerResult> | TestHttpHandlerResult;
export class TestHttpClient extends HttpClient {
@ -57,9 +57,14 @@ export class TestHttpClient extends HttpClient {
}
if (typeof val === "string") {
// string payload
return new HttpResponse(200, "OK", val);
} else if(typeof val === "object" && val.statusCode) {
// HttpResponse payload
return val as HttpResponse;
} else {
return val;
// JSON payload
return new HttpResponse(200, "OK", JSON.stringify(val));
}
} else {
return await oldHandler(request);

View File

@ -56,4 +56,4 @@ export class PromiseSource<T> {
public reject(reason?: any) {
this.rejecter(reason);
}
}
}

View File

@ -1,60 +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.
import { HubMessage, IHubProtocol, ProtocolType } from "./IHubProtocol";
export class Base64EncodedHubProtocol implements IHubProtocol {
private wrappedProtocol: IHubProtocol;
constructor(protocol: IHubProtocol) {
this.wrappedProtocol = protocol;
this.name = this.wrappedProtocol.name;
this.type = ProtocolType.Text;
}
public readonly name: string;
public readonly type: ProtocolType;
public parseMessages(input: any): HubMessage[] {
// The format of the message is `size:message;`
const pos = input.indexOf(":");
if (pos === -1 || input[input.length - 1] !== ";") {
throw new Error("Invalid payload.");
}
const lenStr = input.substring(0, pos);
if (!/^[0-9]+$/.test(lenStr)) {
throw new Error(`Invalid length: '${lenStr}'`);
}
const messageSize = parseInt(lenStr, 10);
// 2 accounts for ':' after message size and trailing ';'
if (messageSize !== input.length - pos - 2) {
throw new Error("Invalid message size.");
}
const encodedMessage = input.substring(pos + 1, input.length - 1);
// atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use
// base64-js module
const s = atob(encodedMessage);
const payload = new Uint8Array(s.length);
for (let i = 0; i < payload.length; i++) {
payload[i] = s.charCodeAt(i);
}
return this.wrappedProtocol.parseMessages(payload.buffer);
}
public writeMessage(message: HubMessage): any {
const payload = new Uint8Array(this.wrappedProtocol.writeMessage(message));
let s = "";
for (let i = 0; i < payload.byteLength; i++) {
s += String.fromCharCode(payload[i]);
}
// atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use
// base64-js module
const encodedMessage = btoa(s);
return `${encodedMessage.length.toString()}:${encodedMessage};`;
}
}

View File

@ -6,7 +6,8 @@ import { DefaultHttpClient, HttpClient } from "./HttpClient";
import { IConnection } from "./IConnection";
import { ILogger, LogLevel } from "./ILogger";
import { LoggerFactory } from "./Loggers";
import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferMode, TransportType, WebSocketTransport } from "./Transports";
import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferFormat, TransportType, WebSocketTransport } from "./Transports";
import { Arg } from "./Utils";
export interface IHttpConnectionOptions {
httpClient?: HttpClient;
@ -23,7 +24,12 @@ const enum ConnectionState {
interface INegotiateResponse {
connectionId: string;
availableTransports: string[];
availableTransports: IAvailableTransport[];
}
interface IAvailableTransport {
transport: keyof typeof TransportType;
transferFormats: Array<keyof typeof TransferFormat>;
}
export class HttpConnection implements IConnection {
@ -33,13 +39,19 @@ export class HttpConnection implements IConnection {
private readonly httpClient: HttpClient;
private readonly logger: ILogger;
private readonly options: IHttpConnectionOptions;
private readonly transferFormat: TransferFormat;
private transport: ITransport;
private connectionId: string;
private startPromise: Promise<void>;
public readonly features: any = {};
constructor(url: string, options: IHttpConnectionOptions = {}) {
constructor(url: string, transferFormat: TransferFormat, options: IHttpConnectionOptions = {}) {
Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
this.transferFormat = transferFormat;
this.logger = LoggerFactory.createLogger(options.logger);
this.baseUrl = this.resolveUrl(url);
@ -67,7 +79,7 @@ export class HttpConnection implements IConnection {
if (this.options.transport === TransportType.WebSockets) {
// No need to add a connection ID in this case
this.url = this.baseUrl;
this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]);
this.transport = this.constructTransport(TransportType.WebSockets);
} else {
let headers;
const token = this.options.accessTokenFactory();
@ -91,50 +103,73 @@ export class HttpConnection implements IConnection {
if (this.connectionId) {
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports);
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports, this.transferFormat);
}
}
this.transport.onreceive = this.onreceive;
this.transport.onclose = (e) => this.stopConnection(true, e);
const requestedTransferMode =
this.features.transferMode === TransferMode.Binary
? TransferMode.Binary
: TransferMode.Text;
this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode, this);
await this.transport.connect(this.url, this.transferFormat, this);
// only change the state if we were connecting to not overwrite
// the state if the connection is already marked as Disconnected
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
} catch (e) {
this.logger.log(LogLevel.Error, "Failed to start the connection. " + e);
this.logger.log(LogLevel.Error, "Failed to start the connection: " + e);
this.connectionState = ConnectionState.Disconnected;
this.transport = null;
throw e;
}
}
private createTransport(transport: TransportType | ITransport, availableTransports: string[]): ITransport {
if ((transport === null || transport === undefined) && availableTransports.length > 0) {
transport = TransportType[availableTransports[0]];
}
if (transport === TransportType.WebSockets && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new WebSocketTransport(this.options.accessTokenFactory, this.logger);
}
if (transport === TransportType.ServerSentEvents && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
}
if (transport === TransportType.LongPolling && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport {
if (this.isITransport(requestedTransport)) {
this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly.");
return requestedTransport;
}
if (this.isITransport(transport)) {
return transport;
for (const endpoint of availableTransports) {
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat);
if (transport) {
return this.constructTransport(transport);
}
}
throw new Error("No available transports found.");
throw new Error("Unable to initialize any of the available transports.");
}
private constructTransport(transport: TransportType) {
switch (transport) {
case TransportType.WebSockets:
return new WebSocketTransport(this.options.accessTokenFactory, this.logger);
case TransportType.ServerSentEvents:
return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
case TransportType.LongPolling:
return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
default:
throw new Error(`Unknown transport: ${transport}.`);
}
}
private resolveTransport(endpoint: IAvailableTransport, requestedTransport: TransportType, requestedTransferFormat: TransferFormat): TransportType | null {
const transport = TransportType[endpoint.transport];
if (!transport) {
this.logger.log(LogLevel.Trace, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`);
} else {
const transferFormats = endpoint.transferFormats.map((s) => TransferFormat[s]);
if (!requestedTransport || transport === requestedTransport) {
if (transferFormats.indexOf(requestedTransferFormat) >= 0) {
this.logger.log(LogLevel.Trace, `Selecting transport '${transport}'`);
return transport;
} else {
this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it does not support the requested transfer format '${requestedTransferFormat}'.`);
}
} else {
this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it was disabled by the client.`);
}
}
return null;
}
private isITransport(transport: any): transport is ITransport {
@ -151,7 +186,7 @@ export class HttpConnection implements IConnection {
public send(data: any): Promise<void> {
if (this.connectionState !== ConnectionState.Connected) {
throw new Error("Cannot send data if the connection is not in the 'Connected' State");
throw new Error("Cannot send data if the connection is not in the 'Connected' State.");
}
return this.transport.send(data);
@ -210,7 +245,7 @@ export class HttpConnection implements IConnection {
}
const normalizedUrl = baseUrl + url;
this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'`);
this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'.`);
return normalizedUrl;
}

View File

@ -1,17 +1,16 @@
// 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.
import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol";
import { ConnectionClosed } from "./Common";
import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection";
import { IConnection } from "./IConnection";
import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { JsonHubProtocol } from "./JsonHubProtocol";
import { ConsoleLogger, LoggerFactory, NullLogger } from "./Loggers";
import { Observable, Subject } from "./Observable";
import { TextMessageFormat } from "./TextMessageFormat";
import { TransferMode, TransportType } from "./Transports";
import { TransferFormat, TransportType } from "./Transports";
export { JsonHubProtocol };
@ -40,15 +39,16 @@ export class HubConnection {
this.timeoutInMilliseconds = options.timeoutInMilliseconds || DEFAULT_TIMEOUT_IN_MS;
this.protocol = options.protocol || new JsonHubProtocol();
if (typeof urlOrConnection === "string") {
this.connection = new HttpConnection(urlOrConnection, options);
this.connection = new HttpConnection(urlOrConnection, this.protocol.transferFormat, options);
} else {
this.connection = urlOrConnection;
}
this.logger = LoggerFactory.createLogger(options.logger);
this.protocol = options.protocol || new JsonHubProtocol();
this.connection.onreceive = (data: any) => this.processIncomingData(data);
this.connection.onclose = (error?: Error) => this.connectionClosed(error);
@ -133,14 +133,7 @@ export class HubConnection {
}
public async start(): Promise<void> {
const requestedTransferMode =
(this.protocol.type === ProtocolType.Binary)
? TransferMode.Binary
: TransferMode.Text;
this.connection.features.transferMode = requestedTransferMode;
await this.connection.start();
const actualTransferMode = this.connection.features.transferMode;
await this.connection.send(
TextMessageFormat.write(
@ -148,10 +141,6 @@ export class HubConnection {
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`);
if (requestedTransferMode === TransferMode.Binary && actualTransferMode === TransferMode.Text) {
this.protocol = new Base64EncodedHubProtocol(this.protocol);
}
this.configureTimeout();
}

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { ConnectionClosed, DataReceived } from "./Common";
import { ITransport, TransferMode, TransportType } from "./Transports";
import { ITransport, TransportType } from "./Transports";
export interface IConnection {
readonly features: any;

View File

@ -1,4 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
import { TransferFormat } from "./Transports";
// 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.
export const enum MessageType {
@ -58,14 +60,9 @@ export interface CancelInvocationMessage extends HubInvocationMessage {
readonly type: MessageType.CancelInvocation;
}
export const enum ProtocolType {
Text = 1,
Binary,
}
export interface IHubProtocol {
readonly name: string;
readonly type: ProtocolType;
readonly transferFormat: TransferFormat;
parseMessages(input: any): HubMessage[];
writeMessage(message: HubMessage): any;
}

View File

@ -1,8 +1,9 @@
// 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.
import { HubMessage, IHubProtocol, ProtocolType } from "./IHubProtocol";
import { HubMessage, IHubProtocol } from "./IHubProtocol";
import { TextMessageFormat } from "./TextMessageFormat";
import { TransferFormat } from "./Transports";
export const JSON_HUB_PROTOCOL_NAME: string = "json";
@ -10,7 +11,7 @@ export class JsonHubProtocol implements IHubProtocol {
public readonly name: string = JSON_HUB_PROTOCOL_NAME;
public readonly type: ProtocolType = ProtocolType.Text;
public readonly transferFormat: TransferFormat = TransferFormat.Text;
public parseMessages(input: string): HubMessage[] {
if (!input) {

View File

@ -7,6 +7,7 @@ import { HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest } from "./HttpClient";
import { IConnection } from "./IConnection";
import { ILogger, LogLevel } from "./ILogger";
import { Arg } from "./Utils";
export enum TransportType {
WebSockets,
@ -14,13 +15,13 @@ export enum TransportType {
LongPolling,
}
export const enum TransferMode {
export enum TransferFormat {
Text = 1,
Binary,
}
export interface ITransport {
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode>;
connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void>;
send(data: any): Promise<void>;
stop(): Promise<void>;
onreceive: DataReceived;
@ -37,9 +38,17 @@ export class WebSocketTransport implements ITransport {
this.accessTokenFactory = accessTokenFactory || (() => null);
}
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
Arg.isRequired(connection, "connection");
return new Promise<TransferMode>((resolve, reject) => {
if (typeof (WebSocket) === "undefined") {
throw new Error("'WebSocket' is not supported in your environment.");
}
return new Promise<void>((resolve, reject) => {
url = url.replace(/^http/, "ws");
const token = this.accessTokenFactory();
if (token) {
@ -47,18 +56,18 @@ export class WebSocketTransport implements ITransport {
}
const webSocket = new WebSocket(url);
if (requestedTransferMode === TransferMode.Binary) {
if (transferFormat === TransferFormat.Binary) {
webSocket.binaryType = "arraybuffer";
}
webSocket.onopen = (event: Event) => {
this.logger.log(LogLevel.Information, `WebSocket connected to ${url}`);
this.webSocket = webSocket;
resolve(requestedTransferMode);
resolve();
};
webSocket.onerror = (event: Event) => {
reject();
webSocket.onerror = (event: ErrorEvent) => {
reject(event.error);
};
webSocket.onmessage = (message: MessageEvent) => {
@ -115,13 +124,22 @@ export class ServerSentEventsTransport implements ITransport {
this.logger = logger;
}
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
Arg.isRequired(connection, "connection");
if (typeof (EventSource) === "undefined") {
Promise.reject("EventSource not supported by the browser.");
throw new Error("'EventSource' is not supported in your environment.");
}
this.url = url;
return new Promise<TransferMode>((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
if (transferFormat !== TransferFormat.Text) {
reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
}
const token = this.accessTokenFactory();
if (token) {
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
@ -145,7 +163,7 @@ export class ServerSentEventsTransport implements ITransport {
};
eventSource.onerror = (e: any) => {
reject();
reject(new Error(e.message || "Error occurred"));
// don't report an error if the transport did not start successfully
if (this.eventSource && this.onclose) {
@ -157,7 +175,7 @@ export class ServerSentEventsTransport implements ITransport {
this.logger.log(LogLevel.Information, `SSE connected to ${this.url}`);
this.eventSource = eventSource;
// SSE is a text protocol
resolve(TransferMode.Text);
resolve();
};
} catch (e) {
return Promise.reject(e);
@ -197,29 +215,34 @@ export class LongPollingTransport implements ITransport {
this.pollAbort = new AbortController();
}
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
Arg.isRequired(connection, "connection");
this.url = url;
// Set a flag indicating we have inherent keep-alive in this transport.
connection.features.inherentKeepAlive = true;
if (requestedTransferMode === TransferMode.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
if (transferFormat === TransferFormat.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
// This will work if we fix: https://github.com/aspnet/SignalR/issues/742
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
}
this.poll(this.url, requestedTransferMode);
return Promise.resolve(requestedTransferMode);
this.poll(this.url, transferFormat);
return Promise.resolve();
}
private async poll(url: string, transferMode: TransferMode): Promise<void> {
private async poll(url: string, transferFormat: TransferFormat): Promise<void> {
const pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal,
headers: new Map<string, string>(),
timeout: 90000,
};
if (transferMode === TransferMode.Binary) {
if (transferFormat === TransferFormat.Binary) {
pollOptions.responseType = "arraybuffer";
}

View File

@ -0,0 +1,17 @@
// 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.
export class Arg {
public static isRequired(val: any, name: string): void {
if (val === null || val === undefined) {
throw new Error(`The '${name}' argument is required.`);
}
}
public static isIn(val: any, values: any, name: string): void {
// TypeScript enums have keys for **both** the name and the value of each enum member on the type itself.
if (!(val in values)) {
throw new Error(`Unknown ${name} value: ${val}.`);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -8,6 +8,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
@ -42,7 +43,7 @@ namespace ClientSample
var closeTcs = new TaskCompletionSource<object>();
connection.Closed += e => closeTcs.SetResult(null);
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
await connection.StartAsync();
await connection.StartAsync(TransferFormat.Text);
Console.WriteLine($"Connected to {baseUrl}");
var cts = new CancellationTokenSource();

View File

@ -29,10 +29,10 @@ namespace SocialWeather
connection.Features.Get<IConnectionMetadataFeature>().Metadata["format"] = format;
if (string.Equals(format, "protobuf", StringComparison.OrdinalIgnoreCase))
{
var transferModeFeature = connection.Features.Get<ITransferModeFeature>();
if (transferModeFeature != null)
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
if (transferFormatFeature != null)
{
transferModeFeature.TransferMode = TransferMode.Binary;
transferFormatFeature.ActiveFormat = TransferFormat.Binary;
}
}
_connectionList.Add(connection);

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.DependencyInjection;
using MsgPack.Serialization;
using SocketsSample.EndPoints;

View File

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
-->
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
</system.webServer>
</configuration>
</configuration>

View File

@ -15,7 +15,8 @@
</select>
<select id="transport">
<option value="WebSockets" selected>WebSockets</option>
<option value="Automatic" selected>Automatic</option>
<option value="WebSockets">WebSockets</option>
<option value="ServerSentEvents">ServerSentEvents</option>
<option value="LongPolling">LongPolling</option>
</select>
@ -135,14 +136,19 @@
var connection;
click('connect', function (event) {
let transportType = signalR.TransportType[transportDropdown.value] || signalR.TransportType.WebSockets;
let hubRoute = hubTypeDropdown.value || "default";
let protocol = protocolDropdown.value === "msgpack" ?
new signalR.protocols.msgpack.MessagePackHubProtocol() :
new signalR.JsonHubProtocol();
let options = { logger: logger, protocol: protocol };
if (transportDropdown.value !== "Automatic") {
options.transport = signalR.TransportType[transportDropdown.value];
}
console.log('http://' + document.location.host + '/' + hubRoute);
connection = new signalR.HubConnection(hubRoute, { transport: transportType, logger: logger, protocol: protocol });
connection = new signalR.HubConnection(hubRoute, options);
connection.on('Send', function (msg) {
addLine('message-list', msg);
});

View File

@ -23,10 +23,34 @@ The `POST [endpoint-base]/negotiate` request is used to establish connection bet
```
{
"connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d",
"availableTransports":["WebSockets","ServerSentEvents","LongPolling"]
"availableTransports":[
{
"transport": "WebSockets",
"transferFormats": [ "Text", "Binary" ]
},
{
"transport": "ServerSentEvents",
"transferFormats": [ "Text" ]
},
{
"transport": "LongPolling",
"transferFormats": [ "Text", "Binary" ]
}
]
}
```
The payload returned from this endpoint provides the following data:
* The `connectionId` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives).
* The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`)
## Transfer Formats
ASP.NET Endpoints support two different transfer formats: `Text` and `Binary`. `Text` refers to UTF-8 text, and `Binary` refers to any arbitrary binary data. The transfer format serves two purposes. First, in the WebSockets transport, it is used to determine if `Text` or `Binary` WebSocket frames should be used to carry data. This is useful in debugging as most browser Dev Tools only show the content of `Text` frames. When using a text-based protocol like JSON, it is preferable for the WebSockets transport to use `Text` frames. How a client/server indicate the transfer format currently being used is implementation-defined.
Some transports are limited to supporting only `Text` data (specifically, Server-Sent Events). These transports cannot carry arbitrary binary data (without additional encoding, such as Base-64) due to limitations in their protocol. The transfer formats supported by each transport are described as part of the `POST [endpoint-base]/negotiate` response to allow clients to ignore transports that cannot support arbitrary binary data when they have a need to send/receive that data. How the client indicates the transfer format it wishes to use is also implementation-defined.
## 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 `POST [endpoint-base]/negotiate` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata.
@ -53,7 +77,6 @@ If the relevant connection has been terminated, a `404 Not Found` status code is
Server-Sent Events (SSE) is a protocol specified by WHATWG at [https://html.spec.whatwg.org/multipage/comms.html#server-sent-events](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events). It is capable of sending data from server to client only, so it must be paired with the HTTP Post transport. It also requires a connection already be established using the `POST [endpoint-base]/negotiate` request.
The protocol is similar to Long Polling in that the client opens a request to an endpoint and leaves it open. The server transmits frames as "events" using the SSE protocol. The protocol encodes a single event as a sequence of key-value pair lines, separated by `:` and using any of `\r\n`, `\n` or `\r` as line-terminators, followed by a final blank line. Keys can be duplicated and their values are concatenated with `\n`. So the following represents two events:
```
@ -71,6 +94,8 @@ In the first event, the value of `baz` would be `boz\nbiz\nflarg`, due to the co
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 Server-Sent Events transport only supports text data, because it is a text-based protocol. As a result, it is reported by the server as supporting only the `Text` transfer format. If a client wishes to send arbitrary binary data, it should skip the Server-Sent Events transport when selecting an appropriate transport.
## 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 `POST [endpoint-base]/negotiate` request.

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;

View File

@ -4,13 +4,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
@ -30,7 +28,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
private readonly IConnection _connection;
private readonly IHubProtocol _protocol;
private readonly HubBinder _binder;
private HubProtocolReaderWriter _protocolReaderWriter;
private readonly object _pendingCallsLock = new object();
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
@ -113,26 +110,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
private async Task StartAsyncCore()
{
var transferModeFeature = _connection.Features.Get<ITransferModeFeature>();
if (transferModeFeature == null)
{
transferModeFeature = new TransferModeFeature();
_connection.Features.Set(transferModeFeature);
}
var requestedTransferMode =
_protocol.Type == ProtocolType.Binary
? TransferMode.Binary
: TransferMode.Text;
transferModeFeature.TransferMode = requestedTransferMode;
await _connection.StartAsync();
await _connection.StartAsync(_protocol.TransferFormat);
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
var actualTransferMode = transferModeFeature.TransferMode;
_protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode));
Log.HubProtocol(_logger, _protocol.Name);
_connectionActive = new CancellationTokenSource();
@ -146,20 +126,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
ResetTimeoutTimer();
}
private IDataEncoder GetDataEncoder(TransferMode requestedTransferMode, TransferMode actualTransferMode)
{
if (requestedTransferMode == TransferMode.Binary && actualTransferMode == TransferMode.Text)
{
// This is for instance for SSE which is a Text protocol and the user wants to use a binary
// protocol so we need to encode messages.
return new Base64Encoder();
}
Debug.Assert(requestedTransferMode == actualTransferMode, "All transports besides SSE are expected to support binary mode.");
return new PassThroughEncoder();
}
public async Task StopAsync() => await StopAsyncCore().ForceAsync();
private Task StopAsyncCore() => _connection.StopAsync();
@ -295,7 +261,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
{
try
{
var payload = _protocolReaderWriter.WriteMessage(hubMessage);
var payload = _protocol.WriteToArray(hubMessage);
Log.SendInvocation(_logger, hubMessage.InvocationId);
await _connection.SendAsync(payload, irq.CancellationToken);
@ -328,7 +294,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
{
Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length);
var payload = _protocolReaderWriter.WriteMessage(invocationMessage);
var payload = _protocol.WriteToArray(invocationMessage);
Log.SendInvocation(_logger, invocationMessage.InvocationId);
await _connection.SendAsync(payload, cancellationToken);
@ -345,7 +311,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
{
ResetTimeoutTimer();
Log.ParsingMessages(_logger, data.Length);
if (_protocolReaderWriter.ReadMessages(data, _binder, out var messages))
var messages = new List<HubMessage>();
if (_protocol.TryParseMessages(data, _binder, messages))
{
Log.ReceivingMessages(_logger, messages.Count);
foreach (var message in messages)
@ -621,10 +588,5 @@ namespace Microsoft.AspNetCore.SignalR.Client
return _callback(parameters, _state);
}
}
private class TransferModeFeature : ITransferModeFeature
{
public TransferMode TransferMode { get; set; }
}
}
}

View File

@ -1,59 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
{
public class Base64Encoder : IDataEncoder
{
public bool TryDecode(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> data)
{
if (LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message))
{
Span<byte> decoded = new byte[Base64.GetMaxDecodedFromUtf8Length(message.Length)];
var status = Base64.DecodeFromUtf8(message, decoded, out _, out var written);
Debug.Assert(status == OperationStatus.Done);
data = decoded.Slice(0, written);
return true;
}
return false;
}
private const int Int32OverflowLength = 10;
public byte[] Encode(byte[] payload)
{
var maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(payload.Length);
// Int32OverflowLength + length of separator (':') + length of terminator (';')
if (int.MaxValue - maxEncodedLength < Int32OverflowLength + 2)
{
throw new FormatException("The encoded message exceeds the maximum supported size.");
}
//The format is: [{length}:{message};] so allocate enough to be able to write the entire message
Span<byte> buffer = new byte[Int32OverflowLength + 1 + maxEncodedLength + 1];
buffer[Int32OverflowLength] = (byte)':';
var status = Base64.EncodeToUtf8(payload, buffer.Slice(Int32OverflowLength + 1), out _, out var written);
Debug.Assert(status == OperationStatus.Done);
buffer[Int32OverflowLength + 1 + written] = (byte)';';
var prefixLength = 0;
var prefix = written;
do
{
buffer[Int32OverflowLength - 1 - prefixLength] = (byte)('0' + prefix % 10);
prefix /= 10;
prefixLength++;
}
while (prefix > 0);
return buffer.Slice(Int32OverflowLength - prefixLength, prefixLength + 1 + written + 1).ToArray();
}
}
}

View File

@ -1,13 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
{
public interface IDataEncoder
{
byte[] Encode(byte[] payload);
bool TryDecode(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> data);
}
}

View File

@ -1,94 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers.Text;
using System.Text;
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
{
public static class LengthPrefixedTextMessageParser
{
private const char FieldDelimiter = ':';
private const char MessageDelimiter = ';';
/// <summary>
/// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an
/// exception if there is a format error in the provided data.
/// </summary>
public static bool TryParseMessage(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> payload)
{
payload = default;
if (!TryReadLength(buffer, out var index, out var length))
{
return false;
}
var remaining = buffer.Slice(index);
if (!TryReadDelimiter(remaining, FieldDelimiter, "length"))
{
return false;
}
// Skip the delimeter
remaining = remaining.Slice(1);
if (remaining.Length < length + 1)
{
return false;
}
payload = remaining.Slice(0, length);
remaining = remaining.Slice(length);
if (!TryReadDelimiter(remaining, MessageDelimiter, "payload"))
{
return false;
}
// Skip the delimeter
buffer = remaining.Slice(1);
return true;
}
private static bool TryReadLength(ReadOnlySpan<byte> buffer, out int index, out int length)
{
length = 0;
// Read until the first ':' to find the length
index = buffer.IndexOf((byte)FieldDelimiter);
if (index == -1)
{
// Insufficient data
return false;
}
var lengthSpan = buffer.Slice(0, index);
if (!Utf8Parser.TryParse(buffer, out length, out var bytesConsumed) || bytesConsumed < lengthSpan.Length)
{
throw new FormatException($"Invalid length: '{Encoding.UTF8.GetString(lengthSpan.ToArray())}'");
}
return true;
}
private static bool TryReadDelimiter(ReadOnlySpan<byte> buffer, char delimiter, string field)
{
if (buffer.Length == 0)
{
return false;
}
if (buffer[0] != delimiter)
{
throw new FormatException($"Missing delimiter '{delimiter}' after {field}");
}
return true;
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
{
public class PassThroughEncoder : IDataEncoder
{
public bool TryDecode(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> data)
{
data = buffer;
buffer = Array.Empty<byte>();
return true;
}
public byte[] Encode(byte[] payload)
{
return payload;
}
}
}

View File

@ -1,74 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
namespace Microsoft.AspNetCore.SignalR.Internal
{
public class HubProtocolReaderWriter
{
private readonly IHubProtocol _hubProtocol;
private readonly IDataEncoder _dataEncoder;
public HubProtocolReaderWriter(IHubProtocol hubProtocol, IDataEncoder dataEncoder)
{
_hubProtocol = hubProtocol;
_dataEncoder = dataEncoder;
}
public bool ReadMessages(ReadOnlySequence<byte> buffer, IInvocationBinder binder, out IList<HubMessage> messages, out SequencePosition consumed, out SequencePosition examined)
{
// TODO: Fix this implementation to be incremental
consumed = buffer.End;
examined = consumed;
return ReadMessages(buffer.ToArray(), binder, out messages);
}
public bool ReadMessages(byte[] input, IInvocationBinder binder, out IList<HubMessage> messages)
{
messages = new List<HubMessage>();
ReadOnlySpan<byte> span = input;
while (span.Length > 0 && _dataEncoder.TryDecode(ref span, out var data))
{
_hubProtocol.TryParseMessages(data, binder, messages);
}
return messages.Count > 0;
}
public byte[] WriteMessage(HubMessage hubMessage)
{
using (var ms = new MemoryStream())
{
_hubProtocol.WriteMessage(hubMessage, ms);
return _dataEncoder.Encode(ms.ToArray());
}
}
public override bool Equals(object obj)
{
var readerWriter = obj as HubProtocolReaderWriter;
if (readerWriter == null)
{
return false;
}
// Note: ReferenceEquals on HubProtocol works for our implementation of IHubProtocolResolver because we use Singletons from DI
// However if someone replaces the implementation and returns a new ProtocolResolver for every connection they wont get the perf benefits
// Memory growth is mitigated by capping the cache size
return ReferenceEquals(_dataEncoder, readerWriter._dataEncoder) && ReferenceEquals(_hubProtocol, readerWriter._hubProtocol);
}
// This should never be used, needed because you can't override Equals without it
public override int GetHashCode()
{
return base.GetHashCode();
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
@ -11,39 +12,48 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
}
// Initialize with capacity 4 for the 2 built in protocols and 2 data encoders
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(4);
// Initialize with capacity 2 for the 2 built in protocols
private object _lock = new object();
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(2);
public byte[] WriteMessage(HubProtocolReaderWriter protocolReaderWriter)
public byte[] WriteMessage(IHubProtocol protocol)
{
for (var i = 0; i < _serializedMessages.Count; i++)
// REVIEW: Revisit lock
// Could use a reader/writer lock to allow the loop to take place in "unlocked" code
// Or, could use a fixed size array and Interlocked to manage it.
// Or, Immutable *ducks*
lock (_lock)
{
if (_serializedMessages[i].ProtocolReaderWriter.Equals(protocolReaderWriter))
for (var i = 0; i < _serializedMessages.Count; i++)
{
return _serializedMessages[i].Message;
if (_serializedMessages[i].Protocol.Equals(protocol))
{
return _serializedMessages[i].Message;
}
}
var bytes = protocol.WriteToArray(this);
// We don't want to balloon memory if someone writes a poor IHubProtocolResolver
// So we cap how many caches we store and worst case just serialize the message for every connection
if (_serializedMessages.Count < 10)
{
_serializedMessages.Add(new SerializedMessage(protocol, bytes));
}
return bytes;
}
var bytes = protocolReaderWriter.WriteMessage(this);
// We don't want to balloon memory if someone writes a poor IHubProtocolResolver
// So we cap how many caches we store and worst case just serialize the message for every connection
if (_serializedMessages.Count < 10)
{
_serializedMessages.Add(new SerializedMessage(protocolReaderWriter, bytes));
}
return bytes;
}
private readonly struct SerializedMessage
{
public readonly HubProtocolReaderWriter ProtocolReaderWriter;
public readonly IHubProtocol Protocol;
public readonly byte[] Message;
public SerializedMessage(HubProtocolReaderWriter protocolReaderWriter, byte[] message)
public SerializedMessage(IHubProtocol protocol, byte[] message)
{
ProtocolReaderWriter = protocolReaderWriter;
Protocol = protocol;
Message = message;
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public static class HubProtocolExtensions
{
public static byte[] WriteToArray(this IHubProtocol hubProtocol, HubMessage message)
{
using (var ms = new MemoryStream())
{
hubProtocol.WriteMessage(message, ms);
return ms.ToArray();
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Sockets;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
string Name { get; }
ProtocolType Type { get; }
TransferFormat TransferFormat { get; }
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages);

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public string Name => ProtocolName;
public ProtocolType Type => ProtocolType.Text;
public TransferFormat TransferFormat => TransferFormat.Text;
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{

View File

@ -1,11 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public enum ProtocolType
{
Binary,
Text
}
}

View File

@ -15,4 +15,8 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -16,9 +16,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Core;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.Extensions.Logging;
@ -27,8 +25,6 @@ namespace Microsoft.AspNetCore.SignalR
public class HubConnectionContext
{
private static Action<object> _abortedCallback = AbortConnection;
private static readonly Base64Encoder Base64Encoder = new Base64Encoder();
private static readonly PassThroughEncoder PassThroughEncoder = new PassThroughEncoder();
private readonly ConnectionContext _connectionContext;
private readonly ILogger _logger;
@ -62,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR
public string UserIdentifier { get; private set; }
internal virtual HubProtocolReaderWriter ProtocolReaderWriter { get; set; }
internal virtual IHubProtocol Protocol { get; set; }
internal ExceptionDispatchInfo AbortException { get; private set; }
@ -85,7 +81,7 @@ namespace Microsoft.AspNetCore.SignalR
{
// This will internally cache the buffer for each unique HubProtocol/DataEncoder combination
// So that we don't serialize the HubMessage for every single connection
var buffer = message.WriteMessage(ProtocolReaderWriter);
var buffer = message.WriteMessage(Protocol);
_connectionContext.Transport.Output.Write(buffer);
Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp());
@ -156,27 +152,25 @@ namespace Microsoft.AspNetCore.SignalR
{
if (NegotiationProtocol.TryParseMessage(buffer, out var negotiationMessage, out consumed, out examined))
{
var protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this);
Protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this);
var transportCapabilities = Features.Get<IConnectionTransportFeature>()?.TransportCapabilities
?? throw new InvalidOperationException("Unable to read transport capabilities.");
// If there's a transfer format feature, we need to check if we're compatible and set the active format.
// If there isn't a feature, it means that the transport supports binary data and doesn't need us to tell them
// what format we're writing.
var transferFormatFeature = Features.Get<ITransferFormatFeature>();
if (transferFormatFeature != null)
{
if ((transferFormatFeature.SupportedFormats & Protocol.TransferFormat) == 0)
{
throw new InvalidOperationException($"Cannot use the '{Protocol.Name}' protocol on the current transport. The transport does not support the '{Protocol.TransferFormat}' transfer mode.");
}
var dataEncoder = (protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) == 0)
? (IDataEncoder)Base64Encoder
: PassThroughEncoder;
transferFormatFeature.ActiveFormat = Protocol.TransferFormat;
}
var transferModeFeature = Features.Get<ITransferModeFeature>() ??
throw new InvalidOperationException("Unable to read transfer mode.");
_cachedPingMessage = Protocol.WriteToArray(PingMessage.Instance);
transferModeFeature.TransferMode =
(protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) != 0)
? TransferMode.Binary
: TransferMode.Text;
ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder);
_cachedPingMessage = ProtocolReaderWriter.WriteMessage(PingMessage.Instance);
Log.UsingHubProtocol(_logger, protocol.Name);
Log.UsingHubProtocol(_logger, Protocol.Name);
UserIdentifier = userIdProvider.GetUserId(this);

View File

@ -2,10 +2,13 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Core;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -141,7 +144,10 @@ namespace Microsoft.AspNetCore.SignalR
{
if (!buffer.IsEmpty)
{
if (connection.ProtocolReaderWriter.ReadMessages(buffer, _dispatcher, out var hubMessages, out consumed, out examined))
var hubMessages = new List<HubMessage>();
// TODO: Make this incremental
if (connection.Protocol.TryParseMessages(buffer.ToArray(), _dispatcher, hubMessages))
{
foreach (var hubMessage in hubMessages)
{

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Options;
using MsgPack;
using MsgPack.Serialization;
@ -24,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public string Name => ProtocolName;
public ProtocolType Type => ProtocolType.Binary;
public TransferFormat TransferFormat => TransferFormat.Binary;
public MessagePackHubProtocol()
: this(Options.Create(new MessagePackHubProtocolOptions()))

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO.Pipelines;
@ -8,7 +8,5 @@ namespace Microsoft.AspNetCore.Sockets.Features
public interface IConnectionTransportFeature
{
IDuplexPipe Transport { get; set; }
TransferMode TransportCapabilities { get; set; }
}
}

View File

@ -0,0 +1,11 @@
// 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.Features
{
public interface ITransferFormatFeature
{
TransferFormat SupportedFormats { get; }
TransferFormat ActiveFormat { get; set; }
}
}

View File

@ -1,10 +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.Features
{
public interface ITransferModeFeature
{
TransferMode TransferMode { get; set; }
}
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
public interface IConnection
{
Task StartAsync();
Task StartAsync(TransferFormat transferFormat);
Task SendAsync(byte[] data, CancellationToken cancellationToken);
Task StopAsync();
Task DisposeAsync();

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -6,7 +6,7 @@ using System;
namespace Microsoft.AspNetCore.Sockets
{
[Flags]
public enum TransferMode
public enum TransferFormat
{
Binary = 0x01,
Text = 0x02

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -85,6 +85,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
private static readonly Action<ILogger, string, string, Exception> _connectionStateChanged =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(25, "ConnectionStateChanged"), "Connection state changed from {previousState} to {newState}.");
private static readonly Action<ILogger, string, Exception> _transportNotSupported =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(26, "TransportNotSupported"), "Skipping transport {transportName} because it is not supported by this client.");
private static readonly Action<ILogger, string, string, Exception> _transportDoesNotSupportTransferFormat =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(27, "TransportDoesNotSupportTransferFormat"), "Skipping transport {transportName} because it does not support the requested transfer format '{transferFormat}'.");
private static readonly Action<ILogger, string, Exception> _transportDisabledByClient =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(28, "TransportDisabledByClient"), "Skipping transport {transportName} because it was disabled by the client.");
private static readonly Action<ILogger, string, Exception> _transportFailed =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {transportName} because it failed to initialize.");
public static void HttpConnectionStarting(ILogger logger)
{
_httpConnectionStarting(logger, null);
@ -218,6 +230,35 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
_errorDuringClosedEvent(logger, exception);
}
public static void TransportNotSupported(ILogger logger, string transport)
{
_transportNotSupported(logger, transport, null);
}
public static void TransportDoesNotSupportTransferFormat(ILogger logger, TransportType transport, TransferFormat transferFormat)
{
if (logger.IsEnabled(LogLevel.Debug))
{
_transportDoesNotSupportTransferFormat(logger, transport.ToString(), transferFormat.ToString(), null);
}
}
public static void TransportDisabledByClient(ILogger logger, TransportType transport)
{
if (logger.IsEnabled(LogLevel.Debug))
{
_transportDisabledByClient(logger, transport.ToString(), null);
}
}
public static void TransportFailed(ILogger logger, TransportType transport, Exception ex)
{
if (logger.IsEnabled(LogLevel.Debug))
{
_transportFailed(logger, transport.ToString(), ex);
}
}
}
}
}

View File

@ -6,13 +6,13 @@ using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Sockets.Client.Http;
using Microsoft.AspNetCore.Sockets.Client.Internal;
using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.AspNetCore.Sockets.Http.Internal;
using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.Extensions.Logging;
@ -47,9 +47,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
private PipeWriter Output => _transportChannel.Output;
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
private readonly TransportType _requestedTransportType = TransportType.All;
private TransportType _serverTransports = TransportType.All;
// The order of the transports here is the order determines the fallback order.
private static readonly TransportType[] AllTransports = new[]{ TransportType.WebSockets, TransportType.ServerSentEvents, TransportType.LongPolling };
private readonly ConnectionLogScope _logScope;
private readonly IDisposable _scopeDisposable;
@ -153,9 +150,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
_scopeDisposable = _logger.BeginScope(_logScope);
}
public async Task StartAsync() => await StartAsyncCore().ForceAsync();
public Task StartAsync() => StartAsync(TransferFormat.Binary);
public async Task StartAsync(TransferFormat transferFormat) => await StartAsyncCore(transferFormat).ForceAsync();
private Task StartAsyncCore()
private Task StartAsyncCore(TransferFormat transferFormat)
{
if (ChangeState(from: ConnectionState.Disconnected, to: ConnectionState.Connecting) != ConnectionState.Disconnected)
{
@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
_startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_eventQueue = new TaskQueue();
StartAsyncInternal()
StartAsyncInternal(transferFormat)
.ContinueWith(t =>
{
var abortException = _abortException;
@ -195,7 +193,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
return negotiationResponse;
}
private async Task StartAsyncInternal()
private async Task StartAsyncInternal(TransferFormat transferFormat)
{
Log.HttpConnectionStarting(_logger);
@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
if (_requestedTransportType == TransportType.WebSockets)
{
Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
await StartTransport(connectUrl, _requestedTransportType);
await StartTransport(connectUrl, _requestedTransportType, transferFormat);
}
else
{
@ -219,14 +217,32 @@ namespace Microsoft.AspNetCore.Sockets.Client
}
// This should only need to happen once
_serverTransports = GetAvailableServerTransports(negotiationResponse);
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
foreach (var transport in AllTransports)
// We're going to search for the transfer format as a string because we don't want to parse
// all the transfer formats in the negotiation response, and we want to allow transfer formats
// we don't understand in the negotiate response.
var transferFormatString = transferFormat.ToString();
foreach (var transport in negotiationResponse.AvailableTransports)
{
if (!Enum.TryParse<TransportType>(transport.Transport, out var transportType))
{
Log.TransportNotSupported(_logger, transport.Transport);
continue;
}
try
{
if ((transport & _serverTransports & _requestedTransportType) != 0)
if ((transportType & _requestedTransportType) == 0)
{
Log.TransportDisabledByClient(_logger, transportType);
}
else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal))
{
Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat);
}
else
{
// The negotiation response gets cleared in the fallback scenario.
if (negotiationResponse == null)
@ -235,13 +251,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
}
Log.StartingTransport(_logger, transport, connectUrl);
await StartTransport(connectUrl, transport);
Log.StartingTransport(_logger, transportType, connectUrl);
await StartTransport(connectUrl, transportType, transferFormat);
break;
}
}
catch (Exception)
catch (Exception ex)
{
Log.TransportFailed(_logger, transportType, ex);
// Try the next transport
// Clear the negotiation response so we know to re-negotiate.
negotiationResponse = null;
@ -382,22 +399,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
return negotiationResponse;
}
private TransportType GetAvailableServerTransports(NegotiationResponse negotiationResponse)
{
if (negotiationResponse.AvailableTransports == null)
{
throw new FormatException("No transports returned in negotiation response.");
}
var availableServerTransports = (TransportType)0;
foreach (var t in negotiationResponse.AvailableTransports)
{
availableServerTransports |= t;
}
return availableServerTransports;
}
private static Uri CreateConnectUrl(Uri url, string connectionId)
{
if (string.IsNullOrWhiteSpace(connectionId))
@ -408,9 +409,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
return Utils.AppendQueryString(url, "id=" + connectionId);
}
private async Task StartTransport(Uri connectUrl, TransportType transportType)
private async Task StartTransport(Uri connectUrl, TransportType transportType, TransferFormat transferFormat)
{
var options = new PipeOptions(writerScheduler: PipeScheduler.Inline, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options);
_transportChannel = pair.Transport;
@ -419,15 +419,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
// Start the transport, giving it one end of the pipeline
try
{
await _transport.StartAsync(connectUrl, pair.Application, GetTransferMode(), this);
// actual transfer mode can differ from the one that was requested so set it on the feature
if (!_transport.Mode.HasValue)
{
// This can happen with custom transports so it should be an exception, not an assert.
throw new InvalidOperationException("Transport was expected to set the Mode property after StartAsync, but it has not been set.");
}
SetTransferMode(_transport.Mode.Value);
await _transport.StartAsync(connectUrl, pair.Application, transferFormat, this);
}
catch (Exception ex)
{
@ -437,29 +429,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
}
}
private TransferMode GetTransferMode()
{
var transferModeFeature = Features.Get<ITransferModeFeature>();
if (transferModeFeature == null)
{
return TransferMode.Text;
}
return transferModeFeature.TransferMode;
}
private void SetTransferMode(TransferMode transferMode)
{
var transferModeFeature = Features.Get<ITransferModeFeature>();
if (transferModeFeature == null)
{
transferModeFeature = new TransferModeFeature();
Features.Set(transferModeFeature);
}
transferModeFeature.TransferMode = transferMode;
}
private async Task ReceiveAsync()
{
try
@ -753,7 +722,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
private class NegotiationResponse
{
public string ConnectionId { get; set; }
public TransportType[] AvailableTransports { get; set; }
public AvailableTransport[] AvailableTransports { get; set; }
}
private class AvailableTransport
{
public string Transport { get; set; }
public string[] TransferFormats { get; set; }
}
}
}

View File

@ -9,8 +9,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
public interface ITransport
{
Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection);
Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection);
Task StopAsync();
TransferMode? Mode { get; }
}
}

View File

@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
private static class Log
{
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
// EventIds 100 - 106 used in SendUtils
public static void StartTransport(ILogger logger, TransferMode transferMode)
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
{
_startTransport(logger, transferMode, null);
_startTransport(logger, transferFormat, null);
}
public static void TransportStopped(ILogger logger, Exception exception)

View File

@ -27,8 +27,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
public Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public LongPollingTransport(HttpClient httpClient)
: this(httpClient, null, null)
{ }
@ -40,19 +38,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
}
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
{
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text)
{
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
}
connection.Features.Set<IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(_httpClient.Timeout));
_application = application;
Mode = requestedTransferMode;
Log.StartTransport(_logger, Mode.Value);
Log.StartTransport(_logger, transferFormat);
// Start sending and polling (ask for binary if the server supports it)
_poller = Poll(url, _transportCts.Token);

View File

@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
private static class Log
{
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
// EventIds 100 - 106 used in SendUtils
public static void StartTransport(ILogger logger, TransferMode transferMode)
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
{
_startTransport(logger, transferMode, null);
_startTransport(logger, transferFormat, null);
}
public static void TransportStopped(ILogger logger, Exception exception)

View File

@ -26,8 +26,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
public Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public ServerSentEventsTransport(HttpClient httpClient)
: this(httpClient, null, null)
{ }
@ -44,17 +42,16 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>();
}
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
{
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
if (transferFormat != TransferFormat.Text)
{
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
}
_application = application;
Mode = TransferMode.Text; // Server Sent Events is a text only transport
Log.StartTransport(_logger, Mode.Value);
Log.StartTransport(_logger, transferFormat);
var sendTask = SendUtils.SendMessages(url, _application, _httpClient, _httpOptions, _transportCts, _logger);
var receiveTask = OpenConnection(_application, url, _transportCts.Token);

View File

@ -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.
using Microsoft.AspNetCore.Sockets.Features;
namespace Microsoft.AspNetCore.Sockets.Client
{
public class TransferModeFeature : ITransferModeFeature
{
public TransferMode TransferMode { get; set; }
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
private static class Log
{
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
@ -65,9 +65,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
private static readonly Action<ILogger, Exception> _cancelMessage =
LoggerMessage.Define(LogLevel.Debug, new EventId(18, "CancelMessage"), "Canceled passing message to application.");
public static void StartTransport(ILogger logger, TransferMode transferMode)
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
{
_startTransport(logger, transferMode, null);
_startTransport(logger, transferFormat, null);
}
public static void TransportStopped(ILogger logger, Exception exception)

View File

@ -19,14 +19,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
{
private readonly ClientWebSocket _webSocket;
private IDuplexPipe _application;
private WebSocketMessageType _webSocketMessageType;
private readonly ILogger _logger;
private readonly TimeSpan _closeTimeout;
private volatile bool _aborted;
public Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public WebSocketsTransport()
: this(null, null)
{
@ -87,7 +86,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>();
}
public async Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
public async Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
{
if (url == null)
{
@ -99,15 +98,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
throw new ArgumentNullException(nameof(application));
}
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text)
{
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
}
_application = application;
Mode = requestedTransferMode;
_webSocketMessageType = transferFormat == TransferFormat.Binary
? WebSocketMessageType.Binary
: WebSocketMessageType.Text;
Log.StartTransport(_logger, Mode.Value);
Log.StartTransport(_logger, transferFormat);
await Connect(url);
@ -243,11 +245,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
private async Task StartSending(WebSocket socket)
{
var webSocketMessageType =
Mode == TransferMode.Binary
? WebSocketMessageType.Binary
: WebSocketMessageType.Text;
Exception error = null;
try
@ -274,7 +271,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
if (WebSocketCanSend(socket))
{
await socket.SendAsync(buffer, webSocketMessageType);
await socket.SendAsync(buffer, _webSocketMessageType);
}
else
{

View File

@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Sockets
Log.EstablishedConnection(_logger);
// ServerSentEvents is a text protocol only
connection.TransportCapabilities = TransferMode.Text;
connection.SupportedFormats = TransferFormat.Text;
// We only need to provide the Input channel since writing to the application is handled through /send.
var sse = new ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory);
@ -389,15 +389,15 @@ namespace Microsoft.AspNetCore.Sockets
jsonWriter.WriteStartArray();
if ((options.Transports & TransportType.WebSockets) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.WebSockets));
WriteTransport(jsonWriter, nameof(TransportType.WebSockets), TransferFormat.Text | TransferFormat.Binary);
}
if ((options.Transports & TransportType.ServerSentEvents) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.ServerSentEvents));
WriteTransport(jsonWriter, nameof(TransportType.ServerSentEvents), TransferFormat.Text);
}
if ((options.Transports & TransportType.LongPolling) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.LongPolling));
WriteTransport(jsonWriter, nameof(TransportType.LongPolling), TransferFormat.Text | TransferFormat.Binary);
}
jsonWriter.WriteEndArray();
jsonWriter.WriteEndObject();
@ -406,6 +406,27 @@ namespace Microsoft.AspNetCore.Sockets
return sb.ToString();
}
private static void WriteTransport(JsonWriter writer, string transportName, TransferFormat supportedTransferFormats)
{
writer.WriteStartObject();
writer.WritePropertyName("transport");
writer.WriteValue(transportName);
writer.WritePropertyName("transferFormats");
writer.WriteStartArray();
if ((supportedTransferFormats & TransferFormat.Binary) != 0)
{
writer.WriteValue(nameof(TransferFormat.Binary));
}
if ((supportedTransferFormats & TransferFormat.Text) != 0)
{
writer.WriteValue(nameof(TransferFormat.Text));
}
writer.WriteEndArray();
writer.WriteEndObject();
}
private static string GetConnectionId(HttpContext context) => context.Request.Query["id"];
private async Task ProcessSend(HttpContext context)
@ -477,9 +498,6 @@ namespace Microsoft.AspNetCore.Sockets
connection.User = context.User;
connection.SetHttpContext(context);
// this is the default setting which should be overwritten by transports that have different capabilities (e.g. SSE)
connection.TransportCapabilities = TransferMode.Binary | TransferMode.Text;
// Set the Connection ID on the logging scope so that logs from now on will have the
// Connection ID metadata set.
logScope.ConnectionId = connection.ConnectionId;

View File

@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
{
Log.SendPayload(_logger, buffer.Length);
var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary
var webSocketMessageType = (_connection.ActiveFormat == TransferFormat.Binary
? WebSocketMessageType.Binary
: WebSocketMessageType.Text);

View File

@ -2,7 +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.Pipelines;
using System.Security.Claims;
@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets
IConnectionTransportFeature,
IConnectionUserFeature,
IConnectionHeartbeatFeature,
ITransferModeFeature
ITransferFormatFeature
{
private List<(Action<object> handler, object state)> _heartbeatHandlers = new List<(Action<object> handler, object state)>();
@ -37,14 +36,18 @@ namespace Microsoft.AspNetCore.Sockets
ConnectionId = id;
LastSeenUtc = DateTime.UtcNow;
// The default behavior is that both formats are supported.
SupportedFormats = TransferFormat.Binary | TransferFormat.Text;
ActiveFormat = TransferFormat.Text;
// PERF: This type could just implement IFeatureCollection
Features = new FeatureCollection();
Features.Set<IConnectionUserFeature>(this);
Features.Set<IConnectionMetadataFeature>(this);
Features.Set<IConnectionIdFeature>(this);
Features.Set<IConnectionTransportFeature>(this);
Features.Set<ITransferModeFeature>(this);
Features.Set<IConnectionHeartbeatFeature>(this);
Features.Set<ITransferFormatFeature>(this);
}
public CancellationTokenSource Cancellation { get; set; }
@ -71,9 +74,9 @@ namespace Microsoft.AspNetCore.Sockets
public override IDuplexPipe Transport { get; set; }
public TransferMode TransportCapabilities { get; set; }
public TransferFormat SupportedFormats { get; set; }
public TransferMode TransferMode { get; set; }
public TransferFormat ActiveFormat { get; set; }
public void OnHeartbeat(Action<object> action, object state)
{

View File

@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
await connection.StartAsync().OrTimeout();
var result = await connection.InvokeAsync<string>("HelloWorld").OrTimeout();
var result = await connection.InvokeAsync<string>(nameof(TestHub.HelloWorld)).OrTimeout();
Assert.Equal("Hello World!", result);
}
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
await connection.StartAsync().OrTimeout();
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
}
@ -115,11 +115,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try
{
await connection.StartAsync().OrTimeout();
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
await connection.StopAsync().OrTimeout();
await connection.StartAsync().OrTimeout();
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
}
catch (Exception ex)
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try
{
await connection.StartAsync().OrTimeout();
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
logger.LogInformation("Stopping connection");
@ -169,7 +169,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
await restartTcs.Task.OrTimeout();
logger.LogInformation("Reconnection complete");
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
}
@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
await connection.StartAsync().OrTimeout();
var result = await connection.InvokeAsync<string>("echo", originalMessage).OrTimeout();
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo).ToLowerInvariant(), originalMessage).OrTimeout();
Assert.Equal(originalMessage, result);
}
@ -677,7 +677,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try
{
await hubConnection.StartAsync().OrTimeout();
var message = await hubConnection.InvokeAsync<string>("Echo", "Hello, World!").OrTimeout();
var message = await hubConnection.InvokeAsync<string>(nameof(TestHub.Echo), "Hello, World!").OrTimeout();
Assert.Equal("Hello, World!", message);
}
catch (Exception ex)
@ -708,7 +708,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try
{
await hubConnection.StartAsync().OrTimeout();
var headerValues = await hubConnection.InvokeAsync<string[]>("GetHeaderValues", new object[] { new[] { "X-test", "X-42" } }).OrTimeout();
var headerValues = await hubConnection.InvokeAsync<string[]>(nameof(TestHub.GetHeaderValues), new object[] { new[] { "X-test", "X-42" } }).OrTimeout();
Assert.Equal(new[] { "42", "test" }, headerValues);
}
catch (Exception ex)
@ -742,7 +742,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try
{
await hubConnection.StartAsync().OrTimeout();
var cookieValue = await hubConnection.InvokeAsync<string>("GetCookieValue", new object[] { "Foo" }).OrTimeout();
var cookieValue = await hubConnection.InvokeAsync<string>(nameof(TestHub.GetCookieValue), new object[] { "Foo" }).OrTimeout();
Assert.Equal("Bar", cookieValue);
}
catch (Exception ex)
@ -772,7 +772,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
await hubConnection.StartAsync().OrTimeout();
var features = await hubConnection.InvokeAsync<object[]>("GetIHttpConnectionFeatureProperties").OrTimeout();
var features = await hubConnection.InvokeAsync<object[]>(nameof(TestHub.GetIHttpConnectionFeatureProperties)).OrTimeout();
var localPort = (Int64)features[0];
var remotePort = (Int64)features[1];
var localIP = (string)features[2];
@ -795,23 +795,56 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
}
}
[Fact]
public async Task NegotiationSkipsServerSentEventsWhenUsingBinaryProtocol()
{
using (StartLog(out var loggerFactory))
{
var hubConnection = new HubConnectionBuilder()
.WithUrl(_serverFixture.Url + "/default-nowebsockets")
.WithHubProtocol(new MessagePackHubProtocol())
.WithLoggerFactory(loggerFactory)
.Build();
try
{
await hubConnection.StartAsync().OrTimeout();
var transport = await hubConnection.InvokeAsync<TransportType>(nameof(TestHub.GetActiveTransportName)).OrTimeout();
Assert.Equal(TransportType.LongPolling, transport);
}
catch (Exception ex)
{
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
throw;
}
finally
{
await hubConnection.DisposeAsync().OrTimeout();
}
}
}
public static IEnumerable<object[]> HubProtocolsAndTransportsAndHubPaths
{
get
{
foreach (var protocol in HubProtocols)
{
foreach (var transport in TransportTypes().SelectMany(t => t))
foreach (var transport in TransportTypes().SelectMany(t => t).Cast<TransportType>())
{
foreach (var hubPath in HubPaths)
{
yield return new object[] { protocol, transport, hubPath };
if (!(protocol is MessagePackHubProtocol) || transport != TransportType.ServerSentEvents)
{
yield return new object[] { protocol, transport, hubPath };
}
}
}
}
}
}
// This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests.
public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" };
public static IEnumerable<IHubProtocol> HubProtocols =>

View File

@ -9,6 +9,7 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Sockets;
namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
@ -57,6 +58,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
return result;
}
public string GetActiveTransportName()
{
return Context.Connection.Metadata[ConnectionMetadataNames.Transport].ToString();
}
}
public class DynamicTestHub : DynamicHub

View File

@ -7,6 +7,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
routes.MapHub<DynamicTestHub>("/dynamic");
routes.MapHub<TestHubT>("/hubT");
routes.MapHub<HubWithAuthorization>("/authorizedhub");
routes.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = TransportType.LongPolling | TransportType.ServerSentEvents);
});
app.Run(async (context) =>

View File

@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Client.Tests
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return WithConnectionAsync(CreateConnection(), async (connection, closed) =>
{
// Start the connection
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Abort with an error
var expected = new Exception("Ruh roh!");
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
{
// Start the connection
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Stop normally
var stopTask = connection.StopAsync().OrTimeout();
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
{
// Start the connection
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Abort with an error
var expected = new Exception("Ruh roh!");
@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(out var syncPoint))), async (connection, closed) =>
{
// Start the connection
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Abort with an error
var expected = new Exception("Ruh roh!");
@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
// Wait to reach the first sync point
await syncPoint.WaitForSyncPoint().OrTimeout();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message);
// Release the sync point and wait for close to complete
@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
Assert.Same(expected, actual);
// We can start now
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// And we can stop without getting the abort exception.
await connection.StopAsync().OrTimeout();

View File

@ -6,6 +6,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
var exception =
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await connection.StartAsync().OrTimeout());
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
});
}
@ -47,11 +48,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync();
var exception =
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await connection.StartAsync().OrTimeout());
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
});
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await connection.DisposeAsync();
var exception =
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await connection.StartAsync().OrTimeout());
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
});
@ -91,7 +92,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
// Start the connection and wait for the transport to start up.
var startTask = connection.StartAsync();
var startTask = connection.StartAsync(TransferFormat.Text);
await transportStart.WaitForSyncPoint().OrTimeout();
// While the transport is starting, dispose the connection
@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
Assert.Equal(0, startCounter);
await connection.StartAsync();
await connection.StartAsync(TransferFormat.Text);
Assert.Equal(passThreshold, startCounter);
});
}
@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection, closed) =>
{
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync());
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text));
Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message);
Assert.Equal(3, startCounter);
});
@ -180,9 +181,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StopAsync().OrTimeout();
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
});
}
}
@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
// Start and wait for the transport to start up.
var startTask = connection.StartAsync();
var startTask = connection.StartAsync(TransferFormat.Text);
await transportStart.WaitForSyncPoint().OrTimeout();
// Stop the connection while it's starting
@ -223,7 +224,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout();
await closed.OrTimeout();
});
@ -257,14 +258,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(httpHandler, loggerFactory),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.SendAsync(new byte[] { 0x42 }).OrTimeout();
// Wait for the connection to close, because the send failed.
await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout());
// Start it up again
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
});
}
}
@ -281,7 +282,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
// Start the connection
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Stop the connection
var stopTask = connection.StopAsync().OrTimeout();
@ -299,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await disposeTask.OrTimeout();
// We should be disposed and thus unable to restart.
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
});
}
@ -314,7 +315,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StopAsync().OrTimeout();
await closed.OrTimeout();
await connection.DisposeAsync().OrTimeout();
@ -329,7 +330,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout();
await closed.OrTimeout();
});
@ -346,7 +347,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(transport: testTransport),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
testTransport.Application.Output.Complete(expected);
var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout());
Assert.Same(expected, actual);
@ -384,7 +385,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
// Start the transport
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.False(longPollingTransport.Running.IsCompleted, "Expected that the transport would still be running");
// Stop the connection, and we should stop the transport

View File

@ -14,26 +14,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
public partial class HttpConnectionTests
{
private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null)
private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null)
{
var httpOptions = new HttpOptions()
{
HttpMessageHandler = (httpMessageHandler) => httpHandler ?? TestHttpMessageHandler.CreateDefault(),
};
return CreateConnection(httpOptions, loggerFactory, url, transport);
return CreateConnection(httpOptions, loggerFactory, url, transport, transportFactory);
}
private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null)
private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null)
{
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
var uri = new Uri(url ?? "http://fakeuri.org/");
var connection = (transport != null) ?
new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions) :
new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions);
return connection;
if (transportFactory != null)
{
return new HttpConnection(uri, transportFactory, loggerFactory, httpOptions);
}
else if (transport != null)
{
return new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions);
}
else
{
return new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions);
}
}
private static async Task WithConnectionAsync(HttpConnection connection, Func<HttpConnection, Task, Task> body)

View File

@ -5,6 +5,10 @@ using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Moq;
using Newtonsoft.Json;
using Xunit;
using TransportType = Microsoft.AspNetCore.Sockets.TransportType;
@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
[Fact]
public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports()
{
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(transportTypes: null), "No transports returned in negotiation response.");
return RunInvalidNegotiateResponseTest<InvalidOperationException>(ResponseUtils.CreateNegotiationContent(transportTypes: 0), "Unable to connect to the server with any of the available transports.");
}
[Theory]
@ -67,12 +71,104 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(testHttpHandler, url: requestedUrl),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
});
Assert.Equal(expectedNegotiate, await negotiateUrlTcs.Task.OrTimeout());
}
[Fact]
public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand()
{
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnNegotiate((request, cancellationToken) =>
{
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "00000000-0000-0000-0000-000000000000",
availableTransports = new object[]
{
new
{
transport = "QuantumEntanglement",
transferFormats = new string[] { "Qbits" },
},
new
{
transport = "CarrierPigeon",
transferFormats = new string[] { "Text" },
},
new
{
transport = "LongPolling",
transferFormats = new string[] { "Text", "Binary" }
},
}
}));
});
var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);
transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling))
.Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));
await WithConnectionAsync(
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
async (connection, closed) =>
{
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
});
}
[Fact]
public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferFormat()
{
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnNegotiate((request, cancellationToken) =>
{
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "00000000-0000-0000-0000-000000000000",
availableTransports = new object[]
{
new
{
transport = "WebSockets",
transferFormats = new string[] { "Qbits" },
},
new
{
transport = "ServerSentEvents",
transferFormats = new string[] { "Text" },
},
new
{
transport = "LongPolling",
transferFormats = new string[] { "Text", "Binary" }
},
}
}));
});
var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);
transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling))
.Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));
await WithConnectionAsync(
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
async (connection, closed) =>
{
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
});
}
private async Task RunInvalidNegotiateResponseTest<TException>(string negotiatePayload, string expectedExceptionMessage) where TException : Exception
{
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
@ -84,7 +180,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) =>
{
var exception = await Assert.ThrowsAsync<TException>(
() => connection.StartAsync().OrTimeout());
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal(expectedExceptionMessage, exception.Message);
});

View File

@ -6,6 +6,7 @@ using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Client.Tests
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return Task.CompletedTask;
}, receiveTcs);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
});
}
@ -65,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return Task.CompletedTask;
}, receiveTcs);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
Assert.True(receivedRaised);
});
@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return Task.CompletedTask;
}, receiveTcs);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
Assert.True(receivedRaised);
});

View File

@ -6,6 +6,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Client.Tests
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(testHttpHandler),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.SendAsync(data).OrTimeout();
@ -66,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StopAsync().OrTimeout();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
@ -82,7 +83,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
@ -114,7 +115,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(testHttpHandler),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.SendAsync(new byte[] { 0 }).OrTimeout();

View File

@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var onReceived = new SyncPoint();
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// This will trigger the received callback
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
logger.LogInformation("Starting connection");
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
logger.LogInformation("Started connection");
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
@ -131,23 +131,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
}
[Fact]
public async Task StartAsyncSetsTransferModeFeature()
{
var testTransport = new TestTransport(transferMode: TransferMode.Binary);
await WithConnectionAsync(
CreateConnection(transport: testTransport),
async (connection, closed) =>
{
Assert.Null(connection.Features.Get<ITransferModeFeature>());
await connection.StartAsync().OrTimeout();
var transferModeFeature = connection.Features.Get<ITransferModeFeature>();
Assert.NotNull(transferModeFeature);
Assert.Equal(TransferMode.Binary, transferModeFeature.TransferMode);
});
}
[Fact]
public async Task HttpOptionsSetOntoHttpClientHandler()
{
@ -180,7 +163,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(httpOptions, url: "http://fakeuri.org/"),
async (connection, closed) =>
{
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
});
Assert.NotNull(httpClientHandler);

View File

@ -324,76 +324,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
}
[Fact]
public async Task MessagesEncodedWhenUsingBinaryProtocolOverTextTransport()
{
var connection = new TestConnection(TransferMode.Text);
var hubConnection = new HubConnection(connection,
new MessagePackHubProtocol(), new LoggerFactory());
try
{
await hubConnection.StartAsync().OrTimeout();
await hubConnection.SendAsync("MyMethod", 42).OrTimeout();
await connection.ReadSentTextMessageAsync().OrTimeout();
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
// The message is in the following format `size:payload;`
var parts = invokeMessage.Split(':');
Assert.Equal(2, parts.Length);
Assert.True(int.TryParse(parts[0], out var payloadSize));
Assert.Equal(payloadSize, parts[1].Length - 1);
Assert.EndsWith(";", parts[1]);
// this throws if the message is not a valid base64 string
Convert.FromBase64String(parts[1].Substring(0, payloadSize));
}
finally
{
await hubConnection.DisposeAsync().OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
}
[Fact]
public async Task MessagesDecodedWhenUsingBinaryProtocolOverTextTransport()
{
var connection = new TestConnection(TransferMode.Text);
var hubConnection = new HubConnection(connection,
new MessagePackHubProtocol(), new LoggerFactory());
var invocationTcs = new TaskCompletionSource<int>();
try
{
await hubConnection.StartAsync().OrTimeout();
hubConnection.On<int>("MyMethod", result => invocationTcs.SetResult(result));
using (var ms = new MemoryStream())
{
new MessagePackHubProtocol()
.WriteMessage(new InvocationMessage(null, "MyMethod", null, 42), ms);
var invokeMessage = Convert.ToBase64String(ms.ToArray());
var payloadSize = invokeMessage.Length.ToString(CultureInfo.InvariantCulture);
var message = $"{payloadSize}:{invokeMessage};";
connection.ReceivedMessages.TryWrite(Encoding.UTF8.GetBytes(message));
}
Assert.Equal(42, await invocationTcs.Task.OrTimeout());
}
finally
{
await hubConnection.DisposeAsync().OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
}
[Fact]
public async Task AcceptsPingMessages()
{
var connection = new TestConnection(TransferMode.Text);
var connection = new TestConnection();
var hubConnection = new HubConnection(connection,
new JsonHubProtocol(), new LoggerFactory());

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.Logging;
using Moq;
@ -21,12 +22,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public async Task StartAsyncCallsConnectionStart()
{
var connection = new Mock<IConnection>();
var protocol = new Mock<IHubProtocol>();
protocol.SetupGet(p => p.TransferFormat).Returns(TransferFormat.Text);
connection.SetupGet(p => p.Features).Returns(new FeatureCollection());
connection.Setup(m => m.StartAsync()).Returns(Task.CompletedTask).Verifiable();
var hubConnection = new HubConnection(connection.Object, Mock.Of<IHubProtocol>(), null);
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Returns(Task.CompletedTask).Verifiable();
var hubConnection = new HubConnection(connection.Object, protocol.Object, null);
await hubConnection.StartAsync();
connection.Verify(c => c.StartAsync(), Times.Once());
connection.Verify(c => c.StartAsync(TransferFormat.Text), Times.Once());
}
[Fact]
@ -34,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
var connection = new Mock<IConnection>();
connection.Setup(m => m.Features).Returns(new FeatureCollection());
connection.Setup(m => m.StartAsync()).Verifiable();
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Verifiable();
var hubConnection = new HubConnection(connection.Object, Mock.Of<IHubProtocol>(), null);
await hubConnection.DisposeAsync();
@ -249,7 +252,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public string Name => "MockHubProtocol";
public ProtocolType Type => ProtocolType.Binary;
public TransferFormat TransferFormat => TransferFormat.Binary;
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
transportActiveTask = longPollingTransport.Running;
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
await longPollingTransport.Running.OrTimeout();
@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
var data = await pair.Transport.Input.ReadAllAsync().OrTimeout();
await longPollingTransport.Running.OrTimeout();
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
var exception =
await Assert.ThrowsAsync<HttpRequestException>(async () =>
@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
pair.Transport.Output.Complete();
@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
// Start the transport
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
// Wait for the transport to finish
await longPollingTransport.Running.OrTimeout();
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Client.Tests
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("World"));
// Start the transport
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
pair.Transport.Output.Complete();
@ -360,9 +360,9 @@ namespace Microsoft.AspNetCore.Client.Tests
}
[Theory]
[InlineData(TransferMode.Binary)]
[InlineData(TransferMode.Text)]
public async Task LongPollingTransportSetsTransferMode(TransferMode transferMode)
[InlineData(TransferFormat.Binary)]
[InlineData(TransferFormat.Text)]
public async Task LongPollingTransportSetsTransferFormat(TransferFormat transferFormat)
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
@ -381,9 +381,7 @@ namespace Microsoft.AspNetCore.Client.Tests
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Assert.Null(longPollingTransport.Mode);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: new TestConnection());
Assert.Equal(transferMode, longPollingTransport.Mode);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: new TestConnection());
}
finally
{
@ -415,8 +413,7 @@ namespace Microsoft.AspNetCore.Client.Tests
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Assert.Null(longPollingTransport.Mode);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: new TestConnection());
}
finally
{
@ -436,8 +433,10 @@ namespace Microsoft.AspNetCore.Client.Tests
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
}
[Fact]
public async Task LongPollingTransportThrowsForInvalidTransferMode()
[Theory]
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
[InlineData((TransferFormat)42)] // Unexpected value
public async Task LongPollingTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
@ -452,10 +451,10 @@ namespace Microsoft.AspNetCore.Client.Tests
{
var longPollingTransport = new LongPollingTransport(httpClient);
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: new TestConnection()));
longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: new TestConnection()));
Assert.Contains("Invalid transfer mode.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName);
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("transferFormat", exception.ParamName);
}
}
@ -488,7 +487,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
var completedTask = await Task.WhenAny(completionTcs.Task, longPollingTransport.Running).OrTimeout();
Assert.Equal(completionTcs.Task, completedTask);

View File

@ -2,10 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Sockets;
using Newtonsoft.Json;
using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType;
namespace Microsoft.AspNetCore.Client.Tests
@ -36,35 +37,36 @@ namespace Microsoft.AspNetCore.Client.Tests
}
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
SocketsTransportType? transportTypes = SocketsTransportType.All)
SocketsTransportType transportTypes = SocketsTransportType.All)
{
var sb = new StringBuilder("{ ");
if (connectionId != null)
{
sb.Append($"\"connectionId\": \"{connectionId}\",");
}
if (transportTypes != null)
{
sb.Append($"\"availableTransports\": [ ");
if ((transportTypes & SocketsTransportType.WebSockets) == SocketsTransportType.WebSockets)
{
sb.Append($"\"{nameof(SocketsTransportType.WebSockets)}\",");
}
if ((transportTypes & SocketsTransportType.ServerSentEvents) == SocketsTransportType.ServerSentEvents)
{
sb.Append($"\"{nameof(SocketsTransportType.ServerSentEvents)}\",");
}
if ((transportTypes & SocketsTransportType.LongPolling) == SocketsTransportType.LongPolling)
{
sb.Append($"\"{nameof(SocketsTransportType.LongPolling)}\",");
}
sb.Length--;
sb.Append("],");
}
sb.Length--;
sb.Append("}");
var availableTransports = new List<object>();
return sb.ToString();
if ((transportTypes & SocketsTransportType.WebSockets) != 0)
{
availableTransports.Add(new
{
transport = nameof(SocketsTransportType.WebSockets),
transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) }
});
}
if ((transportTypes & SocketsTransportType.ServerSentEvents) != 0)
{
availableTransports.Add(new
{
transport = nameof(SocketsTransportType.ServerSentEvents),
transferFormats = new[] { nameof(TransferFormat.Text) }
});
}
if ((transportTypes & SocketsTransportType.LongPolling) != 0)
{
availableTransports.Add(new
{
transport = nameof(SocketsTransportType.LongPolling),
transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) }
});
}
return JsonConvert.SerializeObject(new { connectionId, availableTransports });
}
}
}

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var sseTransport = new ServerSentEventsTransport(httpClient);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
await eventStreamTcs.Task.OrTimeout();
await sseTransport.StopAsync().OrTimeout();
@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
transportActiveTask = sseTransport.Running;
Assert.False(transportActiveTask.IsCompleted);
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
var exception = await Assert.ThrowsAsync<FormatException>(() => sseTransport.Running.OrTimeout());
Assert.Equal("Incomplete message.", exception.Message);
@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
await eventStreamTcs.Task;
await pair.Transport.Output.WriteAsync(new byte[] { 0x42 });
@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
await eventStreamTcs.Task.OrTimeout();
pair.Transport.Output.Complete();
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
var message = await pair.Transport.Input.ReadSingleAsync().OrTimeout();
Assert.Equal("3:abc", Encoding.ASCII.GetString(message));
@ -276,10 +276,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
}
[Theory]
[InlineData(TransferMode.Text)]
[InlineData(TransferMode.Binary)]
public async Task SSETransportSetsTransferMode(TransferMode transferMode)
[Fact]
public async Task SSETransportDoesNotSupportBinary()
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
@ -293,12 +291,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var sseTransport = new ServerSentEventsTransport(httpClient);
Assert.Null(sseTransport.Mode);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: Mock.Of<IConnection>()).OrTimeout();
Assert.Equal(TransferMode.Text, sseTransport.Mode);
await sseTransport.StopAsync().OrTimeout();
var ex = await Assert.ThrowsAsync<ArgumentException>(() => sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout());
Assert.Equal($"The 'Binary' transfer format is not supported by this transport.{Environment.NewLine}Parameter name: transferFormat", ex.Message);
}
}
@ -322,7 +318,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var sseTransport = new ServerSentEventsTransport(httpClient);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
await sseTransport.StopAsync().OrTimeout();
}
@ -338,8 +334,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
}
[Fact]
public async Task SSETransportThrowsForInvalidTransferMode()
[Theory]
[InlineData(TransferFormat.Binary)] // Binary not supported
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
[InlineData((TransferFormat)42)] // Unexpected value
public async Task SSETransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
@ -354,10 +353,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
var sseTransport = new ServerSentEventsTransport(httpClient);
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: Mock.Of<IConnection>()));
sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: Mock.Of<IConnection>()));
Assert.Contains("Invalid transfer mode.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName);
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("transferFormat", exception.ParamName);
}
}
}

View File

@ -29,8 +29,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource();
private Task _receiveLoop;
private TransferMode? _transferMode;
public event Action<Exception> Closed;
public Task Started => _started.Task;
public Task Disposed => _disposed.Task;
@ -44,9 +42,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public IFeatureCollection Features { get; } = new FeatureCollection();
public TestConnection(TransferMode? transferMode = null)
public TestConnection()
{
_transferMode = transferMode;
_receiveLoop = ReceiveLoopAsync(_receiveShutdownToken.Token);
}
@ -80,20 +77,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
throw new ObjectDisposedException("Unable to send message, underlying channel was closed");
}
public Task StartAsync()
public Task StartAsync(TransferFormat transferFormat)
{
if (_transferMode.HasValue)
{
var transferModeFeature = Features.Get<ITransferModeFeature>();
if (transferModeFeature == null)
{
transferModeFeature = new TransferModeFeature();
Features.Set(transferModeFeature);
}
transferModeFeature.TransferMode = _transferMode.Value;
}
_started.TrySetResult(null);
return Task.CompletedTask;
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO.Pipelines;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
@ -12,18 +11,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
private readonly Func<Task> _stopHandler;
private readonly Func<Task> _startHandler;
public TransferMode? Mode { get; }
public TransferFormat? Format { get; }
public IDuplexPipe Application { get; private set; }
public TestTransport(Func<Task> onTransportStop = null, Func<Task> onTransportStart = null, TransferMode transferMode = TransferMode.Text)
public TestTransport(Func<Task> onTransportStop = null, Func<Task> onTransportStart = null, TransferFormat transferFormat = TransferFormat.Text)
{
_stopHandler = onTransportStop ?? new Func<Task>(() => Task.CompletedTask);
_startHandler = onTransportStart ?? new Func<Task>(() => Task.CompletedTask);
Mode = transferMode;
Format = transferFormat;
}
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
{
if ((Format & transferFormat) == 0)
{
throw new InvalidOperationException($"The '{transferFormat}' transfer format is not supported by this transport.");
}
Application = application;
return _startHandler();
}

View File

@ -1,59 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
{
public class Base64EncoderTests
{
[Theory]
[MemberData(nameof(Payloads))]
public void VerifyDecode(string payload, string encoded)
{
var message = Encoding.UTF8.GetBytes(payload);
var encodedMessage = Encoding.UTF8.GetString(new Base64Encoder().Encode(message));
Assert.Equal(encoded, encodedMessage);
}
[Theory]
[MemberData(nameof(Payloads))]
public void VerifyEncode(string payload, string encoded)
{
ReadOnlySpan<byte> encodedMessage = Encoding.UTF8.GetBytes(encoded);
var encoder = new Base64Encoder();
encoder.TryDecode(ref encodedMessage, out var data);
var decodedMessage = Encoding.UTF8.GetString(data.ToArray());
Assert.Equal(payload, decodedMessage);
}
[Fact]
public void CanParseMultipleMessages()
{
ReadOnlySpan<byte> data = Encoding.UTF8.GetBytes("28:QQpSDUMNCjtERUYxMjM0NTY3ODkw;4:QUJD;4:QUJD;");
var encoder = new Base64Encoder();
Assert.True(encoder.TryDecode(ref data, out var payload1));
Assert.True(encoder.TryDecode(ref data, out var payload2));
Assert.True(encoder.TryDecode(ref data, out var payload3));
Assert.False(encoder.TryDecode(ref data, out var payload4));
Assert.Equal(0, data.Length);
var payload1Value = Encoding.UTF8.GetString(payload1.ToArray());
var payload2Value = Encoding.UTF8.GetString(payload2.ToArray());
var payload3Value = Encoding.UTF8.GetString(payload3.ToArray());
Assert.Equal("A\nR\rC\r\n;DEF1234567890", payload1Value);
Assert.Equal("ABC", payload2Value);
Assert.Equal("ABC", payload3Value);
}
public static IEnumerable<object[]> Payloads =>
new object[][]
{
new object[] { "", "0:;" },
new object[] { "ABC", "4:QUJD;" },
new object[] { "A\nR\rC\r\n;DEF1234567890", "28:QQpSDUMNCjtERUYxMjM0NTY3ODkw;" },
};
}
}

View File

@ -1,90 +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.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Encoders
{
public class LengthPrefixedTextMessageParserTests
{
[Theory]
[InlineData("0:;", "")]
[InlineData("3:ABC;", "ABC")]
[InlineData("11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")]
[InlineData("12:Hello, World;", "Hello, World")]
public void ReadTextMessage(string encoded, string payload)
{
ReadOnlySpan<byte> buffer = Encoding.UTF8.GetBytes(encoded);
Assert.True(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message));
Assert.Equal(0, buffer.Length);
Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray());
}
[Fact]
public void ReadMultipleMessages()
{
const string encoded = "0:;14:Hello,\r\nWorld!;";
ReadOnlySpan<byte> buffer = Encoding.UTF8.GetBytes(encoded);
var messages = new List<byte[]>();
while (LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message))
{
messages.Add(message.ToArray());
}
Assert.Equal(0, buffer.Length);
Assert.Equal(2, messages.Count);
Assert.Equal(new byte[0], messages[0]);
Assert.Equal(Encoding.UTF8.GetBytes("Hello,\r\nWorld!"), messages[1]);
}
[Theory]
[InlineData("")]
[InlineData("ABC")]
[InlineData("1230450945")]
[InlineData("1:")]
[InlineData("10")]
[InlineData("5:A")]
[InlineData("5:ABCDE")]
public void ReadIncompleteMessages(string encoded)
{
ReadOnlySpan<byte> buffer = Encoding.UTF8.GetBytes(encoded);
Assert.False(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _));
}
[Theory]
[InlineData("X:", "Invalid length: 'X'")]
[InlineData("1:asdf", "Missing delimiter ';' after payload")]
[InlineData("1029348109238412903849023841290834901283409128349018239048102394:ABCDEF", "Invalid length: '1029348109238412903849023841290834901283409128349018239048102394'")]
[InlineData("12ab34:", "Invalid length: '12ab34'")]
[InlineData("5:ABCDEF", "Missing delimiter ';' after payload")]
public void ReadInvalidMessages(string encoded, string expectedMessage)
{
var ex = Assert.Throws<FormatException>(() =>
{
ReadOnlySpan<byte> buffer = Encoding.UTF8.GetBytes(encoded);
LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _);
});
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void ReadInvalidEncodedMessage()
{
var ex = Assert.Throws<FormatException>(() =>
{
// Invalid because first character is a UTF-8 "continuation" character
// We need to include the ':' so that
ReadOnlySpan<byte> buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' };
LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _);
});
Assert.Equal("Invalid length: 'He<48>lo'", ex.Message);
}
}
}

View File

@ -112,7 +112,11 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
foreach (var transport in TransportTypes())
{
yield return new object[] { transport, new JsonHubProtocol() };
yield return new object[] { transport, new MessagePackHubProtocol() };
if (transport != TransportType.ServerSentEvents)
{
yield return new object[] { transport, new MessagePackHubProtocol() };
}
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance)
{
ProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder())
Protocol = new JsonHubProtocol()
};
}
public static Mock<HubConnectionContext> CreateMock(DefaultConnectionContext connection)
{
var mock = new Mock<HubConnectionContext>(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { CallBase = true };
var readerWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder());
mock.SetupGet(m => m.ProtocolReaderWriter).Returns(readerWriter);
var protocol = new JsonHubProtocol();
mock.SetupGet(m => m.Protocol).Returns(protocol);
return mock;
}

View File

@ -2,6 +2,7 @@
// 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;
@ -9,7 +10,6 @@ using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public class TestClient : IDisposable
{
private static int _id;
private readonly HubProtocolReaderWriter _protocolReaderWriter;
private readonly IHubProtocol _protocol;
private readonly IInvocationBinder _invocationBinder;
private CancellationTokenSource _cts;
private Queue<HubMessage> _messages = new Queue<HubMessage>();
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public DefaultConnectionContext Connection { get; }
public Task Connected => ((TaskCompletionSource<bool>)Connection.Metadata["ConnectedTask"]).Task;
public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IDataEncoder dataEncoder = null, IInvocationBinder invocationBinder = null, bool addClaimId = false)
public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, bool addClaimId = false)
{
var options = new PipeOptions(readerScheduler: synchronousCallbacks ? PipeScheduler.Inline : null);
var pair = DuplexPipe.CreateConnectionPair(options, options);
@ -42,16 +42,14 @@ namespace Microsoft.AspNetCore.SignalR.Tests
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims));
Connection.Metadata["ConnectedTask"] = new TaskCompletionSource<bool>();
protocol = protocol ?? new JsonHubProtocol();
dataEncoder = dataEncoder ?? new PassThroughEncoder();
_protocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder);
_protocol = protocol ?? new JsonHubProtocol();
_invocationBinder = invocationBinder ?? new DefaultInvocationBinder();
_cts = new CancellationTokenSource();
using (var memoryStream = new MemoryStream())
{
NegotiationProtocol.WriteMessage(new NegotiationMessage(protocol.Name), memoryStream);
NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream);
Connection.Application.Output.WriteAsync(memoryStream.ToArray());
}
}
@ -143,7 +141,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public async Task<string> SendHubMessageAsync(HubMessage message)
{
var payload = _protocolReaderWriter.WriteMessage(message);
var payload = _protocol.WriteToArray(message);
await Connection.Application.Output.WriteAsync(payload);
return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null;
}
@ -201,7 +200,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
try
{
if (_protocolReaderWriter.ReadMessages(result.Buffer, _invocationBinder, out var messages, out consumed, out examined))
var messages = new List<HubMessage>();
if (_protocol.TryParseMessages(result.Buffer.ToArray(), _invocationBinder, messages))
{
foreach (var m in messages)
{

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
// The test should connect to the server using WebSockets transport on Windows 8 and newer.
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
var connection = new HttpConnection(new Uri(url));
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
// The test logic lives in the TestTransportFactory and FakeTransport.
var connection = new HttpConnection(new Uri(url), new TestTransportFactory(), null, null);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), transportType);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}, receiveTcs);
var message = new byte[] { 42 };
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
await connection.SendAsync(message).OrTimeout();
var receivedData = await receiveTcs.Task.OrTimeout();
@ -166,8 +166,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
[Theory(Skip = "https://github.com/aspnet/SignalR/issues/1485")]
[MemberData(nameof(TransportTypesAndTransferModes))]
public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferMode requestedTransferMode)
[MemberData(nameof(TransportTypesAndTransferFormats))]
public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferFormat requestedTransferFormat)
{
using (StartLog(out var loggerFactory, testName: $"ConnectionCanSendAndReceiveMessages_{transportType.ToString()}"))
{
@ -177,9 +177,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), transportType, loggerFactory);
connection.Features.Set<ITransferModeFeature>(
new TransferModeFeature { TransferMode = requestedTransferMode });
try
{
var closeTcs = new TaskCompletionSource<object>();
@ -199,28 +196,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests
connection.OnReceived((data, state) =>
{
logger.LogInformation("Received {length} byte message", data.Length);
if (IsBase64Encoded(requestedTransferMode, connection))
{
data = Convert.FromBase64String(Encoding.UTF8.GetString(data));
}
var tcs = (TaskCompletionSource<string>)state;
tcs.TrySetResult(Encoding.UTF8.GetString(data));
return Task.CompletedTask;
}, receiveTcs);
logger.LogInformation("Starting connection to {url}", url);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(requestedTransferFormat).OrTimeout();
logger.LogInformation("Started connection to {url}", url);
var bytes = Encoding.UTF8.GetBytes(message);
// Need to encode binary payloads sent over text transports
if (IsBase64Encoded(requestedTransferMode, connection))
{
bytes = Encoding.UTF8.GetBytes(Convert.ToBase64String(bytes));
}
logger.LogInformation("Sending {length} byte message", bytes.Length);
try
{
@ -255,12 +241,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
}
private bool IsBase64Encoded(TransferMode transferMode, IConnection connection)
{
return transferMode == TransferMode.Binary &&
connection.Features.Get<ITransferModeFeature>().TransferMode == TransferMode.Text;
}
public static IEnumerable<object[]> MessageSizesData
{
get
@ -281,8 +261,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), TransportType.WebSockets, loggerFactory);
connection.Features.Set<ITransferModeFeature>(
new TransferModeFeature { TransferMode = TransferMode.Binary });
try
{
@ -296,7 +274,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}, receiveTcs);
logger.LogInformation("Starting connection to {url}", url);
await connection.StartAsync().OrTimeout();
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
logger.LogInformation("Started connection to {url}", url);
var bytes = Encoding.UTF8.GetBytes(message);
@ -416,16 +394,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
private class FakeTransport : ITransport
{
public TransferMode? Mode => TransferMode.Text;
public string prevConnectionId = null;
private int tries = 0;
private IDuplexPipe _application;
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
{
_application = application;
tries++;
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
if (prevConnectionId == null)
{
prevConnectionId = id;
@ -467,14 +444,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
}
public static IEnumerable<object[]> TransportTypesAndTransferModes
public static IEnumerable<object[]> TransportTypesAndTransferFormats
{
get
{
foreach (var transport in TransportTypes)
{
yield return new object[] { transport[0], TransferMode.Text };
yield return new object[] { transport[0], TransferMode.Binary };
yield return new object[] { transport[0], TransferFormat.Text };
if ((TransportType)transport[0] != TransportType.ServerSentEvents)
{
yield return new object[] { transport[0], TransferFormat.Binary };
}
}
}
}

View File

@ -9,7 +9,6 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils;
using Microsoft.AspNetCore.Sockets;
@ -1333,10 +1332,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
using (var client = new TestClient(synchronousCallbacks: false, protocol: protocol, invocationBinder: invocationBinder.Object))
{
var transportFeature = new Mock<IConnectionTransportFeature>();
transportFeature.SetupGet(f => f.TransportCapabilities)
.Returns(protocol.Type == ProtocolType.Binary ? TransferMode.Binary : TransferMode.Text);
client.Connection.Features.Set(transportFeature.Object);
client.Connection.SupportedFormats = protocol.TransferFormat;
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
@ -1419,7 +1415,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
using (var client1 = new TestClient(protocol: new JsonHubProtocol()))
using (var client2 = new TestClient(protocol: new MessagePackHubProtocol(), dataEncoder: new Base64Encoder()))
using (var client2 = new TestClient(protocol: new MessagePackHubProtocol()))
{
var endPointLifetime1 = endPoint.OnConnectedAsync(client1.Connection);
var endPointLifetime2 = endPoint.OnConnectedAsync(client2.Connection);
@ -1617,9 +1613,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var msgPackOptions = serviceProvider.GetRequiredService<IOptions<MessagePackHubProtocolOptions>>();
using (var client = new TestClient(synchronousCallbacks: false, protocol: new MessagePackHubProtocol(msgPackOptions)))
{
var transportFeature = new Mock<IConnectionTransportFeature>();
transportFeature.SetupGet(f => f.TransportCapabilities).Returns(TransferMode.Binary);
client.Connection.Features.Set(transportFeature.Object);
client.Connection.SupportedFormats = TransferFormat.Binary;
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
await client.Connected.OrTimeout();
@ -1792,6 +1786,21 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
}
[Fact]
public async Task NegotiatingFailsIfMsgPackRequestedOverTextOnlyTransport()
{
var serviceProvider = HubEndPointTestUtils.CreateServiceProvider(services =>
services.Configure<HubOptions>(options =>
options.KeepAliveInterval = TimeSpan.FromMilliseconds(100)));
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
using (var client = new TestClient(false, new MessagePackHubProtocol()))
{
client.Connection.SupportedFormats = TransferFormat.Text;
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => endPoint.OnConnectedAsync(client.Connection).OrTimeout());
}
}
public static IEnumerable<object[]> HubTypes()
{
yield return new[] { typeof(DynamicTestHub) };

View File

@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
TransferMode.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
await webSocketsTransport.StopAsync().OrTimeout();
await webSocketsTransport.Running.OrTimeout();
}
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application,
TransferMode.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("User-Agent"));
@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
TransferMode.Binary, connection: Mock.Of<IConnection>());
TransferFormat.Binary, connection: Mock.Of<IConnection>());
pair.Transport.Output.Complete();
await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10));
}
@ -128,15 +128,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
[InlineData(TransferMode.Text)]
[InlineData(TransferMode.Binary)]
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferMode transferMode)
[InlineData(TransferFormat.Text)]
[InlineData(TransferFormat.Binary)]
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat)
{
using (StartLog(out var loggerFactory))
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferMode, connection: Mock.Of<IConnection>());
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferFormat, connection: Mock.Of<IConnection>());
await pair.Transport.Output.WriteAsync(new byte[] { 0x42 });
@ -151,38 +151,38 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
[InlineData(TransferMode.Text)]
[InlineData(TransferMode.Binary)]
public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode)
[InlineData(TransferFormat.Text)]
[InlineData(TransferFormat.Binary)]
public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat)
{
using (StartLog(out var loggerFactory))
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
Assert.Null(webSocketsTransport.Mode);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
transferMode, connection: Mock.Of<IConnection>()).OrTimeout();
Assert.Equal(transferMode, webSocketsTransport.Mode);
transferFormat, connection: Mock.Of<IConnection>()).OrTimeout();
await webSocketsTransport.StopAsync().OrTimeout();
await webSocketsTransport.Running.OrTimeout();
}
}
[ConditionalFact]
[ConditionalTheory]
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
[InlineData((TransferFormat)42)] // Unexpected value
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
public async Task WebSocketsTransportThrowsForInvalidTransferMode()
public async Task WebSocketsTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
{
using (StartLog(out var loggerFactory))
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text | TransferMode.Binary, connection: Mock.Of<IConnection>()));
webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: Mock.Of<IConnection>()));
Assert.Contains("Invalid transfer mode.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName);
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("transferFormat", exception.ParamName);
}
}
}

View File

@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[InlineData(TransportType.All)]
[InlineData((TransportType)0)]
[InlineData(TransportType.LongPolling | TransportType.WebSockets)]
public async Task NegotiateReturnsAvailableTransports(TransportType transports)
public async Task NegotiateReturnsAvailableTransportsAfterFilteringByOptions(TransportType transports)
{
using (StartLog(out var loggerFactory, LogLevel.Debug))
{
@ -167,7 +167,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
var availableTransports = (TransportType)0;
foreach (var transport in negotiateResponse["availableTransports"])
{
availableTransports |= (TransportType)Enum.Parse(typeof(TransportType), transport.Value<string>());
var transportType = (TransportType)Enum.Parse(typeof(TransportType), transport.Value<string>("transport"));
availableTransports |= transportType;
}
Assert.Equal(transports, availableTransports);
@ -820,10 +821,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests
}
[Theory]
[InlineData(TransportType.LongPolling, TransferMode.Binary | TransferMode.Text)]
[InlineData(TransportType.ServerSentEvents, TransferMode.Text)]
[InlineData(TransportType.WebSockets, TransferMode.Binary | TransferMode.Text)]
public async Task TransportCapabilitiesSet(TransportType transportType, TransferMode expectedTransportCapabilities)
[InlineData(TransportType.LongPolling, null)]
[InlineData(TransportType.ServerSentEvents, TransferFormat.Text)]
[InlineData(TransportType.WebSockets, TransferFormat.Binary | TransferFormat.Text)]
public async Task TransferModeSet(TransportType transportType, TransferFormat? expectedTransferFormats)
{
using (StartLog(out var loggerFactory, LogLevel.Debug))
{
@ -845,7 +846,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0);
await dispatcher.ExecuteAsync(context, options, app);
Assert.Equal(expectedTransportCapabilities, connection.TransportCapabilities);
if (expectedTransferFormats != null)
{
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
Assert.Equal(expectedTransferFormats.Value, transferFormatFeature.SupportedFormats);
}
}
}

View File

@ -8,6 +8,8 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.AspNetCore.Sockets.Internal.Transports;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
@ -71,9 +73,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
}
[Theory]
[InlineData(TransferMode.Text, WebSocketMessageType.Text)]
[InlineData(TransferMode.Binary, WebSocketMessageType.Binary)]
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferModeFeature(TransferMode transferMode, WebSocketMessageType expectedMessageType)
[InlineData(TransferFormat.Text, WebSocketMessageType.Text)]
[InlineData(TransferFormat.Binary, WebSocketMessageType.Binary)]
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, WebSocketMessageType expectedMessageType)
{
using (StartLog(out var loggerFactory, LogLevel.Debug))
{
@ -82,7 +84,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
using (var feature = new TestWebSocketConnectionFeature())
{
var connectionContext = new DefaultConnectionContext(string.Empty, null, null) { TransferMode = transferMode };
var connectionContext = new DefaultConnectionContext(string.Empty, null, null);
connectionContext.ActiveFormat = transferFormat;
var ws = new WebSocketsTransport(new WebSocketOptions(), connection.Application, connectionContext, loggerFactory);
// Give the server socket to the transport and run it