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 System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -40,14 +39,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
protocol = new MessagePackHubProtocol(); protocol = new MessagePackHubProtocol();
} }
var encoder = new PassThroughEncoder();
for (var i = 0; i < Connections; ++i) for (var i = 0; i < Connections; ++i)
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport); var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport);
var hubConnection = new HubConnectionContext(connection, Timeout.InfiniteTimeSpan, NullLoggerFactory.Instance); var hubConnection = new HubConnectionContext(connection, Timeout.InfiniteTimeSpan, NullLoggerFactory.Instance);
hubConnection.ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, encoder); hubConnection.Protocol = protocol;
_hubLifetimeManager.OnConnectedAsync(hubConnection).Wait(); _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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -11,12 +11,11 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using DefaultConnectionContext = Microsoft.AspNetCore.Sockets.DefaultConnectionContext; using DefaultConnectionContext = Microsoft.AspNetCore.Sockets.DefaultConnectionContext;
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
@ -47,13 +46,13 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
_connectionContext = new NoErrorHubConnectionContext(connection, TimeSpan.Zero, NullLoggerFactory.Instance); _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 class FakeHubProtocol : IHubProtocol
{ {
public string Name { get; } public string Name { get; }
public ProtocolType Type { get; } public TransferFormat TransferFormat { get; }
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages) 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 class NoErrorHubConnectionContext : HubConnectionContext
{ {
public NoErrorHubConnectionContext(ConnectionContext connectionContext, TimeSpan keepAliveInterval, ILoggerFactory loggerFactory) : base(connectionContext, keepAliveInterval, loggerFactory) 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)); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
{ {
public class HubProtocolBenchmark public class HubProtocolBenchmark
{ {
private HubProtocolReaderWriter _hubProtocolReaderWriter; private IHubProtocol _hubProtocol;
private byte[] _binaryInput; private byte[] _binaryInput;
private TestBinder _binder; private TestBinder _binder;
private HubMessage _hubMessage; private HubMessage _hubMessage;
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
switch (HubProtocol) switch (HubProtocol)
{ {
case Protocol.MsgPack: case Protocol.MsgPack:
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new MessagePackHubProtocol(), new PassThroughEncoder()); _hubProtocol = new MessagePackHubProtocol();
break; break;
case Protocol.Json: case Protocol.Json:
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder()); _hubProtocol = new JsonHubProtocol();
break; break;
} }
@ -51,14 +52,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
break; break;
} }
_binaryInput = GetBytes(_hubMessage); _binaryInput = _hubProtocol.WriteToArray(_hubMessage);
_binder = new TestBinder(_hubMessage); _binder = new TestBinder(_hubMessage);
} }
[Benchmark] [Benchmark]
public void ReadSingleMessage() 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"); throw new InvalidOperationException("Failed to read message");
} }
@ -67,7 +69,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
[Benchmark] [Benchmark]
public void WriteSingleMessage() 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"); throw new InvalidOperationException("Failed to write message");
} }
@ -86,10 +89,5 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
ManyArguments = 2, ManyArguments = 2,
LargeArguments = 3 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> { function getJwtToken(url): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();

View File

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<!-- <!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
--> -->
<system.webServer> <system.webServer>
<handlers> <handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers> </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> </system.webServer>
</configuration> </configuration>

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@aspnet/signalr-protocol-msgpack", "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", "description": "MsgPack Protocol support for ASP.NET Core SignalR",
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"browser": "./dist/browser/signalr-protocol-msgpack.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: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: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", "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", "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/signalr-protocol-msgpack/spec/**/*.spec.js" "test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
}, },
"keywords": [ "keywords": [
"signalr", "signalr",

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@aspnet/signalr", "name": "@aspnet/signalr",
"version": "1.0.0-preview1-t000", "version": "1.0.0-preview2-t000",
"description": "ASP.NET Core SignalR Client", "description": "ASP.NET Core SignalR Client",
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"browser": "./dist/browser/signalr.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 { HttpConnection } from "../src/HttpConnection";
import { IHttpConnectionOptions } from "../src/HttpConnection"; import { IHttpConnectionOptions } from "../src/HttpConnection";
import { HttpResponse } from "../src/index"; 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 { eachEndpointUrl, eachTransport } from "./Common";
import { TestHttpClient } from "./TestHttpClient"; import { TestHttpClient } from "./TestHttpClient";
@ -15,13 +15,13 @@ const commonOptions: IHttpConnectionOptions = {
describe("HttpConnection", () => { describe("HttpConnection", () => {
it("cannot be created with relative url if document object is not present", () => { 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'.")); .toThrow(new Error("Cannot resolve '/test'."));
}); });
it("cannot be created with relative url if window object is not present", () => { it("cannot be created with relative url if window object is not present", () => {
(global as any).window = {}; (global as any).window = {};
expect(() => new HttpConnection("/test", commonOptions)) expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions))
.toThrow(new Error("Cannot resolve '/test'.")); .toThrow(new Error("Cannot resolve '/test'."));
delete (global as any).window; delete (global as any).window;
}); });
@ -34,7 +34,7 @@ describe("HttpConnection", () => {
.on("GET", (r) => ""), .on("GET", (r) => ""),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
@ -64,7 +64,7 @@ describe("HttpConnection", () => {
}), }),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
@ -86,7 +86,7 @@ describe("HttpConnection", () => {
.on("GET", (r) => ""), .on("GET", (r) => ""),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
@ -117,7 +117,7 @@ describe("HttpConnection", () => {
}), }),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
@ -129,17 +129,17 @@ describe("HttpConnection", () => {
}); });
it("can stop a non-started connection", async (done) => { 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(); await connection.stop();
done(); done();
}); });
it("preserves users connection string", async (done) => { it("preserves user's query string", async (done) => {
let connectUrl: string; let connectUrl: string;
const fakeTransport: ITransport = { const fakeTransport: ITransport = {
connect(url: string): Promise<TransferMode> { connect(url: string): Promise<void> {
connectUrl = url; connectUrl = url;
return Promise.reject(TransferMode.Text); return Promise.reject("");
}, },
send(data: any): Promise<void> { send(data: any): Promise<void> {
return Promise.reject(""); return Promise.reject("");
@ -159,7 +159,7 @@ describe("HttpConnection", () => {
transport: fakeTransport, transport: fakeTransport,
} as IHttpConnectionOptions; } 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 { try {
await connection.start(); await connection.start();
@ -190,7 +190,7 @@ describe("HttpConnection", () => {
}), }),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
connection = new HttpConnection(givenUrl, options); connection = new HttpConnection(givenUrl, TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
@ -213,18 +213,18 @@ describe("HttpConnection", () => {
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
...commonOptions, ...commonOptions,
httpClient: new TestHttpClient() httpClient: new TestHttpClient()
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") .on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
.on("GET", (r) => ""), .on("GET", (r) => ""),
transport: requestedTransport, transport: requestedTransport,
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
fail(); fail();
done(); done();
} catch (e) { } catch (e) {
expect(e.message).toBe("No available transports found."); expect(e.message).toBe("Unable to initialize any of the available transports.");
done(); done();
} }
}); });
@ -234,17 +234,17 @@ describe("HttpConnection", () => {
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
...commonOptions, ...commonOptions,
httpClient: new TestHttpClient() httpClient: new TestHttpClient()
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") .on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
.on("GET", (r) => ""), .on("GET", (r) => ""),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
fail(); fail();
done(); done();
} catch (e) { } catch (e) {
expect(e.message).toBe("No available transports found."); expect(e.message).toBe("Unable to initialize any of the available transports.");
done(); done();
} }
}); });
@ -256,7 +256,7 @@ describe("HttpConnection", () => {
transport: TransportType.WebSockets, transport: TransportType.WebSockets,
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
try { try {
await connection.start(); await connection.start();
fail(); fail();
@ -264,42 +264,24 @@ describe("HttpConnection", () => {
} catch (e) { } catch (e) {
// WebSocket is created when the transport is connecting which happens after // WebSocket is created when the transport is connecting which happens after
// negotiate request would be sent. No better/easier way to test this. // 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(); done();
} }
}); });
[ describe(".constructor", () => {
[TransferMode.Text, TransferMode.Text], it("throws if no Url is provided", async () => {
[TransferMode.Text, TransferMode.Binary], // Force TypeScript to let us call the constructor incorrectly :)
[TransferMode.Binary, TransferMode.Text], expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
[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;
const options: IHttpConnectionOptions = { it("throws if no TransferFormat is provided", async () => {
...commonOptions, // Force TypeScript to let us call the constructor incorrectly :)
httpClient: new TestHttpClient() expect(() => new (HttpConnection as any)("http://tempuri.org")).toThrowError("The 'transferFormat' argument is required.");
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") });
.on("GET", (r) => ""),
transport: fakeTransport,
} as IHttpConnectionOptions;
const connection = new HttpConnection("https://tempuri.org", options); it("throws if an unsupported TransferFormat is provided", async () => {
connection.features.transferMode = requestedTransferMode; expect(() => new HttpConnection("http://tempuri.org", 42)).toThrowError("Unknown transferFormat value: 42.");
await connection.start();
const actualTransferMode = connection.features.transferMode;
expect(actualTransferMode).toBe(transportTransferMode);
}); });
}); });
}); });

View File

@ -8,7 +8,7 @@ import { MessageType } from "../src/IHubProtocol";
import { ILogger, LogLevel } from "../src/ILogger"; import { ILogger, LogLevel } from "../src/ILogger";
import { Observer } from "../src/Observable"; import { Observer } from "../src/Observable";
import { TextMessageFormat } from "../src/TextMessageFormat"; 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 { IHubConnectionOptions } from "../src/HubConnection";
import { asyncit as it, captureException, delay, PromiseSource } from "./Utils"; import { asyncit as it, captureException, delay, PromiseSource } from "./Utils";

View File

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

View File

@ -56,4 +56,4 @@ export class PromiseSource<T> {
public reject(reason?: any) { public reject(reason?: any) {
this.rejecter(reason); 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 { IConnection } from "./IConnection";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { LoggerFactory } from "./Loggers"; 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 { export interface IHttpConnectionOptions {
httpClient?: HttpClient; httpClient?: HttpClient;
@ -23,7 +24,12 @@ const enum ConnectionState {
interface INegotiateResponse { interface INegotiateResponse {
connectionId: string; connectionId: string;
availableTransports: string[]; availableTransports: IAvailableTransport[];
}
interface IAvailableTransport {
transport: keyof typeof TransportType;
transferFormats: Array<keyof typeof TransferFormat>;
} }
export class HttpConnection implements IConnection { export class HttpConnection implements IConnection {
@ -33,13 +39,19 @@ export class HttpConnection implements IConnection {
private readonly httpClient: HttpClient; private readonly httpClient: HttpClient;
private readonly logger: ILogger; private readonly logger: ILogger;
private readonly options: IHttpConnectionOptions; private readonly options: IHttpConnectionOptions;
private readonly transferFormat: TransferFormat;
private transport: ITransport; private transport: ITransport;
private connectionId: string; private connectionId: string;
private startPromise: Promise<void>; private startPromise: Promise<void>;
public readonly features: any = {}; 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.logger = LoggerFactory.createLogger(options.logger);
this.baseUrl = this.resolveUrl(url); this.baseUrl = this.resolveUrl(url);
@ -67,7 +79,7 @@ export class HttpConnection implements IConnection {
if (this.options.transport === TransportType.WebSockets) { if (this.options.transport === TransportType.WebSockets) {
// No need to add a connection ID in this case // No need to add a connection ID in this case
this.url = this.baseUrl; this.url = this.baseUrl;
this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]); this.transport = this.constructTransport(TransportType.WebSockets);
} else { } else {
let headers; let headers;
const token = this.options.accessTokenFactory(); const token = this.options.accessTokenFactory();
@ -91,50 +103,73 @@ export class HttpConnection implements IConnection {
if (this.connectionId) { if (this.connectionId) {
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${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.onreceive = this.onreceive;
this.transport.onclose = (e) => this.stopConnection(true, e); this.transport.onclose = (e) => this.stopConnection(true, e);
const requestedTransferMode = await this.transport.connect(this.url, this.transferFormat, this);
this.features.transferMode === TransferMode.Binary
? TransferMode.Binary
: TransferMode.Text;
this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode, this);
// only change the state if we were connecting to not overwrite // only change the state if we were connecting to not overwrite
// the state if the connection is already marked as Disconnected // the state if the connection is already marked as Disconnected
this.changeState(ConnectionState.Connecting, ConnectionState.Connected); this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
} catch (e) { } 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.connectionState = ConnectionState.Disconnected;
this.transport = null; this.transport = null;
throw e; throw e;
} }
} }
private createTransport(transport: TransportType | ITransport, availableTransports: string[]): ITransport { private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport {
if ((transport === null || transport === undefined) && availableTransports.length > 0) { if (this.isITransport(requestedTransport)) {
transport = TransportType[availableTransports[0]]; this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly.");
} return requestedTransport;
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);
} }
if (this.isITransport(transport)) { for (const endpoint of availableTransports) {
return transport; 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 { private isITransport(transport: any): transport is ITransport {
@ -151,7 +186,7 @@ export class HttpConnection implements IConnection {
public send(data: any): Promise<void> { public send(data: any): Promise<void> {
if (this.connectionState !== ConnectionState.Connected) { 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); return this.transport.send(data);
@ -210,7 +245,7 @@ export class HttpConnection implements IConnection {
} }
const normalizedUrl = baseUrl + url; const normalizedUrl = baseUrl + url;
this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'`); this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'.`);
return normalizedUrl; return normalizedUrl;
} }

View File

@ -1,17 +1,16 @@
// 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. // 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 { ConnectionClosed } from "./Common";
import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection"; import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection";
import { IConnection } from "./IConnection"; 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 { ILogger, LogLevel } from "./ILogger";
import { JsonHubProtocol } from "./JsonHubProtocol"; import { JsonHubProtocol } from "./JsonHubProtocol";
import { ConsoleLogger, LoggerFactory, NullLogger } from "./Loggers"; import { ConsoleLogger, LoggerFactory, NullLogger } from "./Loggers";
import { Observable, Subject } from "./Observable"; import { Observable, Subject } from "./Observable";
import { TextMessageFormat } from "./TextMessageFormat"; import { TextMessageFormat } from "./TextMessageFormat";
import { TransferMode, TransportType } from "./Transports"; import { TransferFormat, TransportType } from "./Transports";
export { JsonHubProtocol }; export { JsonHubProtocol };
@ -40,15 +39,16 @@ export class HubConnection {
this.timeoutInMilliseconds = options.timeoutInMilliseconds || DEFAULT_TIMEOUT_IN_MS; this.timeoutInMilliseconds = options.timeoutInMilliseconds || DEFAULT_TIMEOUT_IN_MS;
this.protocol = options.protocol || new JsonHubProtocol();
if (typeof urlOrConnection === "string") { if (typeof urlOrConnection === "string") {
this.connection = new HttpConnection(urlOrConnection, options); this.connection = new HttpConnection(urlOrConnection, this.protocol.transferFormat, options);
} else { } else {
this.connection = urlOrConnection; this.connection = urlOrConnection;
} }
this.logger = LoggerFactory.createLogger(options.logger); this.logger = LoggerFactory.createLogger(options.logger);
this.protocol = options.protocol || new JsonHubProtocol();
this.connection.onreceive = (data: any) => this.processIncomingData(data); this.connection.onreceive = (data: any) => this.processIncomingData(data);
this.connection.onclose = (error?: Error) => this.connectionClosed(error); this.connection.onclose = (error?: Error) => this.connectionClosed(error);
@ -133,14 +133,7 @@ export class HubConnection {
} }
public async start(): Promise<void> { 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(); await this.connection.start();
const actualTransferMode = this.connection.features.transferMode;
await this.connection.send( await this.connection.send(
TextMessageFormat.write( TextMessageFormat.write(
@ -148,10 +141,6 @@ export class HubConnection {
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`); 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(); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { ConnectionClosed, DataReceived } from "./Common"; import { ConnectionClosed, DataReceived } from "./Common";
import { ITransport, TransferMode, TransportType } from "./Transports"; import { ITransport, TransportType } from "./Transports";
export interface IConnection { export interface IConnection {
readonly features: any; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
export const enum MessageType { export const enum MessageType {
@ -58,14 +60,9 @@ export interface CancelInvocationMessage extends HubInvocationMessage {
readonly type: MessageType.CancelInvocation; readonly type: MessageType.CancelInvocation;
} }
export const enum ProtocolType {
Text = 1,
Binary,
}
export interface IHubProtocol { export interface IHubProtocol {
readonly name: string; readonly name: string;
readonly type: ProtocolType; readonly transferFormat: TransferFormat;
parseMessages(input: any): HubMessage[]; parseMessages(input: any): HubMessage[];
writeMessage(message: HubMessage): any; writeMessage(message: HubMessage): any;
} }

View File

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

View File

@ -7,6 +7,7 @@ import { HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest } from "./HttpClient"; import { HttpClient, HttpRequest } from "./HttpClient";
import { IConnection } from "./IConnection"; import { IConnection } from "./IConnection";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { Arg } from "./Utils";
export enum TransportType { export enum TransportType {
WebSockets, WebSockets,
@ -14,13 +15,13 @@ export enum TransportType {
LongPolling, LongPolling,
} }
export const enum TransferMode { export enum TransferFormat {
Text = 1, Text = 1,
Binary, Binary,
} }
export interface ITransport { 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>; send(data: any): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
onreceive: DataReceived; onreceive: DataReceived;
@ -37,9 +38,17 @@ export class WebSocketTransport implements ITransport {
this.accessTokenFactory = accessTokenFactory || (() => null); 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"); url = url.replace(/^http/, "ws");
const token = this.accessTokenFactory(); const token = this.accessTokenFactory();
if (token) { if (token) {
@ -47,18 +56,18 @@ export class WebSocketTransport implements ITransport {
} }
const webSocket = new WebSocket(url); const webSocket = new WebSocket(url);
if (requestedTransferMode === TransferMode.Binary) { if (transferFormat === TransferFormat.Binary) {
webSocket.binaryType = "arraybuffer"; webSocket.binaryType = "arraybuffer";
} }
webSocket.onopen = (event: Event) => { webSocket.onopen = (event: Event) => {
this.logger.log(LogLevel.Information, `WebSocket connected to ${url}`); this.logger.log(LogLevel.Information, `WebSocket connected to ${url}`);
this.webSocket = webSocket; this.webSocket = webSocket;
resolve(requestedTransferMode); resolve();
}; };
webSocket.onerror = (event: Event) => { webSocket.onerror = (event: ErrorEvent) => {
reject(); reject(event.error);
}; };
webSocket.onmessage = (message: MessageEvent) => { webSocket.onmessage = (message: MessageEvent) => {
@ -115,13 +124,22 @@ export class ServerSentEventsTransport implements ITransport {
this.logger = logger; 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") { 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; 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(); const token = this.accessTokenFactory();
if (token) { if (token) {
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`; url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
@ -145,7 +163,7 @@ export class ServerSentEventsTransport implements ITransport {
}; };
eventSource.onerror = (e: any) => { eventSource.onerror = (e: any) => {
reject(); reject(new Error(e.message || "Error occurred"));
// don't report an error if the transport did not start successfully // don't report an error if the transport did not start successfully
if (this.eventSource && this.onclose) { 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.logger.log(LogLevel.Information, `SSE connected to ${this.url}`);
this.eventSource = eventSource; this.eventSource = eventSource;
// SSE is a text protocol // SSE is a text protocol
resolve(TransferMode.Text); resolve();
}; };
} catch (e) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
@ -197,29 +215,34 @@ export class LongPollingTransport implements ITransport {
this.pollAbort = new AbortController(); 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; this.url = url;
// Set a flag indicating we have inherent keep-alive in this transport. // Set a flag indicating we have inherent keep-alive in this transport.
connection.features.inherentKeepAlive = true; 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 // 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."); throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
} }
this.poll(this.url, requestedTransferMode); this.poll(this.url, transferFormat);
return Promise.resolve(requestedTransferMode); return Promise.resolve();
} }
private async poll(url: string, transferMode: TransferMode): Promise<void> { private async poll(url: string, transferFormat: TransferFormat): Promise<void> {
const pollOptions: HttpRequest = { const pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal, abortSignal: this.pollAbort.signal,
headers: new Map<string, string>(), headers: new Map<string, string>(),
timeout: 90000, timeout: 90000,
}; };
if (transferMode === TransferMode.Binary) { if (transferFormat === TransferFormat.Binary) {
pollOptions.responseType = "arraybuffer"; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -8,6 +8,7 @@ using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -42,7 +43,7 @@ namespace ClientSample
var closeTcs = new TaskCompletionSource<object>(); var closeTcs = new TaskCompletionSource<object>();
connection.Closed += e => closeTcs.SetResult(null); connection.Closed += e => closeTcs.SetResult(null);
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}")); connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
await connection.StartAsync(); await connection.StartAsync(TransferFormat.Text);
Console.WriteLine($"Connected to {baseUrl}"); Console.WriteLine($"Connected to {baseUrl}");
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();

View File

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

View File

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

View File

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<!-- <!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
--> -->
<system.webServer> <system.webServer>
<handlers> <handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers> </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> </system.webServer>
</configuration> </configuration>

View File

@ -15,7 +15,8 @@
</select> </select>
<select id="transport"> <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="ServerSentEvents">ServerSentEvents</option>
<option value="LongPolling">LongPolling</option> <option value="LongPolling">LongPolling</option>
</select> </select>
@ -135,14 +136,19 @@
var connection; var connection;
click('connect', function (event) { click('connect', function (event) {
let transportType = signalR.TransportType[transportDropdown.value] || signalR.TransportType.WebSockets;
let hubRoute = hubTypeDropdown.value || "default"; let hubRoute = hubTypeDropdown.value || "default";
let protocol = protocolDropdown.value === "msgpack" ? let protocol = protocolDropdown.value === "msgpack" ?
new signalR.protocols.msgpack.MessagePackHubProtocol() : new signalR.protocols.msgpack.MessagePackHubProtocol() :
new signalR.JsonHubProtocol(); 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); 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) { connection.on('Send', function (msg) {
addLine('message-list', 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", "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) ## 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. 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. 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: 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. 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 (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. 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;

View File

@ -4,13 +4,11 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
@ -30,7 +28,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
private readonly IConnection _connection; private readonly IConnection _connection;
private readonly IHubProtocol _protocol; private readonly IHubProtocol _protocol;
private readonly HubBinder _binder; private readonly HubBinder _binder;
private HubProtocolReaderWriter _protocolReaderWriter;
private readonly object _pendingCallsLock = new object(); private readonly object _pendingCallsLock = new object();
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>(); private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
@ -113,26 +110,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
private async Task StartAsyncCore() private async Task StartAsyncCore()
{ {
var transferModeFeature = _connection.Features.Get<ITransferModeFeature>(); await _connection.StartAsync(_protocol.TransferFormat);
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();
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null; _needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
var actualTransferMode = transferModeFeature.TransferMode;
_protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode));
Log.HubProtocol(_logger, _protocol.Name); Log.HubProtocol(_logger, _protocol.Name);
_connectionActive = new CancellationTokenSource(); _connectionActive = new CancellationTokenSource();
@ -146,20 +126,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
ResetTimeoutTimer(); 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(); public async Task StopAsync() => await StopAsyncCore().ForceAsync();
private Task StopAsyncCore() => _connection.StopAsync(); private Task StopAsyncCore() => _connection.StopAsync();
@ -295,7 +261,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
{ {
try try
{ {
var payload = _protocolReaderWriter.WriteMessage(hubMessage); var payload = _protocol.WriteToArray(hubMessage);
Log.SendInvocation(_logger, hubMessage.InvocationId); Log.SendInvocation(_logger, hubMessage.InvocationId);
await _connection.SendAsync(payload, irq.CancellationToken); await _connection.SendAsync(payload, irq.CancellationToken);
@ -328,7 +294,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
{ {
Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length);
var payload = _protocolReaderWriter.WriteMessage(invocationMessage); var payload = _protocol.WriteToArray(invocationMessage);
Log.SendInvocation(_logger, invocationMessage.InvocationId); Log.SendInvocation(_logger, invocationMessage.InvocationId);
await _connection.SendAsync(payload, cancellationToken); await _connection.SendAsync(payload, cancellationToken);
@ -345,7 +311,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
{ {
ResetTimeoutTimer(); ResetTimeoutTimer();
Log.ParsingMessages(_logger, data.Length); 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); Log.ReceivingMessages(_logger, messages.Count);
foreach (var message in messages) foreach (var message in messages)
@ -621,10 +588,5 @@ namespace Microsoft.AspNetCore.SignalR.Client
return _callback(parameters, _state); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol 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 // Initialize with capacity 2 for the 2 built in protocols
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(4); 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 private readonly struct SerializedMessage
{ {
public readonly HubProtocolReaderWriter ProtocolReaderWriter; public readonly IHubProtocol Protocol;
public readonly byte[] Message; public readonly byte[] Message;
public SerializedMessage(HubProtocolReaderWriter protocolReaderWriter, byte[] message) public SerializedMessage(IHubProtocol protocol, byte[] message)
{ {
ProtocolReaderWriter = protocolReaderWriter; Protocol = protocol;
Message = message; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Sockets;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
string Name { get; } string Name { get; }
ProtocolType Type { get; } TransferFormat TransferFormat { get; }
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages); 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.Runtime.ExceptionServices;
using System.Text; using System.Text;
using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public string Name => ProtocolName; 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) 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)" /> <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -16,9 +16,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Core; using Microsoft.AspNetCore.SignalR.Core;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Features; using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -27,8 +25,6 @@ namespace Microsoft.AspNetCore.SignalR
public class HubConnectionContext public class HubConnectionContext
{ {
private static Action<object> _abortedCallback = AbortConnection; 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 ConnectionContext _connectionContext;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -62,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR
public string UserIdentifier { get; private set; } public string UserIdentifier { get; private set; }
internal virtual HubProtocolReaderWriter ProtocolReaderWriter { get; set; } internal virtual IHubProtocol Protocol { get; set; }
internal ExceptionDispatchInfo AbortException { get; private 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 // This will internally cache the buffer for each unique HubProtocol/DataEncoder combination
// So that we don't serialize the HubMessage for every single connection // 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); _connectionContext.Transport.Output.Write(buffer);
Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp()); 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)) 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 // If there's a transfer format feature, we need to check if we're compatible and set the active format.
?? throw new InvalidOperationException("Unable to read transport capabilities."); // 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) transferFormatFeature.ActiveFormat = Protocol.TransferFormat;
? (IDataEncoder)Base64Encoder }
: PassThroughEncoder;
var transferModeFeature = Features.Get<ITransferModeFeature>() ?? _cachedPingMessage = Protocol.WriteToArray(PingMessage.Instance);
throw new InvalidOperationException("Unable to read transfer mode.");
transferModeFeature.TransferMode = Log.UsingHubProtocol(_logger, Protocol.Name);
(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);
UserIdentifier = userIdProvider.GetUserId(this); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Core; using Microsoft.AspNetCore.SignalR.Core;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -141,7 +144,10 @@ namespace Microsoft.AspNetCore.SignalR
{ {
if (!buffer.IsEmpty) 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) foreach (var hubMessage in hubMessages)
{ {

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MsgPack; using MsgPack;
using MsgPack.Serialization; using MsgPack.Serialization;
@ -24,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public string Name => ProtocolName; public string Name => ProtocolName;
public ProtocolType Type => ProtocolType.Binary; public TransferFormat TransferFormat => TransferFormat.Binary;
public MessagePackHubProtocol() public MessagePackHubProtocol()
: this(Options.Create(new MessagePackHubProtocolOptions())) : 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO.Pipelines; using System.IO.Pipelines;
@ -8,7 +8,5 @@ namespace Microsoft.AspNetCore.Sockets.Features
public interface IConnectionTransportFeature public interface IConnectionTransportFeature
{ {
IDuplexPipe Transport { get; set; } 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 public interface IConnection
{ {
Task StartAsync(); Task StartAsync(TransferFormat transferFormat);
Task SendAsync(byte[] data, CancellationToken cancellationToken); Task SendAsync(byte[] data, CancellationToken cancellationToken);
Task StopAsync(); Task StopAsync();
Task DisposeAsync(); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -6,7 +6,7 @@ using System;
namespace Microsoft.AspNetCore.Sockets namespace Microsoft.AspNetCore.Sockets
{ {
[Flags] [Flags]
public enum TransferMode public enum TransferFormat
{ {
Binary = 0x01, Binary = 0x01,
Text = 0x02 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -85,6 +85,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
private static readonly Action<ILogger, string, string, Exception> _connectionStateChanged = 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}."); 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) public static void HttpConnectionStarting(ILogger logger)
{ {
_httpConnectionStarting(logger, null); _httpConnectionStarting(logger, null);
@ -218,6 +230,35 @@ namespace Microsoft.AspNetCore.Sockets.Client
{ {
_errorDuringClosedEvent(logger, exception); _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.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Sockets.Client.Http; using Microsoft.AspNetCore.Sockets.Client.Http;
using Microsoft.AspNetCore.Sockets.Client.Internal; using Microsoft.AspNetCore.Sockets.Client.Internal;
using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.AspNetCore.Sockets.Http.Internal; using Microsoft.AspNetCore.Sockets.Http.Internal;
using Microsoft.AspNetCore.Sockets.Internal; using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -47,9 +47,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
private PipeWriter Output => _transportChannel.Output; private PipeWriter Output => _transportChannel.Output;
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>(); private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
private readonly TransportType _requestedTransportType = TransportType.All; 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 ConnectionLogScope _logScope;
private readonly IDisposable _scopeDisposable; private readonly IDisposable _scopeDisposable;
@ -153,9 +150,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
_scopeDisposable = _logger.BeginScope(_logScope); _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) 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); _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_eventQueue = new TaskQueue(); _eventQueue = new TaskQueue();
StartAsyncInternal() StartAsyncInternal(transferFormat)
.ContinueWith(t => .ContinueWith(t =>
{ {
var abortException = _abortException; var abortException = _abortException;
@ -195,7 +193,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
return negotiationResponse; return negotiationResponse;
} }
private async Task StartAsyncInternal() private async Task StartAsyncInternal(TransferFormat transferFormat)
{ {
Log.HttpConnectionStarting(_logger); Log.HttpConnectionStarting(_logger);
@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
if (_requestedTransportType == TransportType.WebSockets) if (_requestedTransportType == TransportType.WebSockets)
{ {
Log.StartingTransport(_logger, _requestedTransportType, connectUrl); Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
await StartTransport(connectUrl, _requestedTransportType); await StartTransport(connectUrl, _requestedTransportType, transferFormat);
} }
else else
{ {
@ -219,14 +217,32 @@ namespace Microsoft.AspNetCore.Sockets.Client
} }
// This should only need to happen once // This should only need to happen once
_serverTransports = GetAvailableServerTransports(negotiationResponse);
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId); 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 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. // The negotiation response gets cleared in the fallback scenario.
if (negotiationResponse == null) if (negotiationResponse == null)
@ -235,13 +251,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId); connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
} }
Log.StartingTransport(_logger, transport, connectUrl); Log.StartingTransport(_logger, transportType, connectUrl);
await StartTransport(connectUrl, transport); await StartTransport(connectUrl, transportType, transferFormat);
break; break;
} }
} }
catch (Exception) catch (Exception ex)
{ {
Log.TransportFailed(_logger, transportType, ex);
// Try the next transport // Try the next transport
// Clear the negotiation response so we know to re-negotiate. // Clear the negotiation response so we know to re-negotiate.
negotiationResponse = null; negotiationResponse = null;
@ -382,22 +399,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
return negotiationResponse; 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) private static Uri CreateConnectUrl(Uri url, string connectionId)
{ {
if (string.IsNullOrWhiteSpace(connectionId)) if (string.IsNullOrWhiteSpace(connectionId))
@ -408,9 +409,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
return Utils.AppendQueryString(url, "id=" + connectionId); 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 options = new PipeOptions(writerScheduler: PipeScheduler.Inline, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
_transportChannel = pair.Transport; _transportChannel = pair.Transport;
@ -419,15 +419,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
// Start the transport, giving it one end of the pipeline // Start the transport, giving it one end of the pipeline
try try
{ {
await _transport.StartAsync(connectUrl, pair.Application, GetTransferMode(), this); await _transport.StartAsync(connectUrl, pair.Application, transferFormat, 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);
} }
catch (Exception ex) 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() private async Task ReceiveAsync()
{ {
try try
@ -753,7 +722,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
private class NegotiationResponse private class NegotiationResponse
{ {
public string ConnectionId { get; set; } 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 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(); Task StopAsync();
TransferMode? Mode { get; }
} }
} }

View File

@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{ {
private static class Log private static class Log
{ {
private static readonly Action<ILogger, TransferMode, Exception> _startTransport = private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped = private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); 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 // 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) 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 Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public LongPollingTransport(HttpClient httpClient) public LongPollingTransport(HttpClient httpClient)
: this(httpClient, null, null) : this(httpClient, null, null)
{ } { }
@ -40,19 +38,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>(); _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)); connection.Features.Set<IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(_httpClient.Timeout));
_application = application; _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) // Start sending and polling (ask for binary if the server supports it)
_poller = Poll(url, _transportCts.Token); _poller = Poll(url, _transportCts.Token);

View File

@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{ {
private static class Log private static class Log
{ {
private static readonly Action<ILogger, TransferMode, Exception> _startTransport = private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped = private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); 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 // 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) 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 Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public ServerSentEventsTransport(HttpClient httpClient) public ServerSentEventsTransport(HttpClient httpClient)
: this(httpClient, null, null) : this(httpClient, null, null)
{ } { }
@ -44,17 +42,16 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>(); _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; _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 sendTask = SendUtils.SendMessages(url, _application, _httpClient, _httpOptions, _transportCts, _logger);
var receiveTask = OpenConnection(_application, url, _transportCts.Token); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
{ {
private static class Log private static class Log
{ {
private static readonly Action<ILogger, TransferMode, Exception> _startTransport = private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
private static readonly Action<ILogger, Exception> _transportStopped = private static readonly Action<ILogger, Exception> _transportStopped =
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); 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 = private static readonly Action<ILogger, Exception> _cancelMessage =
LoggerMessage.Define(LogLevel.Debug, new EventId(18, "CancelMessage"), "Canceled passing message to application."); 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) public static void TransportStopped(ILogger logger, Exception exception)

View File

@ -19,14 +19,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
{ {
private readonly ClientWebSocket _webSocket; private readonly ClientWebSocket _webSocket;
private IDuplexPipe _application; private IDuplexPipe _application;
private WebSocketMessageType _webSocketMessageType;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly TimeSpan _closeTimeout; private readonly TimeSpan _closeTimeout;
private volatile bool _aborted; private volatile bool _aborted;
public Task Running { get; private set; } = Task.CompletedTask; public Task Running { get; private set; } = Task.CompletedTask;
public TransferMode? Mode { get; private set; }
public WebSocketsTransport() public WebSocketsTransport()
: this(null, null) : this(null, null)
{ {
@ -87,7 +86,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>(); _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) if (url == null)
{ {
@ -99,15 +98,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
throw new ArgumentNullException(nameof(application)); 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; _application = application;
Mode = requestedTransferMode; _webSocketMessageType = transferFormat == TransferFormat.Binary
? WebSocketMessageType.Binary
: WebSocketMessageType.Text;
Log.StartTransport(_logger, Mode.Value);
Log.StartTransport(_logger, transferFormat);
await Connect(url); await Connect(url);
@ -243,11 +245,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
private async Task StartSending(WebSocket socket) private async Task StartSending(WebSocket socket)
{ {
var webSocketMessageType =
Mode == TransferMode.Binary
? WebSocketMessageType.Binary
: WebSocketMessageType.Text;
Exception error = null; Exception error = null;
try try
@ -274,7 +271,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
if (WebSocketCanSend(socket)) if (WebSocketCanSend(socket))
{ {
await socket.SendAsync(buffer, webSocketMessageType); await socket.SendAsync(buffer, _webSocketMessageType);
} }
else else
{ {

View File

@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Sockets
Log.EstablishedConnection(_logger); Log.EstablishedConnection(_logger);
// ServerSentEvents is a text protocol only // 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. // 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); var sse = new ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory);
@ -389,15 +389,15 @@ namespace Microsoft.AspNetCore.Sockets
jsonWriter.WriteStartArray(); jsonWriter.WriteStartArray();
if ((options.Transports & TransportType.WebSockets) != 0) 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) if ((options.Transports & TransportType.ServerSentEvents) != 0)
{ {
jsonWriter.WriteValue(nameof(TransportType.ServerSentEvents)); WriteTransport(jsonWriter, nameof(TransportType.ServerSentEvents), TransferFormat.Text);
} }
if ((options.Transports & TransportType.LongPolling) != 0) if ((options.Transports & TransportType.LongPolling) != 0)
{ {
jsonWriter.WriteValue(nameof(TransportType.LongPolling)); WriteTransport(jsonWriter, nameof(TransportType.LongPolling), TransferFormat.Text | TransferFormat.Binary);
} }
jsonWriter.WriteEndArray(); jsonWriter.WriteEndArray();
jsonWriter.WriteEndObject(); jsonWriter.WriteEndObject();
@ -406,6 +406,27 @@ namespace Microsoft.AspNetCore.Sockets
return sb.ToString(); 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 static string GetConnectionId(HttpContext context) => context.Request.Query["id"];
private async Task ProcessSend(HttpContext context) private async Task ProcessSend(HttpContext context)
@ -477,9 +498,6 @@ namespace Microsoft.AspNetCore.Sockets
connection.User = context.User; connection.User = context.User;
connection.SetHttpContext(context); 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 // Set the Connection ID on the logging scope so that logs from now on will have the
// Connection ID metadata set. // Connection ID metadata set.
logScope.ConnectionId = connection.ConnectionId; logScope.ConnectionId = connection.ConnectionId;

View File

@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
{ {
Log.SendPayload(_logger, buffer.Length); Log.SendPayload(_logger, buffer.Length);
var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary var webSocketMessageType = (_connection.ActiveFormat == TransferFormat.Binary
? WebSocketMessageType.Binary ? WebSocketMessageType.Binary
: WebSocketMessageType.Text); : 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Security.Claims; using System.Security.Claims;
@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets
IConnectionTransportFeature, IConnectionTransportFeature,
IConnectionUserFeature, IConnectionUserFeature,
IConnectionHeartbeatFeature, IConnectionHeartbeatFeature,
ITransferModeFeature ITransferFormatFeature
{ {
private List<(Action<object> handler, object state)> _heartbeatHandlers = new List<(Action<object> handler, object state)>(); 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; ConnectionId = id;
LastSeenUtc = DateTime.UtcNow; 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 // PERF: This type could just implement IFeatureCollection
Features = new FeatureCollection(); Features = new FeatureCollection();
Features.Set<IConnectionUserFeature>(this); Features.Set<IConnectionUserFeature>(this);
Features.Set<IConnectionMetadataFeature>(this); Features.Set<IConnectionMetadataFeature>(this);
Features.Set<IConnectionIdFeature>(this); Features.Set<IConnectionIdFeature>(this);
Features.Set<IConnectionTransportFeature>(this); Features.Set<IConnectionTransportFeature>(this);
Features.Set<ITransferModeFeature>(this);
Features.Set<IConnectionHeartbeatFeature>(this); Features.Set<IConnectionHeartbeatFeature>(this);
Features.Set<ITransferFormatFeature>(this);
} }
public CancellationTokenSource Cancellation { get; set; } public CancellationTokenSource Cancellation { get; set; }
@ -71,9 +74,9 @@ namespace Microsoft.AspNetCore.Sockets
public override IDuplexPipe Transport { get; set; } 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) public void OnHeartbeat(Action<object> action, object state)
{ {

View File

@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{ {
await connection.StartAsync().OrTimeout(); 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); Assert.Equal("Hello World!", result);
} }
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{ {
await connection.StartAsync().OrTimeout(); 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); Assert.Equal(originalMessage, result);
} }
@ -115,11 +115,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try try
{ {
await connection.StartAsync().OrTimeout(); 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); Assert.Equal(originalMessage, result);
await connection.StopAsync().OrTimeout(); await connection.StopAsync().OrTimeout();
await connection.StartAsync().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); Assert.Equal(originalMessage, result);
} }
catch (Exception ex) catch (Exception ex)
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try try
{ {
await connection.StartAsync().OrTimeout(); 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); Assert.Equal(originalMessage, result);
logger.LogInformation("Stopping connection"); logger.LogInformation("Stopping connection");
@ -169,7 +169,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
await restartTcs.Task.OrTimeout(); await restartTcs.Task.OrTimeout();
logger.LogInformation("Reconnection complete"); 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); Assert.Equal(originalMessage, result);
} }
@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{ {
await connection.StartAsync().OrTimeout(); 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); Assert.Equal(originalMessage, result);
} }
@ -677,7 +677,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try try
{ {
await hubConnection.StartAsync().OrTimeout(); 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); Assert.Equal("Hello, World!", message);
} }
catch (Exception ex) catch (Exception ex)
@ -708,7 +708,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try try
{ {
await hubConnection.StartAsync().OrTimeout(); 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); Assert.Equal(new[] { "42", "test" }, headerValues);
} }
catch (Exception ex) catch (Exception ex)
@ -742,7 +742,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
try try
{ {
await hubConnection.StartAsync().OrTimeout(); 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); Assert.Equal("Bar", cookieValue);
} }
catch (Exception ex) catch (Exception ex)
@ -772,7 +772,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{ {
await hubConnection.StartAsync().OrTimeout(); 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 localPort = (Int64)features[0];
var remotePort = (Int64)features[1]; var remotePort = (Int64)features[1];
var localIP = (string)features[2]; 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 public static IEnumerable<object[]> HubProtocolsAndTransportsAndHubPaths
{ {
get get
{ {
foreach (var protocol in HubProtocols) 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) 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 string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" };
public static IEnumerable<IHubProtocol> HubProtocols => public static IEnumerable<IHubProtocol> HubProtocols =>

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests; using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Xunit; using Xunit;
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
var exception = var exception =
await Assert.ThrowsAsync<InvalidOperationException>( 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); 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), CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync(); await connection.DisposeAsync();
var exception = var exception =
await Assert.ThrowsAsync<InvalidOperationException>( 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); 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(); await connection.DisposeAsync();
var exception = var exception =
await Assert.ThrowsAsync<InvalidOperationException>( 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); 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) => async (connection, closed) =>
{ {
// Start the connection and wait for the transport to start up. // 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(); await transportStart.WaitForSyncPoint().OrTimeout();
// While the transport is starting, dispose the connection // While the transport is starting, dispose the connection
@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) => async (connection, closed) =>
{ {
Assert.Equal(0, startCounter); Assert.Equal(0, startCounter);
await connection.StartAsync(); await connection.StartAsync(TransferFormat.Text);
Assert.Equal(passThreshold, startCounter); Assert.Equal(passThreshold, startCounter);
}); });
} }
@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
transport: new TestTransport(onTransportStart: OnTransportStart)), transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection, closed) => 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("Unable to connect to the server with any of the available transports.", ex.Message);
Assert.Equal(3, startCounter); Assert.Equal(3, startCounter);
}); });
@ -180,9 +181,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory), CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StopAsync().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) => async (connection, closed) =>
{ {
// Start and wait for the transport to start up. // Start and wait for the transport to start up.
var startTask = connection.StartAsync(); var startTask = connection.StartAsync(TransferFormat.Text);
await transportStart.WaitForSyncPoint().OrTimeout(); await transportStart.WaitForSyncPoint().OrTimeout();
// Stop the connection while it's starting // Stop the connection while it's starting
@ -223,7 +224,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(loggerFactory: loggerFactory), CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout(); await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout();
await closed.OrTimeout(); await closed.OrTimeout();
}); });
@ -257,14 +258,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(httpHandler, loggerFactory), CreateConnection(httpHandler, loggerFactory),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.SendAsync(new byte[] { 0x42 }).OrTimeout(); await connection.SendAsync(new byte[] { 0x42 }).OrTimeout();
// Wait for the connection to close, because the send failed. // Wait for the connection to close, because the send failed.
await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout()); await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout());
// Start it up again // 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) => async (connection, closed) =>
{ {
// Start the connection // Start the connection
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Stop the connection // Stop the connection
var stopTask = connection.StopAsync().OrTimeout(); var stopTask = connection.StopAsync().OrTimeout();
@ -299,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await disposeTask.OrTimeout(); await disposeTask.OrTimeout();
// We should be disposed and thus unable to restart. // 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); 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), CreateConnection(loggerFactory: loggerFactory),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StopAsync().OrTimeout(); await connection.StopAsync().OrTimeout();
await closed.OrTimeout(); await closed.OrTimeout();
await connection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout();
@ -329,7 +330,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(), CreateConnection(),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout();
await closed.OrTimeout(); await closed.OrTimeout();
}); });
@ -346,7 +347,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(transport: testTransport), CreateConnection(transport: testTransport),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
testTransport.Application.Output.Complete(expected); testTransport.Application.Output.Complete(expected);
var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout()); var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout());
Assert.Same(expected, actual); Assert.Same(expected, actual);
@ -384,7 +385,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) => async (connection, closed) =>
{ {
// Start the transport // 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"); Assert.False(longPollingTransport.Running.IsCompleted, "Expected that the transport would still be running");
// Stop the connection, and we should stop the transport // 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 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() var httpOptions = new HttpOptions()
{ {
HttpMessageHandler = (httpMessageHandler) => httpHandler ?? TestHttpMessageHandler.CreateDefault(), 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; loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
var uri = new Uri(url ?? "http://fakeuri.org/"); var uri = new Uri(url ?? "http://fakeuri.org/");
var connection = (transport != null) ? if (transportFactory != null)
new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions) : {
new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions); return new HttpConnection(uri, transportFactory, loggerFactory, httpOptions);
}
return connection; 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) 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.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Client.Tests; using Microsoft.AspNetCore.Client.Tests;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client;
using Moq;
using Newtonsoft.Json;
using Xunit; using Xunit;
using TransportType = Microsoft.AspNetCore.Sockets.TransportType; using TransportType = Microsoft.AspNetCore.Sockets.TransportType;
@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
[Fact] [Fact]
public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports() 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] [Theory]
@ -67,12 +71,104 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(testHttpHandler, url: requestedUrl), CreateConnection(testHttpHandler, url: requestedUrl),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
}); });
Assert.Equal(expectedNegotiate, await negotiateUrlTcs.Task.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 private async Task RunInvalidNegotiateResponseTest<TException>(string negotiatePayload, string expectedExceptionMessage) where TException : Exception
{ {
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
@ -84,7 +180,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
async (connection, closed) => async (connection, closed) =>
{ {
var exception = await Assert.ThrowsAsync<TException>( var exception = await Assert.ThrowsAsync<TException>(
() => connection.StartAsync().OrTimeout()); () => connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal(expectedExceptionMessage, exception.Message); Assert.Equal(expectedExceptionMessage, exception.Message);
}); });

View File

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

View File

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

View File

@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var onReceived = new SyncPoint(); var onReceived = new SyncPoint();
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout()); connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
// This will trigger the received callback // This will trigger the received callback
await testTransport.Application.Output.WriteAsync(new byte[] { 1 }); await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout()); connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
logger.LogInformation("Starting connection"); logger.LogInformation("Starting connection");
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
logger.LogInformation("Started connection"); logger.LogInformation("Started connection");
await testTransport.Application.Output.WriteAsync(new byte[] { 1 }); 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] [Fact]
public async Task HttpOptionsSetOntoHttpClientHandler() public async Task HttpOptionsSetOntoHttpClientHandler()
{ {
@ -180,7 +163,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
CreateConnection(httpOptions, url: "http://fakeuri.org/"), CreateConnection(httpOptions, url: "http://fakeuri.org/"),
async (connection, closed) => async (connection, closed) =>
{ {
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
}); });
Assert.NotNull(httpClientHandler); 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] [Fact]
public async Task AcceptsPingMessages() public async Task AcceptsPingMessages()
{ {
var connection = new TestConnection(TransferMode.Text); var connection = new TestConnection();
var hubConnection = new HubConnection(connection, var hubConnection = new HubConnection(connection,
new JsonHubProtocol(), new LoggerFactory()); new JsonHubProtocol(), new LoggerFactory());

View File

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

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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; transportActiveTask = longPollingTransport.Running;
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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(); await longPollingTransport.Running.OrTimeout();
@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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(); var data = await pair.Transport.Input.ReadAllAsync().OrTimeout();
await longPollingTransport.Running.OrTimeout(); await longPollingTransport.Running.OrTimeout();
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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 = var exception =
await Assert.ThrowsAsync<HttpRequestException>(async () => await Assert.ThrowsAsync<HttpRequestException>(async () =>
@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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")); await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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(); pair.Transport.Output.Complete();
@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
// Start the transport // 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 // Wait for the transport to finish
await longPollingTransport.Running.OrTimeout(); await longPollingTransport.Running.OrTimeout();
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Client.Tests
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("World")); await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("World"));
// Start the transport // 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(); pair.Transport.Output.Complete();
@ -360,9 +360,9 @@ namespace Microsoft.AspNetCore.Client.Tests
} }
[Theory] [Theory]
[InlineData(TransferMode.Binary)] [InlineData(TransferFormat.Binary)]
[InlineData(TransferMode.Text)] [InlineData(TransferFormat.Text)]
public async Task LongPollingTransportSetsTransferMode(TransferMode transferMode) public async Task LongPollingTransportSetsTransferFormat(TransferFormat transferFormat)
{ {
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -381,9 +381,7 @@ namespace Microsoft.AspNetCore.Client.Tests
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Assert.Null(longPollingTransport.Mode); await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: new TestConnection());
Assert.Equal(transferMode, longPollingTransport.Mode);
} }
finally finally
{ {
@ -415,8 +413,7 @@ namespace Microsoft.AspNetCore.Client.Tests
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Assert.Null(longPollingTransport.Mode); await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: new TestConnection());
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: new TestConnection());
} }
finally finally
{ {
@ -436,8 +433,10 @@ namespace Microsoft.AspNetCore.Client.Tests
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
} }
[Fact] [Theory]
public async Task LongPollingTransportThrowsForInvalidTransferMode() [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>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -452,10 +451,10 @@ namespace Microsoft.AspNetCore.Client.Tests
{ {
var longPollingTransport = new LongPollingTransport(httpClient); var longPollingTransport = new LongPollingTransport(httpClient);
var exception = await Assert.ThrowsAsync<ArgumentException>(() => 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.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName); Assert.Equal("transferFormat", exception.ParamName);
} }
} }
@ -488,7 +487,7 @@ namespace Microsoft.AspNetCore.Client.Tests
try try
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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(); var completedTask = await Task.WhenAny(completionTcs.Task, longPollingTransport.Running).OrTimeout();
Assert.Equal(completionTcs.Task, completedTask); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using Microsoft.AspNetCore.Sockets;
using Newtonsoft.Json;
using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType; using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType;
namespace Microsoft.AspNetCore.Client.Tests 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", public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
SocketsTransportType? transportTypes = SocketsTransportType.All) SocketsTransportType transportTypes = SocketsTransportType.All)
{ {
var sb = new StringBuilder("{ "); var availableTransports = new List<object>();
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("}");
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 sseTransport = new ServerSentEventsTransport(httpClient);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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 eventStreamTcs.Task.OrTimeout();
await sseTransport.StopAsync().OrTimeout(); await sseTransport.StopAsync().OrTimeout();
@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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; transportActiveTask = sseTransport.Running;
Assert.False(transportActiveTask.IsCompleted); Assert.False(transportActiveTask.IsCompleted);
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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()); var exception = await Assert.ThrowsAsync<FormatException>(() => sseTransport.Running.OrTimeout());
Assert.Equal("Incomplete message.", exception.Message); Assert.Equal("Incomplete message.", exception.Message);
@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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 eventStreamTcs.Task;
await pair.Transport.Output.WriteAsync(new byte[] { 0x42 }); 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); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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 eventStreamTcs.Task.OrTimeout();
pair.Transport.Output.Complete(); pair.Transport.Output.Complete();
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync( 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(); var message = await pair.Transport.Input.ReadSingleAsync().OrTimeout();
Assert.Equal("3:abc", Encoding.ASCII.GetString(message)); Assert.Equal("3:abc", Encoding.ASCII.GetString(message));
@ -276,10 +276,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
} }
[Theory] [Fact]
[InlineData(TransferMode.Text)] public async Task SSETransportDoesNotSupportBinary()
[InlineData(TransferMode.Binary)]
public async Task SSETransportSetsTransferMode(TransferMode transferMode)
{ {
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -293,12 +291,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
using (var httpClient = new HttpClient(mockHttpHandler.Object)) using (var httpClient = new HttpClient(mockHttpHandler.Object))
{ {
var sseTransport = new ServerSentEventsTransport(httpClient); var sseTransport = new ServerSentEventsTransport(httpClient);
Assert.Null(sseTransport.Mode);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: Mock.Of<IConnection>()).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(TransferMode.Text, sseTransport.Mode); Assert.Equal($"The 'Binary' transfer format is not supported by this transport.{Environment.NewLine}Parameter name: transferFormat", ex.Message);
await sseTransport.StopAsync().OrTimeout();
} }
} }
@ -322,7 +318,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var sseTransport = new ServerSentEventsTransport(httpClient); var sseTransport = new ServerSentEventsTransport(httpClient);
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); 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(); await sseTransport.StopAsync().OrTimeout();
} }
@ -338,8 +334,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
} }
[Fact] [Theory]
public async Task SSETransportThrowsForInvalidTransferMode() [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>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -354,10 +353,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
var sseTransport = new ServerSentEventsTransport(httpClient); var sseTransport = new ServerSentEventsTransport(httpClient);
var exception = await Assert.ThrowsAsync<ArgumentException>(() => 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.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName); Assert.Equal("transferFormat", exception.ParamName);
} }
} }
} }

View File

@ -29,8 +29,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource(); private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource();
private Task _receiveLoop; private Task _receiveLoop;
private TransferMode? _transferMode;
public event Action<Exception> Closed; public event Action<Exception> Closed;
public Task Started => _started.Task; public Task Started => _started.Task;
public Task Disposed => _disposed.Task; public Task Disposed => _disposed.Task;
@ -44,9 +42,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public IFeatureCollection Features { get; } = new FeatureCollection(); public IFeatureCollection Features { get; } = new FeatureCollection();
public TestConnection(TransferMode? transferMode = null) public TestConnection()
{ {
_transferMode = transferMode;
_receiveLoop = ReceiveLoopAsync(_receiveShutdownToken.Token); _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"); 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); _started.TrySetResult(null);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
@ -12,18 +11,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
private readonly Func<Task> _stopHandler; private readonly Func<Task> _stopHandler;
private readonly Func<Task> _startHandler; private readonly Func<Task> _startHandler;
public TransferMode? Mode { get; } public TransferFormat? Format { get; }
public IDuplexPipe Application { get; private set; } 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); _stopHandler = onTransportStop ?? new Func<Task>(() => Task.CompletedTask);
_startHandler = onTransportStart ?? 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; Application = application;
return _startHandler(); 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()) foreach (var transport in TransportTypes())
{ {
yield return new object[] { transport, new JsonHubProtocol() }; 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 System;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) 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) public static Mock<HubConnectionContext> CreateMock(DefaultConnectionContext connection)
{ {
var mock = new Mock<HubConnectionContext>(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { CallBase = true }; var mock = new Mock<HubConnectionContext>(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { CallBase = true };
var readerWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder()); var protocol = new JsonHubProtocol();
mock.SetupGet(m => m.ProtocolReaderWriter).Returns(readerWriter); mock.SetupGet(m => m.Protocol).Returns(protocol);
return mock; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
@ -9,7 +10,6 @@ using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public class TestClient : IDisposable public class TestClient : IDisposable
{ {
private static int _id; private static int _id;
private readonly HubProtocolReaderWriter _protocolReaderWriter; private readonly IHubProtocol _protocol;
private readonly IInvocationBinder _invocationBinder; private readonly IInvocationBinder _invocationBinder;
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
private Queue<HubMessage> _messages = new Queue<HubMessage>(); private Queue<HubMessage> _messages = new Queue<HubMessage>();
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public DefaultConnectionContext Connection { get; } public DefaultConnectionContext Connection { get; }
public Task Connected => ((TaskCompletionSource<bool>)Connection.Metadata["ConnectedTask"]).Task; 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 options = new PipeOptions(readerScheduler: synchronousCallbacks ? PipeScheduler.Inline : null);
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
@ -42,16 +42,14 @@ namespace Microsoft.AspNetCore.SignalR.Tests
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims));
Connection.Metadata["ConnectedTask"] = new TaskCompletionSource<bool>(); Connection.Metadata["ConnectedTask"] = new TaskCompletionSource<bool>();
protocol = protocol ?? new JsonHubProtocol(); _protocol = protocol ?? new JsonHubProtocol();
dataEncoder = dataEncoder ?? new PassThroughEncoder();
_protocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder);
_invocationBinder = invocationBinder ?? new DefaultInvocationBinder(); _invocationBinder = invocationBinder ?? new DefaultInvocationBinder();
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
using (var memoryStream = new MemoryStream()) 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()); Connection.Application.Output.WriteAsync(memoryStream.ToArray());
} }
} }
@ -143,7 +141,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public async Task<string> SendHubMessageAsync(HubMessage message) public async Task<string> SendHubMessageAsync(HubMessage message)
{ {
var payload = _protocolReaderWriter.WriteMessage(message); var payload = _protocol.WriteToArray(message);
await Connection.Application.Output.WriteAsync(payload); await Connection.Application.Output.WriteAsync(payload);
return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null; return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null;
} }
@ -201,7 +200,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
try 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) 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. // 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. // On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
var connection = new HttpConnection(new Uri(url)); var connection = new HttpConnection(new Uri(url));
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Binary).OrTimeout();
await connection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout();
} }
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
// The test logic lives in the TestTransportFactory and FakeTransport. // The test logic lives in the TestTransportFactory and FakeTransport.
var connection = new HttpConnection(new Uri(url), new TestTransportFactory(), null, null); 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(); await connection.DisposeAsync().OrTimeout();
} }
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
var url = _serverFixture.Url + "/echo"; var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), transportType); var connection = new HttpConnection(new Uri(url), transportType);
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout();
} }
@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}, receiveTcs); }, receiveTcs);
var message = new byte[] { 42 }; var message = new byte[] { 42 };
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Binary).OrTimeout();
await connection.SendAsync(message).OrTimeout(); await connection.SendAsync(message).OrTimeout();
var receivedData = await receiveTcs.Task.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")] [Theory(Skip = "https://github.com/aspnet/SignalR/issues/1485")]
[MemberData(nameof(TransportTypesAndTransferModes))] [MemberData(nameof(TransportTypesAndTransferFormats))]
public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferMode requestedTransferMode) public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferFormat requestedTransferFormat)
{ {
using (StartLog(out var loggerFactory, testName: $"ConnectionCanSendAndReceiveMessages_{transportType.ToString()}")) using (StartLog(out var loggerFactory, testName: $"ConnectionCanSendAndReceiveMessages_{transportType.ToString()}"))
{ {
@ -177,9 +177,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var url = _serverFixture.Url + "/echo"; var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), transportType, loggerFactory); var connection = new HttpConnection(new Uri(url), transportType, loggerFactory);
connection.Features.Set<ITransferModeFeature>(
new TransferModeFeature { TransferMode = requestedTransferMode });
try try
{ {
var closeTcs = new TaskCompletionSource<object>(); var closeTcs = new TaskCompletionSource<object>();
@ -199,28 +196,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests
connection.OnReceived((data, state) => connection.OnReceived((data, state) =>
{ {
logger.LogInformation("Received {length} byte message", data.Length); logger.LogInformation("Received {length} byte message", data.Length);
if (IsBase64Encoded(requestedTransferMode, connection))
{
data = Convert.FromBase64String(Encoding.UTF8.GetString(data));
}
var tcs = (TaskCompletionSource<string>)state; var tcs = (TaskCompletionSource<string>)state;
tcs.TrySetResult(Encoding.UTF8.GetString(data)); tcs.TrySetResult(Encoding.UTF8.GetString(data));
return Task.CompletedTask; return Task.CompletedTask;
}, receiveTcs); }, receiveTcs);
logger.LogInformation("Starting connection to {url}", url); logger.LogInformation("Starting connection to {url}", url);
await connection.StartAsync().OrTimeout(); await connection.StartAsync(requestedTransferFormat).OrTimeout();
logger.LogInformation("Started connection to {url}", url); logger.LogInformation("Started connection to {url}", url);
var bytes = Encoding.UTF8.GetBytes(message); 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); logger.LogInformation("Sending {length} byte message", bytes.Length);
try 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 public static IEnumerable<object[]> MessageSizesData
{ {
get get
@ -281,8 +261,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var url = _serverFixture.Url + "/echo"; var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), TransportType.WebSockets, loggerFactory); var connection = new HttpConnection(new Uri(url), TransportType.WebSockets, loggerFactory);
connection.Features.Set<ITransferModeFeature>(
new TransferModeFeature { TransferMode = TransferMode.Binary });
try try
{ {
@ -296,7 +274,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}, receiveTcs); }, receiveTcs);
logger.LogInformation("Starting connection to {url}", url); logger.LogInformation("Starting connection to {url}", url);
await connection.StartAsync().OrTimeout(); await connection.StartAsync(TransferFormat.Binary).OrTimeout();
logger.LogInformation("Started connection to {url}", url); logger.LogInformation("Started connection to {url}", url);
var bytes = Encoding.UTF8.GetBytes(message); var bytes = Encoding.UTF8.GetBytes(message);
@ -416,16 +394,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
private class FakeTransport : ITransport private class FakeTransport : ITransport
{ {
public TransferMode? Mode => TransferMode.Text;
public string prevConnectionId = null; public string prevConnectionId = null;
private int tries = 0; private int tries = 0;
private IDuplexPipe _application; 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; _application = application;
tries++; 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) if (prevConnectionId == null)
{ {
prevConnectionId = id; prevConnectionId = id;
@ -467,14 +444,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests
} }
} }
public static IEnumerable<object[]> TransportTypesAndTransferModes public static IEnumerable<object[]> TransportTypesAndTransferFormats
{ {
get get
{ {
foreach (var transport in TransportTypes) foreach (var transport in TransportTypes)
{ {
yield return new object[] { transport[0], TransferMode.Text }; yield return new object[] { transport[0], TransferFormat.Text };
yield return new object[] { transport[0], TransferMode.Binary };
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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils; using Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils;
using Microsoft.AspNetCore.Sockets; 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)) using (var client = new TestClient(synchronousCallbacks: false, protocol: protocol, invocationBinder: invocationBinder.Object))
{ {
var transportFeature = new Mock<IConnectionTransportFeature>(); client.Connection.SupportedFormats = protocol.TransferFormat;
transportFeature.SetupGet(f => f.TransportCapabilities)
.Returns(protocol.Type == ProtocolType.Binary ? TransferMode.Binary : TransferMode.Text);
client.Connection.Features.Set(transportFeature.Object);
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection); var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
@ -1419,7 +1415,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>(); var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
using (var client1 = new TestClient(protocol: new JsonHubProtocol())) 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 endPointLifetime1 = endPoint.OnConnectedAsync(client1.Connection);
var endPointLifetime2 = endPoint.OnConnectedAsync(client2.Connection); var endPointLifetime2 = endPoint.OnConnectedAsync(client2.Connection);
@ -1617,9 +1613,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var msgPackOptions = serviceProvider.GetRequiredService<IOptions<MessagePackHubProtocolOptions>>(); var msgPackOptions = serviceProvider.GetRequiredService<IOptions<MessagePackHubProtocolOptions>>();
using (var client = new TestClient(synchronousCallbacks: false, protocol: new MessagePackHubProtocol(msgPackOptions))) using (var client = new TestClient(synchronousCallbacks: false, protocol: new MessagePackHubProtocol(msgPackOptions)))
{ {
var transportFeature = new Mock<IConnectionTransportFeature>(); client.Connection.SupportedFormats = TransferFormat.Binary;
transportFeature.SetupGet(f => f.TransportCapabilities).Returns(TransferMode.Binary);
client.Connection.Features.Set(transportFeature.Object);
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection); var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
await client.Connected.OrTimeout(); 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() public static IEnumerable<object[]> HubTypes()
{ {
yield return new[] { typeof(DynamicTestHub) }; 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 pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, 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.StopAsync().OrTimeout();
await webSocketsTransport.Running.OrTimeout(); await webSocketsTransport.Running.OrTimeout();
} }
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application, 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")); 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 pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, 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(); pair.Transport.Output.Complete();
await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10)); await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10));
} }
@ -128,15 +128,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[ConditionalTheory] [ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
[InlineData(TransferMode.Text)] [InlineData(TransferFormat.Text)]
[InlineData(TransferMode.Binary)] [InlineData(TransferFormat.Binary)]
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferMode transferMode) public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat)
{ {
using (StartLog(out var loggerFactory)) using (StartLog(out var loggerFactory))
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); 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 }); await pair.Transport.Output.WriteAsync(new byte[] { 0x42 });
@ -151,38 +151,38 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[ConditionalTheory] [ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
[InlineData(TransferMode.Text)] [InlineData(TransferFormat.Text)]
[InlineData(TransferMode.Binary)] [InlineData(TransferFormat.Binary)]
public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode) public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat)
{ {
using (StartLog(out var loggerFactory)) using (StartLog(out var loggerFactory))
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
Assert.Null(webSocketsTransport.Mode);
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
transferMode, connection: Mock.Of<IConnection>()).OrTimeout(); transferFormat, connection: Mock.Of<IConnection>()).OrTimeout();
Assert.Equal(transferMode, webSocketsTransport.Mode);
await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.StopAsync().OrTimeout();
await webSocketsTransport.Running.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")] [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)) using (StartLog(out var loggerFactory))
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
var exception = await Assert.ThrowsAsync<ArgumentException>(() => 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.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
Assert.Equal("requestedTransferMode", exception.ParamName); Assert.Equal("transferFormat", exception.ParamName);
} }
} }
} }

View File

@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[InlineData(TransportType.All)] [InlineData(TransportType.All)]
[InlineData((TransportType)0)] [InlineData((TransportType)0)]
[InlineData(TransportType.LongPolling | TransportType.WebSockets)] [InlineData(TransportType.LongPolling | TransportType.WebSockets)]
public async Task NegotiateReturnsAvailableTransports(TransportType transports) public async Task NegotiateReturnsAvailableTransportsAfterFilteringByOptions(TransportType transports)
{ {
using (StartLog(out var loggerFactory, LogLevel.Debug)) using (StartLog(out var loggerFactory, LogLevel.Debug))
{ {
@ -167,7 +167,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
var availableTransports = (TransportType)0; var availableTransports = (TransportType)0;
foreach (var transport in negotiateResponse["availableTransports"]) 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); Assert.Equal(transports, availableTransports);
@ -820,10 +821,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests
} }
[Theory] [Theory]
[InlineData(TransportType.LongPolling, TransferMode.Binary | TransferMode.Text)] [InlineData(TransportType.LongPolling, null)]
[InlineData(TransportType.ServerSentEvents, TransferMode.Text)] [InlineData(TransportType.ServerSentEvents, TransferFormat.Text)]
[InlineData(TransportType.WebSockets, TransferMode.Binary | TransferMode.Text)] [InlineData(TransportType.WebSockets, TransferFormat.Binary | TransferFormat.Text)]
public async Task TransportCapabilitiesSet(TransportType transportType, TransferMode expectedTransportCapabilities) public async Task TransferModeSet(TransportType transportType, TransferFormat? expectedTransferFormats)
{ {
using (StartLog(out var loggerFactory, LogLevel.Debug)) using (StartLog(out var loggerFactory, LogLevel.Debug))
{ {
@ -845,7 +846,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0); options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0);
await dispatcher.ExecuteAsync(context, options, app); 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.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.AspNetCore.Sockets.Internal.Transports; using Microsoft.AspNetCore.Sockets.Internal.Transports;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
@ -71,9 +73,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
} }
[Theory] [Theory]
[InlineData(TransferMode.Text, WebSocketMessageType.Text)] [InlineData(TransferFormat.Text, WebSocketMessageType.Text)]
[InlineData(TransferMode.Binary, WebSocketMessageType.Binary)] [InlineData(TransferFormat.Binary, WebSocketMessageType.Binary)]
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferModeFeature(TransferMode transferMode, WebSocketMessageType expectedMessageType) public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, WebSocketMessageType expectedMessageType)
{ {
using (StartLog(out var loggerFactory, LogLevel.Debug)) using (StartLog(out var loggerFactory, LogLevel.Debug))
{ {
@ -82,7 +84,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
using (var feature = new TestWebSocketConnectionFeature()) 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); var ws = new WebSocketsTransport(new WebSocketOptions(), connection.Application, connectionContext, loggerFactory);
// Give the server socket to the transport and run it // Give the server socket to the transport and run it