Remove support for binary over SSE and add transfer format to negotiation (#1564)
This commit is contained in:
parent
adb760210d
commit
fb6121399c
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"lib": [ "es2015", "dom" ],
|
"lib": [ "es2015", "dom" ],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@aspnet/signalr": [ "../../signalr/src/index" ]
|
"@aspnet/*": [ "../../*" ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,4 @@ export class PromiseSource<T> {
|
||||||
public reject(reason?: any) {
|
public reject(reason?: any) {
|
||||||
this.rejecter(reason);
|
this.rejecter(reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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};`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 =>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) };
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue