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 BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
|
@ -40,14 +39,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
protocol = new MessagePackHubProtocol();
|
||||
}
|
||||
|
||||
var encoder = new PassThroughEncoder();
|
||||
|
||||
for (var i = 0; i < Connections; ++i)
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport);
|
||||
var hubConnection = new HubConnectionContext(connection, Timeout.InfiniteTimeSpan, NullLoggerFactory.Instance);
|
||||
hubConnection.ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, encoder);
|
||||
hubConnection.Protocol = protocol;
|
||||
_hubLifetimeManager.OnConnectedAsync(hubConnection).Wait();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -11,12 +11,11 @@ using System.Threading.Tasks;
|
|||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Protocols;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using DefaultConnectionContext = Microsoft.AspNetCore.Sockets.DefaultConnectionContext;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||
|
|
@ -47,13 +46,13 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
|
||||
_connectionContext = new NoErrorHubConnectionContext(connection, TimeSpan.Zero, NullLoggerFactory.Instance);
|
||||
|
||||
_connectionContext.ProtocolReaderWriter = new HubProtocolReaderWriter(new FakeHubProtocol(), new FakeDataEncoder());
|
||||
_connectionContext.Protocol = new FakeHubProtocol();
|
||||
}
|
||||
|
||||
public class FakeHubProtocol : IHubProtocol
|
||||
{
|
||||
public string Name { get; }
|
||||
public ProtocolType Type { get; }
|
||||
public TransferFormat TransferFormat { get; }
|
||||
|
||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
|
|
@ -65,19 +64,6 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
}
|
||||
}
|
||||
|
||||
public class FakeDataEncoder : IDataEncoder
|
||||
{
|
||||
public byte[] Encode(byte[] payload)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryDecode(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class NoErrorHubConnectionContext : HubConnectionContext
|
||||
{
|
||||
public NoErrorHubConnectionContext(ConnectionContext connectionContext, TimeSpan keepAliveInterval, ILoggerFactory loggerFactory) : base(connectionContext, keepAliveInterval, loggerFactory)
|
||||
|
|
@ -232,4 +218,4 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
return _dispatcher.DispatchMessageAsync(_connectionContext, new StreamInvocationMessage("123", "StreamChannelReaderValueTaskAsync", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||
{
|
||||
public class HubProtocolBenchmark
|
||||
{
|
||||
private HubProtocolReaderWriter _hubProtocolReaderWriter;
|
||||
private IHubProtocol _hubProtocol;
|
||||
private byte[] _binaryInput;
|
||||
private TestBinder _binder;
|
||||
private HubMessage _hubMessage;
|
||||
|
|
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
switch (HubProtocol)
|
||||
{
|
||||
case Protocol.MsgPack:
|
||||
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new MessagePackHubProtocol(), new PassThroughEncoder());
|
||||
_hubProtocol = new MessagePackHubProtocol();
|
||||
break;
|
||||
case Protocol.Json:
|
||||
_hubProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder());
|
||||
_hubProtocol = new JsonHubProtocol();
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -51,14 +52,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
break;
|
||||
}
|
||||
|
||||
_binaryInput = GetBytes(_hubMessage);
|
||||
_binaryInput = _hubProtocol.WriteToArray(_hubMessage);
|
||||
_binder = new TestBinder(_hubMessage);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void ReadSingleMessage()
|
||||
{
|
||||
if (!_hubProtocolReaderWriter.ReadMessages(_binaryInput, _binder, out var _))
|
||||
var messages = new List<HubMessage>();
|
||||
if (!_hubProtocol.TryParseMessages(_binaryInput, _binder, messages))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to read message");
|
||||
}
|
||||
|
|
@ -67,7 +69,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
[Benchmark]
|
||||
public void WriteSingleMessage()
|
||||
{
|
||||
if (_hubProtocolReaderWriter.WriteMessage(_hubMessage).Length != _binaryInput.Length)
|
||||
var bytes = _hubProtocol.WriteToArray(_hubMessage);
|
||||
if (bytes.Length != _binaryInput.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to write message");
|
||||
}
|
||||
|
|
@ -86,10 +89,5 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
ManyArguments = 2,
|
||||
LargeArguments = 3
|
||||
}
|
||||
|
||||
private byte[] GetBytes(HubMessage hubMessage)
|
||||
{
|
||||
return _hubProtocolReaderWriter.WriteMessage(_hubMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,6 +521,42 @@ describe("hubConnection", () => {
|
|||
});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
it("allows Server-Sent Events when negotiating for JSON protocol", async (done) => {
|
||||
const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, {
|
||||
logger: LogLevel.Trace,
|
||||
protocol: new JsonHubProtocol(),
|
||||
});
|
||||
|
||||
try {
|
||||
await hubConnection.start();
|
||||
|
||||
// Check what transport was used by asking the server to tell us.
|
||||
expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("ServerSentEvents");
|
||||
done();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("skips Server-Sent Events when negotiating for MsgPack protocol", async (done) => {
|
||||
const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, {
|
||||
logger: LogLevel.Trace,
|
||||
protocol: new MessagePackHubProtocol(),
|
||||
});
|
||||
|
||||
try {
|
||||
await hubConnection.start();
|
||||
|
||||
// Check what transport was used by asking the server to tell us.
|
||||
expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("LongPolling");
|
||||
done();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
function getJwtToken(url): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
|
||||
<!--
|
||||
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
|
||||
-->
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
{
|
||||
"name": "@aspnet/signalr-protocol-msgpack",
|
||||
"version": "1.0.0-preview2-t000",
|
||||
"version": "1.0.0-preview1-t000",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@aspnet/signalr": {
|
||||
"version": "file:../signalr",
|
||||
"dependencies": {
|
||||
"es6-promise": {
|
||||
"version": "4.2.2",
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/bl": {
|
||||
"version": "0.8.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/bl/-/bl-0.8.31.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@aspnet/signalr-protocol-msgpack",
|
||||
"version": "1.0.0-preview1-t000",
|
||||
"version": "1.0.0-preview2-t000",
|
||||
"description": "MsgPack Protocol support for ASP.NET Core SignalR",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"browser": "./dist/browser/signalr-protocol-msgpack.js",
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5",
|
||||
"build:browser": "node ../node_modules/rollup/bin/rollup -c",
|
||||
"build:uglify": "node ../node_modules/uglify-js/bin/uglifyjs --source-map \"url='signalr-protocol-msgpack.min.js.map',content='./dist/browser/signalr-protocol-msgpack.js.map'\" --comments -o ./dist/browser/signalr-protocol-msgpack.min.js ./dist/browser/signalr-protocol-msgpack.js",
|
||||
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json",
|
||||
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/signalr-protocol-msgpack/spec/**/*.spec.js"
|
||||
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json && cd ./spec/obj/src && npm init -y && npm install ../../../../signalr",
|
||||
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
|
||||
},
|
||||
"keywords": [
|
||||
"signalr",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"lib": [ "es2015", "dom" ],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@aspnet/signalr": [ "../../signalr/src/index" ]
|
||||
"@aspnet/*": [ "../../*" ]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "@aspnet/signalr";
|
||||
import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, StreamInvocationMessage, StreamItemMessage, TransferFormat } from "@aspnet/signalr";
|
||||
import { Buffer } from "buffer";
|
||||
import * as msgpack5 from "msgpack5";
|
||||
import { BinaryMessageFormat } from "./BinaryMessageFormat";
|
||||
|
|
@ -10,7 +10,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
|
||||
public readonly name: string = "messagepack";
|
||||
|
||||
public readonly type: ProtocolType = ProtocolType.Binary;
|
||||
public readonly transferFormat: TransferFormat = TransferFormat.Binary;
|
||||
|
||||
public parseMessages(input: ArrayBuffer): HubMessage[] {
|
||||
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@aspnet/signalr",
|
||||
"version": "1.0.0-preview2-t000",
|
||||
"version": "1.0.0-preview1-t000",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@aspnet/signalr",
|
||||
"version": "1.0.0-preview1-t000",
|
||||
"version": "1.0.0-preview2-t000",
|
||||
"description": "ASP.NET Core SignalR Client",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"browser": "./dist/browser/signalr.js",
|
||||
|
|
|
|||
|
|
@ -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 { IHttpConnectionOptions } from "../src/HttpConnection";
|
||||
import { HttpResponse } from "../src/index";
|
||||
import { ITransport, TransferMode, TransportType } from "../src/Transports";
|
||||
import { ITransport, TransferFormat, TransportType } from "../src/Transports";
|
||||
import { eachEndpointUrl, eachTransport } from "./Common";
|
||||
import { TestHttpClient } from "./TestHttpClient";
|
||||
|
||||
|
|
@ -15,13 +15,13 @@ const commonOptions: IHttpConnectionOptions = {
|
|||
|
||||
describe("HttpConnection", () => {
|
||||
it("cannot be created with relative url if document object is not present", () => {
|
||||
expect(() => new HttpConnection("/test", commonOptions))
|
||||
expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions))
|
||||
.toThrow(new Error("Cannot resolve '/test'."));
|
||||
});
|
||||
|
||||
it("cannot be created with relative url if window object is not present", () => {
|
||||
(global as any).window = {};
|
||||
expect(() => new HttpConnection("/test", commonOptions))
|
||||
expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions))
|
||||
.toThrow(new Error("Cannot resolve '/test'."));
|
||||
delete (global as any).window;
|
||||
});
|
||||
|
|
@ -34,7 +34,7 @@ describe("HttpConnection", () => {
|
|||
.on("GET", (r) => ""),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -64,7 +64,7 @@ describe("HttpConnection", () => {
|
|||
}),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -86,7 +86,7 @@ describe("HttpConnection", () => {
|
|||
.on("GET", (r) => ""),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -117,7 +117,7 @@ describe("HttpConnection", () => {
|
|||
}),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -129,17 +129,17 @@ describe("HttpConnection", () => {
|
|||
});
|
||||
|
||||
it("can stop a non-started connection", async (done) => {
|
||||
const connection = new HttpConnection("http://tempuri.org", commonOptions);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, commonOptions);
|
||||
await connection.stop();
|
||||
done();
|
||||
});
|
||||
|
||||
it("preserves users connection string", async (done) => {
|
||||
it("preserves user's query string", async (done) => {
|
||||
let connectUrl: string;
|
||||
const fakeTransport: ITransport = {
|
||||
connect(url: string): Promise<TransferMode> {
|
||||
connect(url: string): Promise<void> {
|
||||
connectUrl = url;
|
||||
return Promise.reject(TransferMode.Text);
|
||||
return Promise.reject("");
|
||||
},
|
||||
send(data: any): Promise<void> {
|
||||
return Promise.reject("");
|
||||
|
|
@ -159,7 +159,7 @@ describe("HttpConnection", () => {
|
|||
transport: fakeTransport,
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
|
||||
const connection = new HttpConnection("http://tempuri.org?q=myData", TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -190,7 +190,7 @@ describe("HttpConnection", () => {
|
|||
}),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
connection = new HttpConnection(givenUrl, options);
|
||||
connection = new HttpConnection(givenUrl, TransferFormat.Text, options);
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
|
|
@ -213,18 +213,18 @@ describe("HttpConnection", () => {
|
|||
const options: IHttpConnectionOptions = {
|
||||
...commonOptions,
|
||||
httpClient: new TestHttpClient()
|
||||
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
|
||||
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
|
||||
.on("GET", (r) => ""),
|
||||
transport: requestedTransport,
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
try {
|
||||
await connection.start();
|
||||
fail();
|
||||
done();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("No available transports found.");
|
||||
expect(e.message).toBe("Unable to initialize any of the available transports.");
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
|
@ -234,17 +234,17 @@ describe("HttpConnection", () => {
|
|||
const options: IHttpConnectionOptions = {
|
||||
...commonOptions,
|
||||
httpClient: new TestHttpClient()
|
||||
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
|
||||
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
|
||||
.on("GET", (r) => ""),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
try {
|
||||
await connection.start();
|
||||
fail();
|
||||
done();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("No available transports found.");
|
||||
expect(e.message).toBe("Unable to initialize any of the available transports.");
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
|
@ -256,7 +256,7 @@ describe("HttpConnection", () => {
|
|||
transport: TransportType.WebSockets,
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options);
|
||||
try {
|
||||
await connection.start();
|
||||
fail();
|
||||
|
|
@ -264,42 +264,24 @@ describe("HttpConnection", () => {
|
|||
} catch (e) {
|
||||
// WebSocket is created when the transport is connecting which happens after
|
||||
// negotiate request would be sent. No better/easier way to test this.
|
||||
expect(e.message).toBe("WebSocket is not defined");
|
||||
expect(e.message).toBe("'WebSocket' is not supported in your environment.");
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
[
|
||||
[TransferMode.Text, TransferMode.Text],
|
||||
[TransferMode.Text, TransferMode.Binary],
|
||||
[TransferMode.Binary, TransferMode.Text],
|
||||
[TransferMode.Binary, TransferMode.Binary],
|
||||
].forEach(([requestedTransferMode, transportTransferMode]) => {
|
||||
it(`connection returns ${transportTransferMode} transfer mode when ${requestedTransferMode} transfer mode is requested`, async () => {
|
||||
const fakeTransport = {
|
||||
// mode: TransferMode : TransferMode.Text
|
||||
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> { return Promise.resolve(transportTransferMode); },
|
||||
mode: transportTransferMode,
|
||||
onclose: null,
|
||||
onreceive: null,
|
||||
send(data: any): Promise<void> { return Promise.resolve(); },
|
||||
stop(): Promise<void> { return Promise.resolve(); },
|
||||
} as ITransport;
|
||||
describe(".constructor", () => {
|
||||
it("throws if no Url is provided", async () => {
|
||||
// Force TypeScript to let us call the constructor incorrectly :)
|
||||
expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
|
||||
});
|
||||
|
||||
const options: IHttpConnectionOptions = {
|
||||
...commonOptions,
|
||||
httpClient: new TestHttpClient()
|
||||
.on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
|
||||
.on("GET", (r) => ""),
|
||||
transport: fakeTransport,
|
||||
} as IHttpConnectionOptions;
|
||||
it("throws if no TransferFormat is provided", async () => {
|
||||
// Force TypeScript to let us call the constructor incorrectly :)
|
||||
expect(() => new (HttpConnection as any)("http://tempuri.org")).toThrowError("The 'transferFormat' argument is required.");
|
||||
});
|
||||
|
||||
const connection = new HttpConnection("https://tempuri.org", options);
|
||||
connection.features.transferMode = requestedTransferMode;
|
||||
await connection.start();
|
||||
const actualTransferMode = connection.features.transferMode;
|
||||
|
||||
expect(actualTransferMode).toBe(transportTransferMode);
|
||||
it("throws if an unsupported TransferFormat is provided", async () => {
|
||||
expect(() => new HttpConnection("http://tempuri.org", 42)).toThrowError("Unknown transferFormat value: 42.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { MessageType } from "../src/IHubProtocol";
|
|||
import { ILogger, LogLevel } from "../src/ILogger";
|
||||
import { Observer } from "../src/Observable";
|
||||
import { TextMessageFormat } from "../src/TextMessageFormat";
|
||||
import { ITransport, TransferMode, TransportType } from "../src/Transports";
|
||||
import { ITransport, TransferFormat, TransportType } from "../src/Transports";
|
||||
|
||||
import { IHubConnectionOptions } from "../src/HubConnection";
|
||||
import { asyncit as it, captureException, delay, PromiseSource } from "./Utils";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { HttpClient, HttpRequest, HttpResponse } from "../src/HttpClient";
|
||||
|
||||
type TestHttpHandlerResult = HttpResponse | string;
|
||||
type TestHttpHandlerResult = any;
|
||||
export type TestHttpHandler = (request: HttpRequest, next?: (request: HttpRequest) => Promise<HttpResponse>) => Promise<TestHttpHandlerResult> | TestHttpHandlerResult;
|
||||
|
||||
export class TestHttpClient extends HttpClient {
|
||||
|
|
@ -57,9 +57,14 @@ export class TestHttpClient extends HttpClient {
|
|||
}
|
||||
|
||||
if (typeof val === "string") {
|
||||
// string payload
|
||||
return new HttpResponse(200, "OK", val);
|
||||
} else if(typeof val === "object" && val.statusCode) {
|
||||
// HttpResponse payload
|
||||
return val as HttpResponse;
|
||||
} else {
|
||||
return val;
|
||||
// JSON payload
|
||||
return new HttpResponse(200, "OK", JSON.stringify(val));
|
||||
}
|
||||
} else {
|
||||
return await oldHandler(request);
|
||||
|
|
|
|||
|
|
@ -56,4 +56,4 @@ export class PromiseSource<T> {
|
|||
public reject(reason?: any) {
|
||||
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 { ILogger, LogLevel } from "./ILogger";
|
||||
import { LoggerFactory } from "./Loggers";
|
||||
import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferMode, TransportType, WebSocketTransport } from "./Transports";
|
||||
import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferFormat, TransportType, WebSocketTransport } from "./Transports";
|
||||
import { Arg } from "./Utils";
|
||||
|
||||
export interface IHttpConnectionOptions {
|
||||
httpClient?: HttpClient;
|
||||
|
|
@ -23,7 +24,12 @@ const enum ConnectionState {
|
|||
|
||||
interface INegotiateResponse {
|
||||
connectionId: string;
|
||||
availableTransports: string[];
|
||||
availableTransports: IAvailableTransport[];
|
||||
}
|
||||
|
||||
interface IAvailableTransport {
|
||||
transport: keyof typeof TransportType;
|
||||
transferFormats: Array<keyof typeof TransferFormat>;
|
||||
}
|
||||
|
||||
export class HttpConnection implements IConnection {
|
||||
|
|
@ -33,13 +39,19 @@ export class HttpConnection implements IConnection {
|
|||
private readonly httpClient: HttpClient;
|
||||
private readonly logger: ILogger;
|
||||
private readonly options: IHttpConnectionOptions;
|
||||
private readonly transferFormat: TransferFormat;
|
||||
private transport: ITransport;
|
||||
private connectionId: string;
|
||||
private startPromise: Promise<void>;
|
||||
|
||||
public readonly features: any = {};
|
||||
|
||||
constructor(url: string, options: IHttpConnectionOptions = {}) {
|
||||
constructor(url: string, transferFormat: TransferFormat, options: IHttpConnectionOptions = {}) {
|
||||
Arg.isRequired(url, "url");
|
||||
Arg.isRequired(transferFormat, "transferFormat");
|
||||
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
|
||||
|
||||
this.transferFormat = transferFormat;
|
||||
this.logger = LoggerFactory.createLogger(options.logger);
|
||||
this.baseUrl = this.resolveUrl(url);
|
||||
|
||||
|
|
@ -67,7 +79,7 @@ export class HttpConnection implements IConnection {
|
|||
if (this.options.transport === TransportType.WebSockets) {
|
||||
// No need to add a connection ID in this case
|
||||
this.url = this.baseUrl;
|
||||
this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]);
|
||||
this.transport = this.constructTransport(TransportType.WebSockets);
|
||||
} else {
|
||||
let headers;
|
||||
const token = this.options.accessTokenFactory();
|
||||
|
|
@ -91,50 +103,73 @@ export class HttpConnection implements IConnection {
|
|||
|
||||
if (this.connectionId) {
|
||||
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
|
||||
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports);
|
||||
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports, this.transferFormat);
|
||||
}
|
||||
}
|
||||
|
||||
this.transport.onreceive = this.onreceive;
|
||||
this.transport.onclose = (e) => this.stopConnection(true, e);
|
||||
|
||||
const requestedTransferMode =
|
||||
this.features.transferMode === TransferMode.Binary
|
||||
? TransferMode.Binary
|
||||
: TransferMode.Text;
|
||||
|
||||
this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode, this);
|
||||
await this.transport.connect(this.url, this.transferFormat, this);
|
||||
|
||||
// only change the state if we were connecting to not overwrite
|
||||
// the state if the connection is already marked as Disconnected
|
||||
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
|
||||
} catch (e) {
|
||||
this.logger.log(LogLevel.Error, "Failed to start the connection. " + e);
|
||||
this.logger.log(LogLevel.Error, "Failed to start the connection: " + e);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
this.transport = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private createTransport(transport: TransportType | ITransport, availableTransports: string[]): ITransport {
|
||||
if ((transport === null || transport === undefined) && availableTransports.length > 0) {
|
||||
transport = TransportType[availableTransports[0]];
|
||||
}
|
||||
if (transport === TransportType.WebSockets && availableTransports.indexOf(TransportType[transport]) >= 0) {
|
||||
return new WebSocketTransport(this.options.accessTokenFactory, this.logger);
|
||||
}
|
||||
if (transport === TransportType.ServerSentEvents && availableTransports.indexOf(TransportType[transport]) >= 0) {
|
||||
return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
|
||||
}
|
||||
if (transport === TransportType.LongPolling && availableTransports.indexOf(TransportType[transport]) >= 0) {
|
||||
return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
|
||||
private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport {
|
||||
if (this.isITransport(requestedTransport)) {
|
||||
this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly.");
|
||||
return requestedTransport;
|
||||
}
|
||||
|
||||
if (this.isITransport(transport)) {
|
||||
return transport;
|
||||
for (const endpoint of availableTransports) {
|
||||
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat);
|
||||
if (transport) {
|
||||
return this.constructTransport(transport);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No available transports found.");
|
||||
throw new Error("Unable to initialize any of the available transports.");
|
||||
}
|
||||
|
||||
private constructTransport(transport: TransportType) {
|
||||
switch (transport) {
|
||||
case TransportType.WebSockets:
|
||||
return new WebSocketTransport(this.options.accessTokenFactory, this.logger);
|
||||
case TransportType.ServerSentEvents:
|
||||
return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
|
||||
case TransportType.LongPolling:
|
||||
return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger);
|
||||
default:
|
||||
throw new Error(`Unknown transport: ${transport}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveTransport(endpoint: IAvailableTransport, requestedTransport: TransportType, requestedTransferFormat: TransferFormat): TransportType | null {
|
||||
const transport = TransportType[endpoint.transport];
|
||||
if (!transport) {
|
||||
this.logger.log(LogLevel.Trace, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`);
|
||||
} else {
|
||||
const transferFormats = endpoint.transferFormats.map((s) => TransferFormat[s]);
|
||||
if (!requestedTransport || transport === requestedTransport) {
|
||||
if (transferFormats.indexOf(requestedTransferFormat) >= 0) {
|
||||
this.logger.log(LogLevel.Trace, `Selecting transport '${transport}'`);
|
||||
return transport;
|
||||
} else {
|
||||
this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it does not support the requested transfer format '${requestedTransferFormat}'.`);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it was disabled by the client.`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isITransport(transport: any): transport is ITransport {
|
||||
|
|
@ -151,7 +186,7 @@ export class HttpConnection implements IConnection {
|
|||
|
||||
public send(data: any): Promise<void> {
|
||||
if (this.connectionState !== ConnectionState.Connected) {
|
||||
throw new Error("Cannot send data if the connection is not in the 'Connected' State");
|
||||
throw new Error("Cannot send data if the connection is not in the 'Connected' State.");
|
||||
}
|
||||
|
||||
return this.transport.send(data);
|
||||
|
|
@ -210,7 +245,7 @@ export class HttpConnection implements IConnection {
|
|||
}
|
||||
|
||||
const normalizedUrl = baseUrl + url;
|
||||
this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'`);
|
||||
this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'.`);
|
||||
return normalizedUrl;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol";
|
||||
import { ConnectionClosed } from "./Common";
|
||||
import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection";
|
||||
import { IConnection } from "./IConnection";
|
||||
import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
|
||||
import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
|
||||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { JsonHubProtocol } from "./JsonHubProtocol";
|
||||
import { ConsoleLogger, LoggerFactory, NullLogger } from "./Loggers";
|
||||
import { Observable, Subject } from "./Observable";
|
||||
import { TextMessageFormat } from "./TextMessageFormat";
|
||||
import { TransferMode, TransportType } from "./Transports";
|
||||
import { TransferFormat, TransportType } from "./Transports";
|
||||
|
||||
export { JsonHubProtocol };
|
||||
|
||||
|
|
@ -40,15 +39,16 @@ export class HubConnection {
|
|||
|
||||
this.timeoutInMilliseconds = options.timeoutInMilliseconds || DEFAULT_TIMEOUT_IN_MS;
|
||||
|
||||
this.protocol = options.protocol || new JsonHubProtocol();
|
||||
|
||||
if (typeof urlOrConnection === "string") {
|
||||
this.connection = new HttpConnection(urlOrConnection, options);
|
||||
this.connection = new HttpConnection(urlOrConnection, this.protocol.transferFormat, options);
|
||||
} else {
|
||||
this.connection = urlOrConnection;
|
||||
}
|
||||
|
||||
this.logger = LoggerFactory.createLogger(options.logger);
|
||||
|
||||
this.protocol = options.protocol || new JsonHubProtocol();
|
||||
this.connection.onreceive = (data: any) => this.processIncomingData(data);
|
||||
this.connection.onclose = (error?: Error) => this.connectionClosed(error);
|
||||
|
||||
|
|
@ -133,14 +133,7 @@ export class HubConnection {
|
|||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
const requestedTransferMode =
|
||||
(this.protocol.type === ProtocolType.Binary)
|
||||
? TransferMode.Binary
|
||||
: TransferMode.Text;
|
||||
|
||||
this.connection.features.transferMode = requestedTransferMode;
|
||||
await this.connection.start();
|
||||
const actualTransferMode = this.connection.features.transferMode;
|
||||
|
||||
await this.connection.send(
|
||||
TextMessageFormat.write(
|
||||
|
|
@ -148,10 +141,6 @@ export class HubConnection {
|
|||
|
||||
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`);
|
||||
|
||||
if (requestedTransferMode === TransferMode.Binary && actualTransferMode === TransferMode.Text) {
|
||||
this.protocol = new Base64EncodedHubProtocol(this.protocol);
|
||||
}
|
||||
|
||||
this.configureTimeout();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { ConnectionClosed, DataReceived } from "./Common";
|
||||
import { ITransport, TransferMode, TransportType } from "./Transports";
|
||||
import { ITransport, TransportType } from "./Transports";
|
||||
|
||||
export interface IConnection {
|
||||
readonly features: any;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
import { TransferFormat } from "./Transports";
|
||||
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
export const enum MessageType {
|
||||
|
|
@ -58,14 +60,9 @@ export interface CancelInvocationMessage extends HubInvocationMessage {
|
|||
readonly type: MessageType.CancelInvocation;
|
||||
}
|
||||
|
||||
export const enum ProtocolType {
|
||||
Text = 1,
|
||||
Binary,
|
||||
}
|
||||
|
||||
export interface IHubProtocol {
|
||||
readonly name: string;
|
||||
readonly type: ProtocolType;
|
||||
readonly transferFormat: TransferFormat;
|
||||
parseMessages(input: any): HubMessage[];
|
||||
writeMessage(message: HubMessage): any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { HubMessage, IHubProtocol, ProtocolType } from "./IHubProtocol";
|
||||
import { HubMessage, IHubProtocol } from "./IHubProtocol";
|
||||
import { TextMessageFormat } from "./TextMessageFormat";
|
||||
import { TransferFormat } from "./Transports";
|
||||
|
||||
export const JSON_HUB_PROTOCOL_NAME: string = "json";
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ export class JsonHubProtocol implements IHubProtocol {
|
|||
|
||||
public readonly name: string = JSON_HUB_PROTOCOL_NAME;
|
||||
|
||||
public readonly type: ProtocolType = ProtocolType.Text;
|
||||
public readonly transferFormat: TransferFormat = TransferFormat.Text;
|
||||
|
||||
public parseMessages(input: string): HubMessage[] {
|
||||
if (!input) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { HttpError, TimeoutError } from "./Errors";
|
|||
import { HttpClient, HttpRequest } from "./HttpClient";
|
||||
import { IConnection } from "./IConnection";
|
||||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { Arg } from "./Utils";
|
||||
|
||||
export enum TransportType {
|
||||
WebSockets,
|
||||
|
|
@ -14,13 +15,13 @@ export enum TransportType {
|
|||
LongPolling,
|
||||
}
|
||||
|
||||
export const enum TransferMode {
|
||||
export enum TransferFormat {
|
||||
Text = 1,
|
||||
Binary,
|
||||
}
|
||||
|
||||
export interface ITransport {
|
||||
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode>;
|
||||
connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void>;
|
||||
send(data: any): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
onreceive: DataReceived;
|
||||
|
|
@ -37,9 +38,17 @@ export class WebSocketTransport implements ITransport {
|
|||
this.accessTokenFactory = accessTokenFactory || (() => null);
|
||||
}
|
||||
|
||||
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
|
||||
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
|
||||
Arg.isRequired(url, "url");
|
||||
Arg.isRequired(transferFormat, "transferFormat");
|
||||
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
|
||||
Arg.isRequired(connection, "connection");
|
||||
|
||||
return new Promise<TransferMode>((resolve, reject) => {
|
||||
if (typeof (WebSocket) === "undefined") {
|
||||
throw new Error("'WebSocket' is not supported in your environment.");
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
url = url.replace(/^http/, "ws");
|
||||
const token = this.accessTokenFactory();
|
||||
if (token) {
|
||||
|
|
@ -47,18 +56,18 @@ export class WebSocketTransport implements ITransport {
|
|||
}
|
||||
|
||||
const webSocket = new WebSocket(url);
|
||||
if (requestedTransferMode === TransferMode.Binary) {
|
||||
if (transferFormat === TransferFormat.Binary) {
|
||||
webSocket.binaryType = "arraybuffer";
|
||||
}
|
||||
|
||||
webSocket.onopen = (event: Event) => {
|
||||
this.logger.log(LogLevel.Information, `WebSocket connected to ${url}`);
|
||||
this.webSocket = webSocket;
|
||||
resolve(requestedTransferMode);
|
||||
resolve();
|
||||
};
|
||||
|
||||
webSocket.onerror = (event: Event) => {
|
||||
reject();
|
||||
webSocket.onerror = (event: ErrorEvent) => {
|
||||
reject(event.error);
|
||||
};
|
||||
|
||||
webSocket.onmessage = (message: MessageEvent) => {
|
||||
|
|
@ -115,13 +124,22 @@ export class ServerSentEventsTransport implements ITransport {
|
|||
this.logger = logger;
|
||||
}
|
||||
|
||||
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
|
||||
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
|
||||
Arg.isRequired(url, "url");
|
||||
Arg.isRequired(transferFormat, "transferFormat");
|
||||
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
|
||||
Arg.isRequired(connection, "connection");
|
||||
|
||||
if (typeof (EventSource) === "undefined") {
|
||||
Promise.reject("EventSource not supported by the browser.");
|
||||
throw new Error("'EventSource' is not supported in your environment.");
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
return new Promise<TransferMode>((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (transferFormat !== TransferFormat.Text) {
|
||||
reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
|
||||
}
|
||||
|
||||
const token = this.accessTokenFactory();
|
||||
if (token) {
|
||||
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
|
||||
|
|
@ -145,7 +163,7 @@ export class ServerSentEventsTransport implements ITransport {
|
|||
};
|
||||
|
||||
eventSource.onerror = (e: any) => {
|
||||
reject();
|
||||
reject(new Error(e.message || "Error occurred"));
|
||||
|
||||
// don't report an error if the transport did not start successfully
|
||||
if (this.eventSource && this.onclose) {
|
||||
|
|
@ -157,7 +175,7 @@ export class ServerSentEventsTransport implements ITransport {
|
|||
this.logger.log(LogLevel.Information, `SSE connected to ${this.url}`);
|
||||
this.eventSource = eventSource;
|
||||
// SSE is a text protocol
|
||||
resolve(TransferMode.Text);
|
||||
resolve();
|
||||
};
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
|
|
@ -197,29 +215,34 @@ export class LongPollingTransport implements ITransport {
|
|||
this.pollAbort = new AbortController();
|
||||
}
|
||||
|
||||
public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
|
||||
public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise<void> {
|
||||
Arg.isRequired(url, "url");
|
||||
Arg.isRequired(transferFormat, "transferFormat");
|
||||
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
|
||||
Arg.isRequired(connection, "connection");
|
||||
|
||||
this.url = url;
|
||||
|
||||
// Set a flag indicating we have inherent keep-alive in this transport.
|
||||
connection.features.inherentKeepAlive = true;
|
||||
|
||||
if (requestedTransferMode === TransferMode.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
|
||||
if (transferFormat === TransferFormat.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
|
||||
// This will work if we fix: https://github.com/aspnet/SignalR/issues/742
|
||||
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
|
||||
}
|
||||
|
||||
this.poll(this.url, requestedTransferMode);
|
||||
return Promise.resolve(requestedTransferMode);
|
||||
this.poll(this.url, transferFormat);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async poll(url: string, transferMode: TransferMode): Promise<void> {
|
||||
private async poll(url: string, transferFormat: TransferFormat): Promise<void> {
|
||||
const pollOptions: HttpRequest = {
|
||||
abortSignal: this.pollAbort.signal,
|
||||
headers: new Map<string, string>(),
|
||||
timeout: 90000,
|
||||
};
|
||||
|
||||
if (transferMode === TransferMode.Binary) {
|
||||
if (transferFormat === TransferFormat.Binary) {
|
||||
pollOptions.responseType = "arraybuffer";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
|
|
@ -8,6 +8,7 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -42,7 +43,7 @@ namespace ClientSample
|
|||
var closeTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += e => closeTcs.SetResult(null);
|
||||
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
|
||||
await connection.StartAsync();
|
||||
await connection.StartAsync(TransferFormat.Text);
|
||||
|
||||
Console.WriteLine($"Connected to {baseUrl}");
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ namespace SocialWeather
|
|||
connection.Features.Get<IConnectionMetadataFeature>().Metadata["format"] = format;
|
||||
if (string.Equals(format, "protobuf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var transferModeFeature = connection.Features.Get<ITransferModeFeature>();
|
||||
if (transferModeFeature != null)
|
||||
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
|
||||
if (transferFormatFeature != null)
|
||||
{
|
||||
transferModeFeature.TransferMode = TransferMode.Binary;
|
||||
transferFormatFeature.ActiveFormat = TransferFormat.Binary;
|
||||
}
|
||||
}
|
||||
_connectionList.Add(connection);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MsgPack.Serialization;
|
||||
using SocketsSample.EndPoints;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
|
||||
<!--
|
||||
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
|
||||
-->
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
</select>
|
||||
|
||||
<select id="transport">
|
||||
<option value="WebSockets" selected>WebSockets</option>
|
||||
<option value="Automatic" selected>Automatic</option>
|
||||
<option value="WebSockets">WebSockets</option>
|
||||
<option value="ServerSentEvents">ServerSentEvents</option>
|
||||
<option value="LongPolling">LongPolling</option>
|
||||
</select>
|
||||
|
|
@ -135,14 +136,19 @@
|
|||
var connection;
|
||||
|
||||
click('connect', function (event) {
|
||||
let transportType = signalR.TransportType[transportDropdown.value] || signalR.TransportType.WebSockets;
|
||||
let hubRoute = hubTypeDropdown.value || "default";
|
||||
let protocol = protocolDropdown.value === "msgpack" ?
|
||||
new signalR.protocols.msgpack.MessagePackHubProtocol() :
|
||||
new signalR.JsonHubProtocol();
|
||||
|
||||
let options = { logger: logger, protocol: protocol };
|
||||
|
||||
if (transportDropdown.value !== "Automatic") {
|
||||
options.transport = signalR.TransportType[transportDropdown.value];
|
||||
}
|
||||
|
||||
console.log('http://' + document.location.host + '/' + hubRoute);
|
||||
connection = new signalR.HubConnection(hubRoute, { transport: transportType, logger: logger, protocol: protocol });
|
||||
connection = new signalR.HubConnection(hubRoute, options);
|
||||
connection.on('Send', function (msg) {
|
||||
addLine('message-list', msg);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,10 +23,34 @@ The `POST [endpoint-base]/negotiate` request is used to establish connection bet
|
|||
```
|
||||
{
|
||||
"connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d",
|
||||
"availableTransports":["WebSockets","ServerSentEvents","LongPolling"]
|
||||
"availableTransports":[
|
||||
{
|
||||
"transport": "WebSockets",
|
||||
"transferFormats": [ "Text", "Binary" ]
|
||||
},
|
||||
{
|
||||
"transport": "ServerSentEvents",
|
||||
"transferFormats": [ "Text" ]
|
||||
},
|
||||
{
|
||||
"transport": "LongPolling",
|
||||
"transferFormats": [ "Text", "Binary" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The payload returned from this endpoint provides the following data:
|
||||
|
||||
* The `connectionId` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives).
|
||||
* The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`)
|
||||
|
||||
## Transfer Formats
|
||||
|
||||
ASP.NET Endpoints support two different transfer formats: `Text` and `Binary`. `Text` refers to UTF-8 text, and `Binary` refers to any arbitrary binary data. The transfer format serves two purposes. First, in the WebSockets transport, it is used to determine if `Text` or `Binary` WebSocket frames should be used to carry data. This is useful in debugging as most browser Dev Tools only show the content of `Text` frames. When using a text-based protocol like JSON, it is preferable for the WebSockets transport to use `Text` frames. How a client/server indicate the transfer format currently being used is implementation-defined.
|
||||
|
||||
Some transports are limited to supporting only `Text` data (specifically, Server-Sent Events). These transports cannot carry arbitrary binary data (without additional encoding, such as Base-64) due to limitations in their protocol. The transfer formats supported by each transport are described as part of the `POST [endpoint-base]/negotiate` response to allow clients to ignore transports that cannot support arbitrary binary data when they have a need to send/receive that data. How the client indicates the transfer format it wishes to use is also implementation-defined.
|
||||
|
||||
## WebSockets (Full Duplex)
|
||||
|
||||
The WebSockets transport is unique in that it is full duplex, and a persistent connection that can be established in a single operation. As a result, the client is not required to use the `POST [endpoint-base]/negotiate` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata.
|
||||
|
|
@ -53,7 +77,6 @@ If the relevant connection has been terminated, a `404 Not Found` status code is
|
|||
|
||||
Server-Sent Events (SSE) is a protocol specified by WHATWG at [https://html.spec.whatwg.org/multipage/comms.html#server-sent-events](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events). It is capable of sending data from server to client only, so it must be paired with the HTTP Post transport. It also requires a connection already be established using the `POST [endpoint-base]/negotiate` request.
|
||||
|
||||
|
||||
The protocol is similar to Long Polling in that the client opens a request to an endpoint and leaves it open. The server transmits frames as "events" using the SSE protocol. The protocol encodes a single event as a sequence of key-value pair lines, separated by `:` and using any of `\r\n`, `\n` or `\r` as line-terminators, followed by a final blank line. Keys can be duplicated and their values are concatenated with `\n`. So the following represents two events:
|
||||
|
||||
```
|
||||
|
|
@ -71,6 +94,8 @@ In the first event, the value of `baz` would be `boz\nbiz\nflarg`, due to the co
|
|||
|
||||
In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata.
|
||||
|
||||
The Server-Sent Events transport only supports text data, because it is a text-based protocol. As a result, it is reported by the server as supporting only the `Text` transfer format. If a client wishes to send arbitrary binary data, it should skip the Server-Sent Events transport when selecting an appropriate transport.
|
||||
|
||||
## Long Polling (Server-to-Client only)
|
||||
|
||||
Long Polling is a server-to-client half-transport, so it is always paired with HTTP Post. It requires a connection already be established using the `POST [endpoint-base]/negotiate` request.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
|
|
@ -30,7 +28,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private readonly IConnection _connection;
|
||||
private readonly IHubProtocol _protocol;
|
||||
private readonly HubBinder _binder;
|
||||
private HubProtocolReaderWriter _protocolReaderWriter;
|
||||
|
||||
private readonly object _pendingCallsLock = new object();
|
||||
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
|
||||
|
|
@ -113,26 +110,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
|
||||
private async Task StartAsyncCore()
|
||||
{
|
||||
var transferModeFeature = _connection.Features.Get<ITransferModeFeature>();
|
||||
if (transferModeFeature == null)
|
||||
{
|
||||
transferModeFeature = new TransferModeFeature();
|
||||
_connection.Features.Set(transferModeFeature);
|
||||
}
|
||||
|
||||
var requestedTransferMode =
|
||||
_protocol.Type == ProtocolType.Binary
|
||||
? TransferMode.Binary
|
||||
: TransferMode.Text;
|
||||
|
||||
transferModeFeature.TransferMode = requestedTransferMode;
|
||||
await _connection.StartAsync();
|
||||
await _connection.StartAsync(_protocol.TransferFormat);
|
||||
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
|
||||
|
||||
var actualTransferMode = transferModeFeature.TransferMode;
|
||||
|
||||
_protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode));
|
||||
|
||||
Log.HubProtocol(_logger, _protocol.Name);
|
||||
|
||||
_connectionActive = new CancellationTokenSource();
|
||||
|
|
@ -146,20 +126,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
ResetTimeoutTimer();
|
||||
}
|
||||
|
||||
private IDataEncoder GetDataEncoder(TransferMode requestedTransferMode, TransferMode actualTransferMode)
|
||||
{
|
||||
if (requestedTransferMode == TransferMode.Binary && actualTransferMode == TransferMode.Text)
|
||||
{
|
||||
// This is for instance for SSE which is a Text protocol and the user wants to use a binary
|
||||
// protocol so we need to encode messages.
|
||||
return new Base64Encoder();
|
||||
}
|
||||
|
||||
Debug.Assert(requestedTransferMode == actualTransferMode, "All transports besides SSE are expected to support binary mode.");
|
||||
|
||||
return new PassThroughEncoder();
|
||||
}
|
||||
|
||||
public async Task StopAsync() => await StopAsyncCore().ForceAsync();
|
||||
|
||||
private Task StopAsyncCore() => _connection.StopAsync();
|
||||
|
|
@ -295,7 +261,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
{
|
||||
try
|
||||
{
|
||||
var payload = _protocolReaderWriter.WriteMessage(hubMessage);
|
||||
var payload = _protocol.WriteToArray(hubMessage);
|
||||
Log.SendInvocation(_logger, hubMessage.InvocationId);
|
||||
|
||||
await _connection.SendAsync(payload, irq.CancellationToken);
|
||||
|
|
@ -328,7 +294,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
{
|
||||
Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length);
|
||||
|
||||
var payload = _protocolReaderWriter.WriteMessage(invocationMessage);
|
||||
var payload = _protocol.WriteToArray(invocationMessage);
|
||||
Log.SendInvocation(_logger, invocationMessage.InvocationId);
|
||||
|
||||
await _connection.SendAsync(payload, cancellationToken);
|
||||
|
|
@ -345,7 +311,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
{
|
||||
ResetTimeoutTimer();
|
||||
Log.ParsingMessages(_logger, data.Length);
|
||||
if (_protocolReaderWriter.ReadMessages(data, _binder, out var messages))
|
||||
var messages = new List<HubMessage>();
|
||||
if (_protocol.TryParseMessages(data, _binder, messages))
|
||||
{
|
||||
Log.ReceivingMessages(_logger, messages.Count);
|
||||
foreach (var message in messages)
|
||||
|
|
@ -621,10 +588,5 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
return _callback(parameters, _state);
|
||||
}
|
||||
}
|
||||
|
||||
private class TransferModeFeature : ITransferModeFeature
|
||||
{
|
||||
public TransferMode TransferMode { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||
{
|
||||
|
|
@ -11,39 +12,48 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
}
|
||||
|
||||
// Initialize with capacity 4 for the 2 built in protocols and 2 data encoders
|
||||
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(4);
|
||||
// Initialize with capacity 2 for the 2 built in protocols
|
||||
private object _lock = new object();
|
||||
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(2);
|
||||
|
||||
public byte[] WriteMessage(HubProtocolReaderWriter protocolReaderWriter)
|
||||
public byte[] WriteMessage(IHubProtocol protocol)
|
||||
{
|
||||
for (var i = 0; i < _serializedMessages.Count; i++)
|
||||
// REVIEW: Revisit lock
|
||||
// Could use a reader/writer lock to allow the loop to take place in "unlocked" code
|
||||
// Or, could use a fixed size array and Interlocked to manage it.
|
||||
// Or, Immutable *ducks*
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_serializedMessages[i].ProtocolReaderWriter.Equals(protocolReaderWriter))
|
||||
for (var i = 0; i < _serializedMessages.Count; i++)
|
||||
{
|
||||
return _serializedMessages[i].Message;
|
||||
if (_serializedMessages[i].Protocol.Equals(protocol))
|
||||
{
|
||||
return _serializedMessages[i].Message;
|
||||
}
|
||||
}
|
||||
|
||||
var bytes = protocol.WriteToArray(this);
|
||||
|
||||
// We don't want to balloon memory if someone writes a poor IHubProtocolResolver
|
||||
// So we cap how many caches we store and worst case just serialize the message for every connection
|
||||
if (_serializedMessages.Count < 10)
|
||||
{
|
||||
_serializedMessages.Add(new SerializedMessage(protocol, bytes));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
var bytes = protocolReaderWriter.WriteMessage(this);
|
||||
|
||||
// We don't want to balloon memory if someone writes a poor IHubProtocolResolver
|
||||
// So we cap how many caches we store and worst case just serialize the message for every connection
|
||||
if (_serializedMessages.Count < 10)
|
||||
{
|
||||
_serializedMessages.Add(new SerializedMessage(protocolReaderWriter, bytes));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private readonly struct SerializedMessage
|
||||
{
|
||||
public readonly HubProtocolReaderWriter ProtocolReaderWriter;
|
||||
public readonly IHubProtocol Protocol;
|
||||
public readonly byte[] Message;
|
||||
|
||||
public SerializedMessage(HubProtocolReaderWriter protocolReaderWriter, byte[] message)
|
||||
public SerializedMessage(IHubProtocol protocol, byte[] message)
|
||||
{
|
||||
ProtocolReaderWriter = protocolReaderWriter;
|
||||
Protocol = protocol;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||
{
|
||||
|
|
@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
string Name { get; }
|
||||
|
||||
ProtocolType Type { get; }
|
||||
TransferFormat TransferFormat { get; }
|
||||
|
||||
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.IO;
|
|||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
|
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
public string Name => ProtocolName;
|
||||
|
||||
public ProtocolType Type => ProtocolType.Text;
|
||||
public TransferFormat TransferFormat => TransferFormat.Text;
|
||||
|
||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Sockets.Abstractions\Microsoft.AspNetCore.Sockets.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ using Microsoft.AspNetCore.Http.Features;
|
|||
using Microsoft.AspNetCore.Protocols;
|
||||
using Microsoft.AspNetCore.SignalR.Core;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Features;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -27,8 +25,6 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
public class HubConnectionContext
|
||||
{
|
||||
private static Action<object> _abortedCallback = AbortConnection;
|
||||
private static readonly Base64Encoder Base64Encoder = new Base64Encoder();
|
||||
private static readonly PassThroughEncoder PassThroughEncoder = new PassThroughEncoder();
|
||||
|
||||
private readonly ConnectionContext _connectionContext;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -62,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
|
||||
public string UserIdentifier { get; private set; }
|
||||
|
||||
internal virtual HubProtocolReaderWriter ProtocolReaderWriter { get; set; }
|
||||
internal virtual IHubProtocol Protocol { get; set; }
|
||||
|
||||
internal ExceptionDispatchInfo AbortException { get; private set; }
|
||||
|
||||
|
|
@ -85,7 +81,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
// This will internally cache the buffer for each unique HubProtocol/DataEncoder combination
|
||||
// So that we don't serialize the HubMessage for every single connection
|
||||
var buffer = message.WriteMessage(ProtocolReaderWriter);
|
||||
var buffer = message.WriteMessage(Protocol);
|
||||
_connectionContext.Transport.Output.Write(buffer);
|
||||
|
||||
Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp());
|
||||
|
|
@ -156,27 +152,25 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
if (NegotiationProtocol.TryParseMessage(buffer, out var negotiationMessage, out consumed, out examined))
|
||||
{
|
||||
var protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this);
|
||||
Protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this);
|
||||
|
||||
var transportCapabilities = Features.Get<IConnectionTransportFeature>()?.TransportCapabilities
|
||||
?? throw new InvalidOperationException("Unable to read transport capabilities.");
|
||||
// If there's a transfer format feature, we need to check if we're compatible and set the active format.
|
||||
// If there isn't a feature, it means that the transport supports binary data and doesn't need us to tell them
|
||||
// what format we're writing.
|
||||
var transferFormatFeature = Features.Get<ITransferFormatFeature>();
|
||||
if (transferFormatFeature != null)
|
||||
{
|
||||
if ((transferFormatFeature.SupportedFormats & Protocol.TransferFormat) == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot use the '{Protocol.Name}' protocol on the current transport. The transport does not support the '{Protocol.TransferFormat}' transfer mode.");
|
||||
}
|
||||
|
||||
var dataEncoder = (protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) == 0)
|
||||
? (IDataEncoder)Base64Encoder
|
||||
: PassThroughEncoder;
|
||||
transferFormatFeature.ActiveFormat = Protocol.TransferFormat;
|
||||
}
|
||||
|
||||
var transferModeFeature = Features.Get<ITransferModeFeature>() ??
|
||||
throw new InvalidOperationException("Unable to read transfer mode.");
|
||||
_cachedPingMessage = Protocol.WriteToArray(PingMessage.Instance);
|
||||
|
||||
transferModeFeature.TransferMode =
|
||||
(protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) != 0)
|
||||
? TransferMode.Binary
|
||||
: TransferMode.Text;
|
||||
|
||||
ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder);
|
||||
_cachedPingMessage = ProtocolReaderWriter.WriteMessage(PingMessage.Instance);
|
||||
|
||||
Log.UsingHubProtocol(_logger, protocol.Name);
|
||||
Log.UsingHubProtocol(_logger, Protocol.Name);
|
||||
|
||||
UserIdentifier = userIdProvider.GetUserId(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Protocols;
|
||||
using Microsoft.AspNetCore.SignalR.Core;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -141,7 +144,10 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
if (connection.ProtocolReaderWriter.ReadMessages(buffer, _dispatcher, out var hubMessages, out consumed, out examined))
|
||||
var hubMessages = new List<HubMessage>();
|
||||
|
||||
// TODO: Make this incremental
|
||||
if (connection.Protocol.TryParseMessages(buffer.ToArray(), _dispatcher, hubMessages))
|
||||
{
|
||||
foreach (var hubMessage in hubMessages)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
|
|
@ -24,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
public string Name => ProtocolName;
|
||||
|
||||
public ProtocolType Type => ProtocolType.Binary;
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
public MessagePackHubProtocol()
|
||||
: this(Options.Create(new MessagePackHubProtocolOptions()))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO.Pipelines;
|
||||
|
|
@ -8,7 +8,5 @@ namespace Microsoft.AspNetCore.Sockets.Features
|
|||
public interface IConnectionTransportFeature
|
||||
{
|
||||
IDuplexPipe Transport { get; set; }
|
||||
|
||||
TransferMode TransportCapabilities { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
Task StartAsync();
|
||||
Task StartAsync(TransferFormat transferFormat);
|
||||
Task SendAsync(byte[] data, CancellationToken cancellationToken);
|
||||
Task StopAsync();
|
||||
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.
|
||||
|
||||
using System;
|
||||
|
|
@ -6,7 +6,7 @@ using System;
|
|||
namespace Microsoft.AspNetCore.Sockets
|
||||
{
|
||||
[Flags]
|
||||
public enum TransferMode
|
||||
public enum TransferFormat
|
||||
{
|
||||
Binary = 0x01,
|
||||
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.
|
||||
|
||||
using System;
|
||||
|
|
@ -85,6 +85,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly Action<ILogger, string, string, Exception> _connectionStateChanged =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(25, "ConnectionStateChanged"), "Connection state changed from {previousState} to {newState}.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportNotSupported =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(26, "TransportNotSupported"), "Skipping transport {transportName} because it is not supported by this client.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, Exception> _transportDoesNotSupportTransferFormat =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(27, "TransportDoesNotSupportTransferFormat"), "Skipping transport {transportName} because it does not support the requested transfer format '{transferFormat}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportDisabledByClient =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(28, "TransportDisabledByClient"), "Skipping transport {transportName} because it was disabled by the client.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportFailed =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {transportName} because it failed to initialize.");
|
||||
|
||||
public static void HttpConnectionStarting(ILogger logger)
|
||||
{
|
||||
_httpConnectionStarting(logger, null);
|
||||
|
|
@ -218,6 +230,35 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
_errorDuringClosedEvent(logger, exception);
|
||||
}
|
||||
|
||||
public static void TransportNotSupported(ILogger logger, string transport)
|
||||
{
|
||||
_transportNotSupported(logger, transport, null);
|
||||
}
|
||||
|
||||
public static void TransportDoesNotSupportTransferFormat(ILogger logger, TransportType transport, TransferFormat transferFormat)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_transportDoesNotSupportTransferFormat(logger, transport.ToString(), transferFormat.ToString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TransportDisabledByClient(ILogger logger, TransportType transport)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_transportDisabledByClient(logger, transport.ToString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TransportFailed(ILogger logger, TransportType transport, Exception ex)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_transportFailed(logger, transport.ToString(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ using System.Buffers;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Features;
|
||||
using Microsoft.AspNetCore.Sockets.Http.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -47,9 +47,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private PipeWriter Output => _transportChannel.Output;
|
||||
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
|
||||
private readonly TransportType _requestedTransportType = TransportType.All;
|
||||
private TransportType _serverTransports = TransportType.All;
|
||||
// The order of the transports here is the order determines the fallback order.
|
||||
private static readonly TransportType[] AllTransports = new[]{ TransportType.WebSockets, TransportType.ServerSentEvents, TransportType.LongPolling };
|
||||
private readonly ConnectionLogScope _logScope;
|
||||
private readonly IDisposable _scopeDisposable;
|
||||
|
||||
|
|
@ -153,9 +150,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_scopeDisposable = _logger.BeginScope(_logScope);
|
||||
}
|
||||
|
||||
public async Task StartAsync() => await StartAsyncCore().ForceAsync();
|
||||
public Task StartAsync() => StartAsync(TransferFormat.Binary);
|
||||
public async Task StartAsync(TransferFormat transferFormat) => await StartAsyncCore(transferFormat).ForceAsync();
|
||||
|
||||
private Task StartAsyncCore()
|
||||
private Task StartAsyncCore(TransferFormat transferFormat)
|
||||
{
|
||||
if (ChangeState(from: ConnectionState.Disconnected, to: ConnectionState.Connecting) != ConnectionState.Disconnected)
|
||||
{
|
||||
|
|
@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_eventQueue = new TaskQueue();
|
||||
|
||||
StartAsyncInternal()
|
||||
StartAsyncInternal(transferFormat)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
var abortException = _abortException;
|
||||
|
|
@ -195,7 +193,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private async Task StartAsyncInternal()
|
||||
private async Task StartAsyncInternal(TransferFormat transferFormat)
|
||||
{
|
||||
Log.HttpConnectionStarting(_logger);
|
||||
|
||||
|
|
@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
if (_requestedTransportType == TransportType.WebSockets)
|
||||
{
|
||||
Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
|
||||
await StartTransport(connectUrl, _requestedTransportType);
|
||||
await StartTransport(connectUrl, _requestedTransportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -219,14 +217,32 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
}
|
||||
|
||||
// This should only need to happen once
|
||||
_serverTransports = GetAvailableServerTransports(negotiationResponse);
|
||||
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
|
||||
foreach (var transport in AllTransports)
|
||||
// We're going to search for the transfer format as a string because we don't want to parse
|
||||
// all the transfer formats in the negotiation response, and we want to allow transfer formats
|
||||
// we don't understand in the negotiate response.
|
||||
var transferFormatString = transferFormat.ToString();
|
||||
|
||||
foreach (var transport in negotiationResponse.AvailableTransports)
|
||||
{
|
||||
if (!Enum.TryParse<TransportType>(transport.Transport, out var transportType))
|
||||
{
|
||||
Log.TransportNotSupported(_logger, transport.Transport);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ((transport & _serverTransports & _requestedTransportType) != 0)
|
||||
if ((transportType & _requestedTransportType) == 0)
|
||||
{
|
||||
Log.TransportDisabledByClient(_logger, transportType);
|
||||
}
|
||||
else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal))
|
||||
{
|
||||
Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The negotiation response gets cleared in the fallback scenario.
|
||||
if (negotiationResponse == null)
|
||||
|
|
@ -235,13 +251,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
}
|
||||
|
||||
Log.StartingTransport(_logger, transport, connectUrl);
|
||||
await StartTransport(connectUrl, transport);
|
||||
Log.StartingTransport(_logger, transportType, connectUrl);
|
||||
await StartTransport(connectUrl, transportType, transferFormat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.TransportFailed(_logger, transportType, ex);
|
||||
// Try the next transport
|
||||
// Clear the negotiation response so we know to re-negotiate.
|
||||
negotiationResponse = null;
|
||||
|
|
@ -382,22 +399,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private TransportType GetAvailableServerTransports(NegotiationResponse negotiationResponse)
|
||||
{
|
||||
if (negotiationResponse.AvailableTransports == null)
|
||||
{
|
||||
throw new FormatException("No transports returned in negotiation response.");
|
||||
}
|
||||
|
||||
var availableServerTransports = (TransportType)0;
|
||||
foreach (var t in negotiationResponse.AvailableTransports)
|
||||
{
|
||||
availableServerTransports |= t;
|
||||
}
|
||||
|
||||
return availableServerTransports;
|
||||
}
|
||||
|
||||
private static Uri CreateConnectUrl(Uri url, string connectionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionId))
|
||||
|
|
@ -408,9 +409,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
return Utils.AppendQueryString(url, "id=" + connectionId);
|
||||
}
|
||||
|
||||
private async Task StartTransport(Uri connectUrl, TransportType transportType)
|
||||
private async Task StartTransport(Uri connectUrl, TransportType transportType, TransferFormat transferFormat)
|
||||
{
|
||||
|
||||
var options = new PipeOptions(writerScheduler: PipeScheduler.Inline, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
|
||||
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||
_transportChannel = pair.Transport;
|
||||
|
|
@ -419,15 +419,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
// Start the transport, giving it one end of the pipeline
|
||||
try
|
||||
{
|
||||
await _transport.StartAsync(connectUrl, pair.Application, GetTransferMode(), this);
|
||||
|
||||
// actual transfer mode can differ from the one that was requested so set it on the feature
|
||||
if (!_transport.Mode.HasValue)
|
||||
{
|
||||
// This can happen with custom transports so it should be an exception, not an assert.
|
||||
throw new InvalidOperationException("Transport was expected to set the Mode property after StartAsync, but it has not been set.");
|
||||
}
|
||||
SetTransferMode(_transport.Mode.Value);
|
||||
await _transport.StartAsync(connectUrl, pair.Application, transferFormat, this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -437,29 +429,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
}
|
||||
}
|
||||
|
||||
private TransferMode GetTransferMode()
|
||||
{
|
||||
var transferModeFeature = Features.Get<ITransferModeFeature>();
|
||||
if (transferModeFeature == null)
|
||||
{
|
||||
return TransferMode.Text;
|
||||
}
|
||||
|
||||
return transferModeFeature.TransferMode;
|
||||
}
|
||||
|
||||
private void SetTransferMode(TransferMode transferMode)
|
||||
{
|
||||
var transferModeFeature = Features.Get<ITransferModeFeature>();
|
||||
if (transferModeFeature == null)
|
||||
{
|
||||
transferModeFeature = new TransferModeFeature();
|
||||
Features.Set(transferModeFeature);
|
||||
}
|
||||
|
||||
transferModeFeature.TransferMode = transferMode;
|
||||
}
|
||||
|
||||
private async Task ReceiveAsync()
|
||||
{
|
||||
try
|
||||
|
|
@ -753,7 +722,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private class NegotiationResponse
|
||||
{
|
||||
public string ConnectionId { get; set; }
|
||||
public TransportType[] AvailableTransports { get; set; }
|
||||
public AvailableTransport[] AvailableTransports { get; set; }
|
||||
}
|
||||
|
||||
private class AvailableTransport
|
||||
{
|
||||
public string Transport { get; set; }
|
||||
public string[] TransferFormats { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
public interface ITransport
|
||||
{
|
||||
Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection);
|
||||
Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection);
|
||||
Task StopAsync();
|
||||
TransferMode? Mode { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
|
||||
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _transportStopped =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
|
||||
|
|
@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
// EventIds 100 - 106 used in SendUtils
|
||||
|
||||
public static void StartTransport(ILogger logger, TransferMode transferMode)
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
|
||||
{
|
||||
_startTransport(logger, transferMode, null);
|
||||
_startTransport(logger, transferFormat, null);
|
||||
}
|
||||
|
||||
public static void TransportStopped(ILogger logger, Exception exception)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
public Task Running { get; private set; } = Task.CompletedTask;
|
||||
|
||||
public TransferMode? Mode { get; private set; }
|
||||
|
||||
public LongPollingTransport(HttpClient httpClient)
|
||||
: this(httpClient, null, null)
|
||||
{ }
|
||||
|
|
@ -40,19 +38,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
|
||||
if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text)
|
||||
{
|
||||
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
|
||||
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
|
||||
}
|
||||
|
||||
connection.Features.Set<IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(_httpClient.Timeout));
|
||||
|
||||
_application = application;
|
||||
Mode = requestedTransferMode;
|
||||
|
||||
Log.StartTransport(_logger, Mode.Value);
|
||||
Log.StartTransport(_logger, transferFormat);
|
||||
|
||||
// Start sending and polling (ask for binary if the server supports it)
|
||||
_poller = Poll(url, _transportCts.Token);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
|
||||
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _transportStopped =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
|
||||
|
|
@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
// EventIds 100 - 106 used in SendUtils
|
||||
|
||||
public static void StartTransport(ILogger logger, TransferMode transferMode)
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
|
||||
{
|
||||
_startTransport(logger, transferMode, null);
|
||||
_startTransport(logger, transferFormat, null);
|
||||
}
|
||||
|
||||
public static void TransportStopped(ILogger logger, Exception exception)
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
public Task Running { get; private set; } = Task.CompletedTask;
|
||||
|
||||
public TransferMode? Mode { get; private set; }
|
||||
|
||||
public ServerSentEventsTransport(HttpClient httpClient)
|
||||
: this(httpClient, null, null)
|
||||
{ }
|
||||
|
|
@ -44,17 +42,16 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>();
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
|
||||
if (transferFormat != TransferFormat.Text)
|
||||
{
|
||||
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
|
||||
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
|
||||
}
|
||||
|
||||
_application = application;
|
||||
Mode = TransferMode.Text; // Server Sent Events is a text only transport
|
||||
|
||||
Log.StartTransport(_logger, Mode.Value);
|
||||
Log.StartTransport(_logger, transferFormat);
|
||||
|
||||
var sendTask = SendUtils.SendMessages(url, _application, _httpClient, _httpOptions, _transportCts, _logger);
|
||||
var receiveTask = OpenConnection(_application, url, _transportCts.Token);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
|
|
@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, TransferMode, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferMode>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}.");
|
||||
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _transportStopped =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
|
||||
|
|
@ -65,9 +65,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly Action<ILogger, Exception> _cancelMessage =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(18, "CancelMessage"), "Canceled passing message to application.");
|
||||
|
||||
public static void StartTransport(ILogger logger, TransferMode transferMode)
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
|
||||
{
|
||||
_startTransport(logger, transferMode, null);
|
||||
_startTransport(logger, transferFormat, null);
|
||||
}
|
||||
|
||||
public static void TransportStopped(ILogger logger, Exception exception)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private readonly ClientWebSocket _webSocket;
|
||||
private IDuplexPipe _application;
|
||||
private WebSocketMessageType _webSocketMessageType;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TimeSpan _closeTimeout;
|
||||
private volatile bool _aborted;
|
||||
|
||||
public Task Running { get; private set; } = Task.CompletedTask;
|
||||
|
||||
public TransferMode? Mode { get; private set; }
|
||||
|
||||
public WebSocketsTransport()
|
||||
: this(null, null)
|
||||
{
|
||||
|
|
@ -87,7 +86,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>();
|
||||
}
|
||||
|
||||
public async Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
|
||||
public async Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
if (url == null)
|
||||
{
|
||||
|
|
@ -99,15 +98,18 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
throw new ArgumentNullException(nameof(application));
|
||||
}
|
||||
|
||||
if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text)
|
||||
if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text)
|
||||
{
|
||||
throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode));
|
||||
throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
|
||||
}
|
||||
|
||||
_application = application;
|
||||
Mode = requestedTransferMode;
|
||||
_webSocketMessageType = transferFormat == TransferFormat.Binary
|
||||
? WebSocketMessageType.Binary
|
||||
: WebSocketMessageType.Text;
|
||||
|
||||
Log.StartTransport(_logger, Mode.Value);
|
||||
|
||||
Log.StartTransport(_logger, transferFormat);
|
||||
|
||||
await Connect(url);
|
||||
|
||||
|
|
@ -243,11 +245,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
private async Task StartSending(WebSocket socket)
|
||||
{
|
||||
var webSocketMessageType =
|
||||
Mode == TransferMode.Binary
|
||||
? WebSocketMessageType.Binary
|
||||
: WebSocketMessageType.Text;
|
||||
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
|
|
@ -274,7 +271,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
if (WebSocketCanSend(socket))
|
||||
{
|
||||
await socket.SendAsync(buffer, webSocketMessageType);
|
||||
await socket.SendAsync(buffer, _webSocketMessageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
Log.EstablishedConnection(_logger);
|
||||
|
||||
// ServerSentEvents is a text protocol only
|
||||
connection.TransportCapabilities = TransferMode.Text;
|
||||
connection.SupportedFormats = TransferFormat.Text;
|
||||
|
||||
// We only need to provide the Input channel since writing to the application is handled through /send.
|
||||
var sse = new ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory);
|
||||
|
|
@ -389,15 +389,15 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
jsonWriter.WriteStartArray();
|
||||
if ((options.Transports & TransportType.WebSockets) != 0)
|
||||
{
|
||||
jsonWriter.WriteValue(nameof(TransportType.WebSockets));
|
||||
WriteTransport(jsonWriter, nameof(TransportType.WebSockets), TransferFormat.Text | TransferFormat.Binary);
|
||||
}
|
||||
if ((options.Transports & TransportType.ServerSentEvents) != 0)
|
||||
{
|
||||
jsonWriter.WriteValue(nameof(TransportType.ServerSentEvents));
|
||||
WriteTransport(jsonWriter, nameof(TransportType.ServerSentEvents), TransferFormat.Text);
|
||||
}
|
||||
if ((options.Transports & TransportType.LongPolling) != 0)
|
||||
{
|
||||
jsonWriter.WriteValue(nameof(TransportType.LongPolling));
|
||||
WriteTransport(jsonWriter, nameof(TransportType.LongPolling), TransferFormat.Text | TransferFormat.Binary);
|
||||
}
|
||||
jsonWriter.WriteEndArray();
|
||||
jsonWriter.WriteEndObject();
|
||||
|
|
@ -406,6 +406,27 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void WriteTransport(JsonWriter writer, string transportName, TransferFormat supportedTransferFormats)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("transport");
|
||||
writer.WriteValue(transportName);
|
||||
writer.WritePropertyName("transferFormats");
|
||||
writer.WriteStartArray();
|
||||
if ((supportedTransferFormats & TransferFormat.Binary) != 0)
|
||||
{
|
||||
writer.WriteValue(nameof(TransferFormat.Binary));
|
||||
}
|
||||
|
||||
if ((supportedTransferFormats & TransferFormat.Text) != 0)
|
||||
{
|
||||
writer.WriteValue(nameof(TransferFormat.Text));
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static string GetConnectionId(HttpContext context) => context.Request.Query["id"];
|
||||
|
||||
private async Task ProcessSend(HttpContext context)
|
||||
|
|
@ -477,9 +498,6 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
connection.User = context.User;
|
||||
connection.SetHttpContext(context);
|
||||
|
||||
// this is the default setting which should be overwritten by transports that have different capabilities (e.g. SSE)
|
||||
connection.TransportCapabilities = TransferMode.Binary | TransferMode.Text;
|
||||
|
||||
// Set the Connection ID on the logging scope so that logs from now on will have the
|
||||
// Connection ID metadata set.
|
||||
logScope.ConnectionId = connection.ConnectionId;
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
|
|||
{
|
||||
Log.SendPayload(_logger, buffer.Length);
|
||||
|
||||
var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary
|
||||
var webSocketMessageType = (_connection.ActiveFormat == TransferFormat.Binary
|
||||
? WebSocketMessageType.Binary
|
||||
: WebSocketMessageType.Text);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Security.Claims;
|
||||
|
|
@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
IConnectionTransportFeature,
|
||||
IConnectionUserFeature,
|
||||
IConnectionHeartbeatFeature,
|
||||
ITransferModeFeature
|
||||
ITransferFormatFeature
|
||||
{
|
||||
private List<(Action<object> handler, object state)> _heartbeatHandlers = new List<(Action<object> handler, object state)>();
|
||||
|
||||
|
|
@ -37,14 +36,18 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
ConnectionId = id;
|
||||
LastSeenUtc = DateTime.UtcNow;
|
||||
|
||||
// The default behavior is that both formats are supported.
|
||||
SupportedFormats = TransferFormat.Binary | TransferFormat.Text;
|
||||
ActiveFormat = TransferFormat.Text;
|
||||
|
||||
// PERF: This type could just implement IFeatureCollection
|
||||
Features = new FeatureCollection();
|
||||
Features.Set<IConnectionUserFeature>(this);
|
||||
Features.Set<IConnectionMetadataFeature>(this);
|
||||
Features.Set<IConnectionIdFeature>(this);
|
||||
Features.Set<IConnectionTransportFeature>(this);
|
||||
Features.Set<ITransferModeFeature>(this);
|
||||
Features.Set<IConnectionHeartbeatFeature>(this);
|
||||
Features.Set<ITransferFormatFeature>(this);
|
||||
}
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
|
@ -71,9 +74,9 @@ namespace Microsoft.AspNetCore.Sockets
|
|||
|
||||
public override IDuplexPipe Transport { get; set; }
|
||||
|
||||
public TransferMode TransportCapabilities { get; set; }
|
||||
public TransferFormat SupportedFormats { get; set; }
|
||||
|
||||
public TransferMode TransferMode { get; set; }
|
||||
public TransferFormat ActiveFormat { get; set; }
|
||||
|
||||
public void OnHeartbeat(Action<object> action, object state)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
||||
var result = await connection.InvokeAsync<string>("HelloWorld").OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>(nameof(TestHub.HelloWorld)).OrTimeout();
|
||||
|
||||
Assert.Equal("Hello World!", result);
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
||||
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
|
||||
|
||||
Assert.Equal(originalMessage, result);
|
||||
}
|
||||
|
|
@ -115,11 +115,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
|
||||
Assert.Equal(originalMessage, result);
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await connection.StartAsync().OrTimeout();
|
||||
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||
result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
|
||||
Assert.Equal(originalMessage, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
|
||||
Assert.Equal(originalMessage, result);
|
||||
|
||||
logger.LogInformation("Stopping connection");
|
||||
|
|
@ -169,7 +169,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
await restartTcs.Task.OrTimeout();
|
||||
logger.LogInformation("Reconnection complete");
|
||||
|
||||
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||
result = await connection.InvokeAsync<string>(nameof(TestHub.Echo), originalMessage).OrTimeout();
|
||||
Assert.Equal(originalMessage, result);
|
||||
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
||||
var result = await connection.InvokeAsync<string>("echo", originalMessage).OrTimeout();
|
||||
var result = await connection.InvokeAsync<string>(nameof(TestHub.Echo).ToLowerInvariant(), originalMessage).OrTimeout();
|
||||
|
||||
Assert.Equal(originalMessage, result);
|
||||
}
|
||||
|
|
@ -677,7 +677,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
var message = await hubConnection.InvokeAsync<string>("Echo", "Hello, World!").OrTimeout();
|
||||
var message = await hubConnection.InvokeAsync<string>(nameof(TestHub.Echo), "Hello, World!").OrTimeout();
|
||||
Assert.Equal("Hello, World!", message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -708,7 +708,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
var headerValues = await hubConnection.InvokeAsync<string[]>("GetHeaderValues", new object[] { new[] { "X-test", "X-42" } }).OrTimeout();
|
||||
var headerValues = await hubConnection.InvokeAsync<string[]>(nameof(TestHub.GetHeaderValues), new object[] { new[] { "X-test", "X-42" } }).OrTimeout();
|
||||
Assert.Equal(new[] { "42", "test" }, headerValues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -742,7 +742,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
var cookieValue = await hubConnection.InvokeAsync<string>("GetCookieValue", new object[] { "Foo" }).OrTimeout();
|
||||
var cookieValue = await hubConnection.InvokeAsync<string>(nameof(TestHub.GetCookieValue), new object[] { "Foo" }).OrTimeout();
|
||||
Assert.Equal("Bar", cookieValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -772,7 +772,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var features = await hubConnection.InvokeAsync<object[]>("GetIHttpConnectionFeatureProperties").OrTimeout();
|
||||
var features = await hubConnection.InvokeAsync<object[]>(nameof(TestHub.GetIHttpConnectionFeatureProperties)).OrTimeout();
|
||||
var localPort = (Int64)features[0];
|
||||
var remotePort = (Int64)features[1];
|
||||
var localIP = (string)features[2];
|
||||
|
|
@ -795,23 +795,56 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NegotiationSkipsServerSentEventsWhenUsingBinaryProtocol()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
var hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(_serverFixture.Url + "/default-nowebsockets")
|
||||
.WithHubProtocol(new MessagePackHubProtocol())
|
||||
.WithLoggerFactory(loggerFactory)
|
||||
.Build();
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var transport = await hubConnection.InvokeAsync<TransportType>(nameof(TestHub.GetActiveTransportName)).OrTimeout();
|
||||
Assert.Equal(TransportType.LongPolling, transport);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> HubProtocolsAndTransportsAndHubPaths
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var protocol in HubProtocols)
|
||||
{
|
||||
foreach (var transport in TransportTypes().SelectMany(t => t))
|
||||
foreach (var transport in TransportTypes().SelectMany(t => t).Cast<TransportType>())
|
||||
{
|
||||
foreach (var hubPath in HubPaths)
|
||||
{
|
||||
yield return new object[] { protocol, transport, hubPath };
|
||||
if (!(protocol is MessagePackHubProtocol) || transport != TransportType.ServerSentEvents)
|
||||
{
|
||||
yield return new object[] { protocol, transport, hubPath };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests.
|
||||
public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" };
|
||||
|
||||
public static IEnumerable<IHubProtocol> HubProtocols =>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Threading.Channels;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||
{
|
||||
|
|
@ -57,6 +58,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetActiveTransportName()
|
||||
{
|
||||
return Context.Connection.Metadata[ConnectionMetadataNames.Transport].ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class DynamicTestHub : DynamicHub
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Security.Claims;
|
|||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
routes.MapHub<DynamicTestHub>("/dynamic");
|
||||
routes.MapHub<TestHubT>("/hubT");
|
||||
routes.MapHub<HubWithAuthorization>("/authorizedhub");
|
||||
routes.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = TransportType.LongPolling | TransportType.ServerSentEvents);
|
||||
});
|
||||
|
||||
app.Run(async (context) =>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
|
|
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return WithConnectionAsync(CreateConnection(), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Stop normally
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
|
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
|
|
@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(out var syncPoint))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
|
|
@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
// Wait to reach the first sync point
|
||||
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message);
|
||||
|
||||
// Release the sync point and wait for close to complete
|
||||
|
|
@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
Assert.Same(expected, actual);
|
||||
|
||||
// We can start now
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// And we can stop without getting the abort exception.
|
||||
await connection.StopAsync().OrTimeout();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
|
@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.StartAsync().OrTimeout());
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
}
|
||||
|
|
@ -47,11 +48,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.StartAsync().OrTimeout());
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
|
|
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
await connection.DisposeAsync();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.StartAsync().OrTimeout());
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
|
|
@ -91,7 +92,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
// Start the connection and wait for the transport to start up.
|
||||
var startTask = connection.StartAsync();
|
||||
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// While the transport is starting, dispose the connection
|
||||
|
|
@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
Assert.Equal(0, startCounter);
|
||||
await connection.StartAsync();
|
||||
await connection.StartAsync(TransferFormat.Text);
|
||||
Assert.Equal(passThreshold, startCounter);
|
||||
});
|
||||
}
|
||||
|
|
@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
transport: new TestTransport(onTransportStart: OnTransportStart)),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync());
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text));
|
||||
Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message);
|
||||
Assert.Equal(3, startCounter);
|
||||
});
|
||||
|
|
@ -180,9 +181,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
// Start and wait for the transport to start up.
|
||||
var startTask = connection.StartAsync();
|
||||
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Stop the connection while it's starting
|
||||
|
|
@ -223,7 +224,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
});
|
||||
|
|
@ -257,14 +258,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(httpHandler, loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.SendAsync(new byte[] { 0x42 }).OrTimeout();
|
||||
|
||||
// Wait for the connection to close, because the send failed.
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout());
|
||||
|
||||
// Start it up again
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -281,7 +282,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Stop the connection
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
|
@ -299,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
await disposeTask.OrTimeout();
|
||||
|
||||
// We should be disposed and thus unable to restart.
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
}
|
||||
|
|
@ -314,7 +315,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
|
|
@ -329,7 +330,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
});
|
||||
|
|
@ -346,7 +347,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(transport: testTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
testTransport.Application.Output.Complete(expected);
|
||||
var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout());
|
||||
Assert.Same(expected, actual);
|
||||
|
|
@ -384,7 +385,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
// Start the transport
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.False(longPollingTransport.Running.IsCompleted, "Expected that the transport would still be running");
|
||||
|
||||
// Stop the connection, and we should stop the transport
|
||||
|
|
|
|||
|
|
@ -14,26 +14,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
public partial class HttpConnectionTests
|
||||
{
|
||||
private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null)
|
||||
private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null)
|
||||
{
|
||||
var httpOptions = new HttpOptions()
|
||||
{
|
||||
HttpMessageHandler = (httpMessageHandler) => httpHandler ?? TestHttpMessageHandler.CreateDefault(),
|
||||
};
|
||||
|
||||
return CreateConnection(httpOptions, loggerFactory, url, transport);
|
||||
return CreateConnection(httpOptions, loggerFactory, url, transport, transportFactory);
|
||||
}
|
||||
|
||||
private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null)
|
||||
private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null)
|
||||
{
|
||||
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||
var uri = new Uri(url ?? "http://fakeuri.org/");
|
||||
|
||||
var connection = (transport != null) ?
|
||||
new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions) :
|
||||
new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions);
|
||||
|
||||
return connection;
|
||||
if (transportFactory != null)
|
||||
{
|
||||
return new HttpConnection(uri, transportFactory, loggerFactory, httpOptions);
|
||||
}
|
||||
else if (transport != null)
|
||||
{
|
||||
return new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WithConnectionAsync(HttpConnection connection, Func<HttpConnection, Task, Task> body)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ using System;
|
|||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
using TransportType = Microsoft.AspNetCore.Sockets.TransportType;
|
||||
|
|
@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
[Fact]
|
||||
public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports()
|
||||
{
|
||||
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(transportTypes: null), "No transports returned in negotiation response.");
|
||||
return RunInvalidNegotiateResponseTest<InvalidOperationException>(ResponseUtils.CreateNegotiationContent(transportTypes: 0), "Unable to connect to the server with any of the available transports.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -67,12 +71,104 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(testHttpHandler, url: requestedUrl),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
|
||||
Assert.Equal(expectedNegotiate, await negotiateUrlTcs.Task.OrTimeout());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
|
||||
testHttpHandler.OnNegotiate((request, cancellationToken) =>
|
||||
{
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
|
||||
JsonConvert.SerializeObject(new
|
||||
{
|
||||
connectionId = "00000000-0000-0000-0000-000000000000",
|
||||
availableTransports = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
transport = "QuantumEntanglement",
|
||||
transferFormats = new string[] { "Qbits" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "CarrierPigeon",
|
||||
transferFormats = new string[] { "Text" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "LongPolling",
|
||||
transferFormats = new string[] { "Text", "Binary" }
|
||||
},
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);
|
||||
|
||||
transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling))
|
||||
.Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferFormat()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
|
||||
testHttpHandler.OnNegotiate((request, cancellationToken) =>
|
||||
{
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
|
||||
JsonConvert.SerializeObject(new
|
||||
{
|
||||
connectionId = "00000000-0000-0000-0000-000000000000",
|
||||
availableTransports = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
transport = "WebSockets",
|
||||
transferFormats = new string[] { "Qbits" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "ServerSentEvents",
|
||||
transferFormats = new string[] { "Text" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "LongPolling",
|
||||
transferFormats = new string[] { "Text", "Binary" }
|
||||
},
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);
|
||||
|
||||
transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling))
|
||||
.Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunInvalidNegotiateResponseTest<TException>(string negotiatePayload, string expectedExceptionMessage) where TException : Exception
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
||||
|
|
@ -84,7 +180,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
async (connection, closed) =>
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<TException>(
|
||||
() => connection.StartAsync().OrTimeout());
|
||||
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
||||
Assert.Equal(expectedExceptionMessage, exception.Message);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Net;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
|
|
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
});
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
Assert.True(receivedRaised);
|
||||
});
|
||||
|
|
@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
Assert.True(receivedRaised);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
await connection.SendAsync(data).OrTimeout();
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
|
|
@ -82,7 +83,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
|
|
@ -114,7 +115,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
await connection.SendAsync(new byte[] { 0 }).OrTimeout();
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var onReceived = new SyncPoint();
|
||||
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// This will trigger the received callback
|
||||
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
|
||||
|
|
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
|
||||
|
||||
logger.LogInformation("Starting connection");
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
logger.LogInformation("Started connection");
|
||||
|
||||
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
|
||||
|
|
@ -131,23 +131,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsyncSetsTransferModeFeature()
|
||||
{
|
||||
var testTransport = new TestTransport(transferMode: TransferMode.Binary);
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: testTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
Assert.Null(connection.Features.Get<ITransferModeFeature>());
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
||||
var transferModeFeature = connection.Features.Get<ITransferModeFeature>();
|
||||
Assert.NotNull(transferModeFeature);
|
||||
Assert.Equal(TransferMode.Binary, transferModeFeature.TransferMode);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpOptionsSetOntoHttpClientHandler()
|
||||
{
|
||||
|
|
@ -180,7 +163,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(httpOptions, url: "http://fakeuri.org/"),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
|
||||
Assert.NotNull(httpClientHandler);
|
||||
|
|
|
|||
|
|
@ -324,76 +324,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MessagesEncodedWhenUsingBinaryProtocolOverTextTransport()
|
||||
{
|
||||
var connection = new TestConnection(TransferMode.Text);
|
||||
|
||||
var hubConnection = new HubConnection(connection,
|
||||
new MessagePackHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
await hubConnection.SendAsync("MyMethod", 42).OrTimeout();
|
||||
|
||||
await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
// The message is in the following format `size:payload;`
|
||||
var parts = invokeMessage.Split(':');
|
||||
Assert.Equal(2, parts.Length);
|
||||
Assert.True(int.TryParse(parts[0], out var payloadSize));
|
||||
Assert.Equal(payloadSize, parts[1].Length - 1);
|
||||
Assert.EndsWith(";", parts[1]);
|
||||
|
||||
// this throws if the message is not a valid base64 string
|
||||
Convert.FromBase64String(parts[1].Substring(0, payloadSize));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MessagesDecodedWhenUsingBinaryProtocolOverTextTransport()
|
||||
{
|
||||
var connection = new TestConnection(TransferMode.Text);
|
||||
var hubConnection = new HubConnection(connection,
|
||||
new MessagePackHubProtocol(), new LoggerFactory());
|
||||
|
||||
var invocationTcs = new TaskCompletionSource<int>();
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
hubConnection.On<int>("MyMethod", result => invocationTcs.SetResult(result));
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
new MessagePackHubProtocol()
|
||||
.WriteMessage(new InvocationMessage(null, "MyMethod", null, 42), ms);
|
||||
|
||||
var invokeMessage = Convert.ToBase64String(ms.ToArray());
|
||||
var payloadSize = invokeMessage.Length.ToString(CultureInfo.InvariantCulture);
|
||||
var message = $"{payloadSize}:{invokeMessage};";
|
||||
|
||||
connection.ReceivedMessages.TryWrite(Encoding.UTF8.GetBytes(message));
|
||||
}
|
||||
|
||||
Assert.Equal(42, await invocationTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcceptsPingMessages()
|
||||
{
|
||||
var connection = new TestConnection(TransferMode.Text);
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection,
|
||||
new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
|
@ -21,12 +22,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
public async Task StartAsyncCallsConnectionStart()
|
||||
{
|
||||
var connection = new Mock<IConnection>();
|
||||
var protocol = new Mock<IHubProtocol>();
|
||||
protocol.SetupGet(p => p.TransferFormat).Returns(TransferFormat.Text);
|
||||
connection.SetupGet(p => p.Features).Returns(new FeatureCollection());
|
||||
connection.Setup(m => m.StartAsync()).Returns(Task.CompletedTask).Verifiable();
|
||||
var hubConnection = new HubConnection(connection.Object, Mock.Of<IHubProtocol>(), null);
|
||||
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Returns(Task.CompletedTask).Verifiable();
|
||||
var hubConnection = new HubConnection(connection.Object, protocol.Object, null);
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
connection.Verify(c => c.StartAsync(), Times.Once());
|
||||
connection.Verify(c => c.StartAsync(TransferFormat.Text), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -34,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
var connection = new Mock<IConnection>();
|
||||
connection.Setup(m => m.Features).Returns(new FeatureCollection());
|
||||
connection.Setup(m => m.StartAsync()).Verifiable();
|
||||
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Verifiable();
|
||||
var hubConnection = new HubConnection(connection.Object, Mock.Of<IHubProtocol>(), null);
|
||||
await hubConnection.DisposeAsync();
|
||||
|
||||
|
|
@ -249,7 +252,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
public string Name => "MockHubProtocol";
|
||||
|
||||
public ProtocolType Type => ProtocolType.Binary;
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
transportActiveTask = longPollingTransport.Running;
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
await longPollingTransport.Running.OrTimeout();
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
var data = await pair.Transport.Input.ReadAllAsync().OrTimeout();
|
||||
await longPollingTransport.Running.OrTimeout();
|
||||
|
|
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
|
|
@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
pair.Transport.Output.Complete();
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
// Start the transport
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
// Wait for the transport to finish
|
||||
await longPollingTransport.Running.OrTimeout();
|
||||
|
|
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("World"));
|
||||
|
||||
// Start the transport
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
pair.Transport.Output.Complete();
|
||||
|
||||
|
|
@ -360,9 +360,9 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransferMode.Binary)]
|
||||
[InlineData(TransferMode.Text)]
|
||||
public async Task LongPollingTransportSetsTransferMode(TransferMode transferMode)
|
||||
[InlineData(TransferFormat.Binary)]
|
||||
[InlineData(TransferFormat.Text)]
|
||||
public async Task LongPollingTransportSetsTransferFormat(TransferFormat transferFormat)
|
||||
{
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
mockHttpHandler.Protected()
|
||||
|
|
@ -381,9 +381,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
Assert.Null(longPollingTransport.Mode);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: new TestConnection());
|
||||
Assert.Equal(transferMode, longPollingTransport.Mode);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: new TestConnection());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -415,8 +413,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
Assert.Null(longPollingTransport.Mode);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: new TestConnection());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -436,8 +433,10 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LongPollingTransportThrowsForInvalidTransferMode()
|
||||
[Theory]
|
||||
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
|
||||
[InlineData((TransferFormat)42)] // Unexpected value
|
||||
public async Task LongPollingTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
|
||||
{
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
mockHttpHandler.Protected()
|
||||
|
|
@ -452,10 +451,10 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
{
|
||||
var longPollingTransport = new LongPollingTransport(httpClient);
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: new TestConnection()));
|
||||
longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: new TestConnection()));
|
||||
|
||||
Assert.Contains("Invalid transfer mode.", exception.Message);
|
||||
Assert.Equal("requestedTransferMode", exception.ParamName);
|
||||
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
|
||||
Assert.Equal("transferFormat", exception.ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -488,7 +487,7 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
try
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection());
|
||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection());
|
||||
|
||||
var completedTask = await Task.WhenAny(completionTcs.Task, longPollingTransport.Running).OrTimeout();
|
||||
Assert.Equal(completionTcs.Task, completedTask);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Newtonsoft.Json;
|
||||
using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType;
|
||||
|
||||
namespace Microsoft.AspNetCore.Client.Tests
|
||||
|
|
@ -36,35 +37,36 @@ namespace Microsoft.AspNetCore.Client.Tests
|
|||
}
|
||||
|
||||
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
|
||||
SocketsTransportType? transportTypes = SocketsTransportType.All)
|
||||
SocketsTransportType transportTypes = SocketsTransportType.All)
|
||||
{
|
||||
var sb = new StringBuilder("{ ");
|
||||
if (connectionId != null)
|
||||
{
|
||||
sb.Append($"\"connectionId\": \"{connectionId}\",");
|
||||
}
|
||||
if (transportTypes != null)
|
||||
{
|
||||
sb.Append($"\"availableTransports\": [ ");
|
||||
if ((transportTypes & SocketsTransportType.WebSockets) == SocketsTransportType.WebSockets)
|
||||
{
|
||||
sb.Append($"\"{nameof(SocketsTransportType.WebSockets)}\",");
|
||||
}
|
||||
if ((transportTypes & SocketsTransportType.ServerSentEvents) == SocketsTransportType.ServerSentEvents)
|
||||
{
|
||||
sb.Append($"\"{nameof(SocketsTransportType.ServerSentEvents)}\",");
|
||||
}
|
||||
if ((transportTypes & SocketsTransportType.LongPolling) == SocketsTransportType.LongPolling)
|
||||
{
|
||||
sb.Append($"\"{nameof(SocketsTransportType.LongPolling)}\",");
|
||||
}
|
||||
sb.Length--;
|
||||
sb.Append("],");
|
||||
}
|
||||
sb.Length--;
|
||||
sb.Append("}");
|
||||
var availableTransports = new List<object>();
|
||||
|
||||
return sb.ToString();
|
||||
if ((transportTypes & SocketsTransportType.WebSockets) != 0)
|
||||
{
|
||||
availableTransports.Add(new
|
||||
{
|
||||
transport = nameof(SocketsTransportType.WebSockets),
|
||||
transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) }
|
||||
});
|
||||
}
|
||||
if ((transportTypes & SocketsTransportType.ServerSentEvents) != 0)
|
||||
{
|
||||
availableTransports.Add(new
|
||||
{
|
||||
transport = nameof(SocketsTransportType.ServerSentEvents),
|
||||
transferFormats = new[] { nameof(TransferFormat.Text) }
|
||||
});
|
||||
}
|
||||
if ((transportTypes & SocketsTransportType.LongPolling) != 0)
|
||||
{
|
||||
availableTransports.Add(new
|
||||
{
|
||||
transport = nameof(SocketsTransportType.LongPolling),
|
||||
transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) }
|
||||
});
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { connectionId, availableTransports });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var sseTransport = new ServerSentEventsTransport(httpClient);
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
await eventStreamTcs.Task.OrTimeout();
|
||||
await sseTransport.StopAsync().OrTimeout();
|
||||
|
|
@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
transportActiveTask = sseTransport.Running;
|
||||
Assert.False(transportActiveTask.IsCompleted);
|
||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<FormatException>(() => sseTransport.Running.OrTimeout());
|
||||
Assert.Equal("Incomplete message.", exception.Message);
|
||||
|
|
@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
await eventStreamTcs.Task;
|
||||
|
||||
await pair.Transport.Output.WriteAsync(new byte[] { 0x42 });
|
||||
|
|
@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
await eventStreamTcs.Task.OrTimeout();
|
||||
|
||||
pair.Transport.Output.Complete();
|
||||
|
|
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
|
||||
await sseTransport.StartAsync(
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
var message = await pair.Transport.Input.ReadSingleAsync().OrTimeout();
|
||||
Assert.Equal("3:abc", Encoding.ASCII.GetString(message));
|
||||
|
|
@ -276,10 +276,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransferMode.Text)]
|
||||
[InlineData(TransferMode.Binary)]
|
||||
public async Task SSETransportSetsTransferMode(TransferMode transferMode)
|
||||
[Fact]
|
||||
public async Task SSETransportDoesNotSupportBinary()
|
||||
{
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
mockHttpHandler.Protected()
|
||||
|
|
@ -293,12 +291,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||
{
|
||||
var sseTransport = new ServerSentEventsTransport(httpClient);
|
||||
Assert.Null(sseTransport.Mode);
|
||||
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
Assert.Equal(TransferMode.Text, sseTransport.Mode);
|
||||
await sseTransport.StopAsync().OrTimeout();
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(() => sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout());
|
||||
Assert.Equal($"The 'Binary' transfer format is not supported by this transport.{Environment.NewLine}Parameter name: transferFormat", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +318,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var sseTransport = new ServerSentEventsTransport(httpClient);
|
||||
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
await sseTransport.StopAsync().OrTimeout();
|
||||
}
|
||||
|
||||
|
|
@ -338,8 +334,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SSETransportThrowsForInvalidTransferMode()
|
||||
[Theory]
|
||||
[InlineData(TransferFormat.Binary)] // Binary not supported
|
||||
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
|
||||
[InlineData((TransferFormat)42)] // Unexpected value
|
||||
public async Task SSETransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
|
||||
{
|
||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||
mockHttpHandler.Protected()
|
||||
|
|
@ -354,10 +353,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
var sseTransport = new ServerSentEventsTransport(httpClient);
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: Mock.Of<IConnection>()));
|
||||
sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: Mock.Of<IConnection>()));
|
||||
|
||||
Assert.Contains("Invalid transfer mode.", exception.Message);
|
||||
Assert.Equal("requestedTransferMode", exception.ParamName);
|
||||
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
|
||||
Assert.Equal("transferFormat", exception.ParamName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource();
|
||||
private Task _receiveLoop;
|
||||
|
||||
private TransferMode? _transferMode;
|
||||
|
||||
public event Action<Exception> Closed;
|
||||
public Task Started => _started.Task;
|
||||
public Task Disposed => _disposed.Task;
|
||||
|
|
@ -44,9 +42,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||
|
||||
public TestConnection(TransferMode? transferMode = null)
|
||||
public TestConnection()
|
||||
{
|
||||
_transferMode = transferMode;
|
||||
_receiveLoop = ReceiveLoopAsync(_receiveShutdownToken.Token);
|
||||
}
|
||||
|
||||
|
|
@ -80,20 +77,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
throw new ObjectDisposedException("Unable to send message, underlying channel was closed");
|
||||
}
|
||||
|
||||
public Task StartAsync()
|
||||
public Task StartAsync(TransferFormat transferFormat)
|
||||
{
|
||||
if (_transferMode.HasValue)
|
||||
{
|
||||
var transferModeFeature = Features.Get<ITransferModeFeature>();
|
||||
if (transferModeFeature == null)
|
||||
{
|
||||
transferModeFeature = new TransferModeFeature();
|
||||
Features.Set(transferModeFeature);
|
||||
}
|
||||
|
||||
transferModeFeature.TransferMode = _transferMode.Value;
|
||||
}
|
||||
|
||||
_started.TrySetResult(null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
|
|
@ -12,18 +11,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
private readonly Func<Task> _stopHandler;
|
||||
private readonly Func<Task> _startHandler;
|
||||
|
||||
public TransferMode? Mode { get; }
|
||||
public TransferFormat? Format { get; }
|
||||
public IDuplexPipe Application { get; private set; }
|
||||
|
||||
public TestTransport(Func<Task> onTransportStop = null, Func<Task> onTransportStart = null, TransferMode transferMode = TransferMode.Text)
|
||||
public TestTransport(Func<Task> onTransportStop = null, Func<Task> onTransportStart = null, TransferFormat transferFormat = TransferFormat.Text)
|
||||
{
|
||||
_stopHandler = onTransportStop ?? new Func<Task>(() => Task.CompletedTask);
|
||||
_startHandler = onTransportStart ?? new Func<Task>(() => Task.CompletedTask);
|
||||
Mode = transferMode;
|
||||
Format = transferFormat;
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
if ((Format & transferFormat) == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"The '{transferFormat}' transfer format is not supported by this transport.");
|
||||
}
|
||||
Application = application;
|
||||
return _startHandler();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
{
|
||||
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 Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
|
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance)
|
||||
{
|
||||
ProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder())
|
||||
Protocol = new JsonHubProtocol()
|
||||
};
|
||||
}
|
||||
|
||||
public static Mock<HubConnectionContext> CreateMock(DefaultConnectionContext connection)
|
||||
{
|
||||
var mock = new Mock<HubConnectionContext>(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { CallBase = true };
|
||||
var readerWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder());
|
||||
mock.SetupGet(m => m.ProtocolReaderWriter).Returns(readerWriter);
|
||||
var protocol = new JsonHubProtocol();
|
||||
mock.SetupGet(m => m.Protocol).Returns(protocol);
|
||||
return mock;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
|
|
@ -9,7 +10,6 @@ using System.Security.Claims;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
public class TestClient : IDisposable
|
||||
{
|
||||
private static int _id;
|
||||
private readonly HubProtocolReaderWriter _protocolReaderWriter;
|
||||
private readonly IHubProtocol _protocol;
|
||||
private readonly IInvocationBinder _invocationBinder;
|
||||
private CancellationTokenSource _cts;
|
||||
private Queue<HubMessage> _messages = new Queue<HubMessage>();
|
||||
|
|
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
public DefaultConnectionContext Connection { get; }
|
||||
public Task Connected => ((TaskCompletionSource<bool>)Connection.Metadata["ConnectedTask"]).Task;
|
||||
|
||||
public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IDataEncoder dataEncoder = null, IInvocationBinder invocationBinder = null, bool addClaimId = false)
|
||||
public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, bool addClaimId = false)
|
||||
{
|
||||
var options = new PipeOptions(readerScheduler: synchronousCallbacks ? PipeScheduler.Inline : null);
|
||||
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||
|
|
@ -42,16 +42,14 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
Connection.Metadata["ConnectedTask"] = new TaskCompletionSource<bool>();
|
||||
|
||||
protocol = protocol ?? new JsonHubProtocol();
|
||||
dataEncoder = dataEncoder ?? new PassThroughEncoder();
|
||||
_protocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder);
|
||||
_protocol = protocol ?? new JsonHubProtocol();
|
||||
_invocationBinder = invocationBinder ?? new DefaultInvocationBinder();
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
NegotiationProtocol.WriteMessage(new NegotiationMessage(protocol.Name), memoryStream);
|
||||
NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream);
|
||||
Connection.Application.Output.WriteAsync(memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +141,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
public async Task<string> SendHubMessageAsync(HubMessage message)
|
||||
{
|
||||
var payload = _protocolReaderWriter.WriteMessage(message);
|
||||
var payload = _protocol.WriteToArray(message);
|
||||
|
||||
await Connection.Application.Output.WriteAsync(payload);
|
||||
return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null;
|
||||
}
|
||||
|
|
@ -201,7 +200,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
try
|
||||
{
|
||||
if (_protocolReaderWriter.ReadMessages(result.Buffer, _invocationBinder, out var messages, out consumed, out examined))
|
||||
var messages = new List<HubMessage>();
|
||||
if (_protocol.TryParseMessages(result.Buffer.ToArray(), _invocationBinder, messages))
|
||||
{
|
||||
foreach (var m in messages)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
// The test should connect to the server using WebSockets transport on Windows 8 and newer.
|
||||
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
|
||||
var connection = new HttpConnection(new Uri(url));
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
// The test logic lives in the TestTransportFactory and FakeTransport.
|
||||
var connection = new HttpConnection(new Uri(url), new TestTransportFactory(), null, null);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
var url = _serverFixture.Url + "/echo";
|
||||
var connection = new HttpConnection(new Uri(url), transportType);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}, receiveTcs);
|
||||
|
||||
var message = new byte[] { 42 };
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
await connection.SendAsync(message).OrTimeout();
|
||||
|
||||
var receivedData = await receiveTcs.Task.OrTimeout();
|
||||
|
|
@ -166,8 +166,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}
|
||||
|
||||
[Theory(Skip = "https://github.com/aspnet/SignalR/issues/1485")]
|
||||
[MemberData(nameof(TransportTypesAndTransferModes))]
|
||||
public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferMode requestedTransferMode)
|
||||
[MemberData(nameof(TransportTypesAndTransferFormats))]
|
||||
public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferFormat requestedTransferFormat)
|
||||
{
|
||||
using (StartLog(out var loggerFactory, testName: $"ConnectionCanSendAndReceiveMessages_{transportType.ToString()}"))
|
||||
{
|
||||
|
|
@ -177,9 +177,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
var url = _serverFixture.Url + "/echo";
|
||||
var connection = new HttpConnection(new Uri(url), transportType, loggerFactory);
|
||||
|
||||
connection.Features.Set<ITransferModeFeature>(
|
||||
new TransferModeFeature { TransferMode = requestedTransferMode });
|
||||
try
|
||||
{
|
||||
var closeTcs = new TaskCompletionSource<object>();
|
||||
|
|
@ -199,28 +196,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
logger.LogInformation("Received {length} byte message", data.Length);
|
||||
|
||||
if (IsBase64Encoded(requestedTransferMode, connection))
|
||||
{
|
||||
data = Convert.FromBase64String(Encoding.UTF8.GetString(data));
|
||||
}
|
||||
var tcs = (TaskCompletionSource<string>)state;
|
||||
tcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
logger.LogInformation("Starting connection to {url}", url);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(requestedTransferFormat).OrTimeout();
|
||||
logger.LogInformation("Started connection to {url}", url);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
// Need to encode binary payloads sent over text transports
|
||||
if (IsBase64Encoded(requestedTransferMode, connection))
|
||||
{
|
||||
bytes = Encoding.UTF8.GetBytes(Convert.ToBase64String(bytes));
|
||||
}
|
||||
|
||||
logger.LogInformation("Sending {length} byte message", bytes.Length);
|
||||
try
|
||||
{
|
||||
|
|
@ -255,12 +241,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsBase64Encoded(TransferMode transferMode, IConnection connection)
|
||||
{
|
||||
return transferMode == TransferMode.Binary &&
|
||||
connection.Features.Get<ITransferModeFeature>().TransferMode == TransferMode.Text;
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> MessageSizesData
|
||||
{
|
||||
get
|
||||
|
|
@ -281,8 +261,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
var url = _serverFixture.Url + "/echo";
|
||||
var connection = new HttpConnection(new Uri(url), TransportType.WebSockets, loggerFactory);
|
||||
connection.Features.Set<ITransferModeFeature>(
|
||||
new TransferModeFeature { TransferMode = TransferMode.Binary });
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -296,7 +274,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}, receiveTcs);
|
||||
|
||||
logger.LogInformation("Starting connection to {url}", url);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
logger.LogInformation("Started connection to {url}", url);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
|
|
@ -416,16 +394,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
private class FakeTransport : ITransport
|
||||
{
|
||||
public TransferMode? Mode => TransferMode.Text;
|
||||
public string prevConnectionId = null;
|
||||
private int tries = 0;
|
||||
private IDuplexPipe _application;
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
_application = application;
|
||||
tries++;
|
||||
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
|
||||
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
|
||||
if (prevConnectionId == null)
|
||||
{
|
||||
prevConnectionId = id;
|
||||
|
|
@ -467,14 +444,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> TransportTypesAndTransferModes
|
||||
public static IEnumerable<object[]> TransportTypesAndTransferFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var transport in TransportTypes)
|
||||
{
|
||||
yield return new object[] { transport[0], TransferMode.Text };
|
||||
yield return new object[] { transport[0], TransferMode.Binary };
|
||||
yield return new object[] { transport[0], TransferFormat.Text };
|
||||
|
||||
if ((TransportType)transport[0] != TransportType.ServerSentEvents)
|
||||
{
|
||||
yield return new object[] { transport[0], TransferFormat.Binary };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Security.Claims;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Encoders;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
|
|
@ -1333,10 +1332,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
using (var client = new TestClient(synchronousCallbacks: false, protocol: protocol, invocationBinder: invocationBinder.Object))
|
||||
{
|
||||
var transportFeature = new Mock<IConnectionTransportFeature>();
|
||||
transportFeature.SetupGet(f => f.TransportCapabilities)
|
||||
.Returns(protocol.Type == ProtocolType.Binary ? TransferMode.Binary : TransferMode.Text);
|
||||
client.Connection.Features.Set(transportFeature.Object);
|
||||
client.Connection.SupportedFormats = protocol.TransferFormat;
|
||||
|
||||
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
|
||||
|
||||
|
|
@ -1419,7 +1415,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
|
||||
|
||||
using (var client1 = new TestClient(protocol: new JsonHubProtocol()))
|
||||
using (var client2 = new TestClient(protocol: new MessagePackHubProtocol(), dataEncoder: new Base64Encoder()))
|
||||
using (var client2 = new TestClient(protocol: new MessagePackHubProtocol()))
|
||||
{
|
||||
var endPointLifetime1 = endPoint.OnConnectedAsync(client1.Connection);
|
||||
var endPointLifetime2 = endPoint.OnConnectedAsync(client2.Connection);
|
||||
|
|
@ -1617,9 +1613,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var msgPackOptions = serviceProvider.GetRequiredService<IOptions<MessagePackHubProtocolOptions>>();
|
||||
using (var client = new TestClient(synchronousCallbacks: false, protocol: new MessagePackHubProtocol(msgPackOptions)))
|
||||
{
|
||||
var transportFeature = new Mock<IConnectionTransportFeature>();
|
||||
transportFeature.SetupGet(f => f.TransportCapabilities).Returns(TransferMode.Binary);
|
||||
client.Connection.Features.Set(transportFeature.Object);
|
||||
client.Connection.SupportedFormats = TransferFormat.Binary;
|
||||
var endPointLifetime = endPoint.OnConnectedAsync(client.Connection);
|
||||
|
||||
await client.Connected.OrTimeout();
|
||||
|
|
@ -1792,6 +1786,21 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NegotiatingFailsIfMsgPackRequestedOverTextOnlyTransport()
|
||||
{
|
||||
var serviceProvider = HubEndPointTestUtils.CreateServiceProvider(services =>
|
||||
services.Configure<HubOptions>(options =>
|
||||
options.KeepAliveInterval = TimeSpan.FromMilliseconds(100)));
|
||||
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
|
||||
|
||||
using (var client = new TestClient(false, new MessagePackHubProtocol()))
|
||||
{
|
||||
client.Connection.SupportedFormats = TransferFormat.Text;
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => endPoint.OnConnectedAsync(client.Connection).OrTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> HubTypes()
|
||||
{
|
||||
yield return new[] { typeof(DynamicTestHub) };
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
|
||||
TransferMode.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
await webSocketsTransport.StopAsync().OrTimeout();
|
||||
await webSocketsTransport.Running.OrTimeout();
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application,
|
||||
TransferMode.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
TransferFormat.Binary, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("User-Agent"));
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
|
||||
TransferMode.Binary, connection: Mock.Of<IConnection>());
|
||||
TransferFormat.Binary, connection: Mock.Of<IConnection>());
|
||||
pair.Transport.Output.Complete();
|
||||
await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
|
@ -128,15 +128,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
|
||||
[InlineData(TransferMode.Text)]
|
||||
[InlineData(TransferMode.Binary)]
|
||||
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferMode transferMode)
|
||||
[InlineData(TransferFormat.Text)]
|
||||
[InlineData(TransferFormat.Binary)]
|
||||
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat)
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferMode, connection: Mock.Of<IConnection>());
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferFormat, connection: Mock.Of<IConnection>());
|
||||
|
||||
await pair.Transport.Output.WriteAsync(new byte[] { 0x42 });
|
||||
|
||||
|
|
@ -151,38 +151,38 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
|
||||
[InlineData(TransferMode.Text)]
|
||||
[InlineData(TransferMode.Binary)]
|
||||
public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode)
|
||||
[InlineData(TransferFormat.Text)]
|
||||
[InlineData(TransferFormat.Binary)]
|
||||
public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat)
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
|
||||
Assert.Null(webSocketsTransport.Mode);
|
||||
await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application,
|
||||
transferMode, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
Assert.Equal(transferMode, webSocketsTransport.Mode);
|
||||
transferFormat, connection: Mock.Of<IConnection>()).OrTimeout();
|
||||
|
||||
await webSocketsTransport.StopAsync().OrTimeout();
|
||||
await webSocketsTransport.Running.OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[ConditionalTheory]
|
||||
[InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
|
||||
[InlineData((TransferFormat)42)] // Unexpected value
|
||||
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
|
||||
public async Task WebSocketsTransportThrowsForInvalidTransferMode()
|
||||
public async Task WebSocketsTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory);
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text | TransferMode.Binary, connection: Mock.Of<IConnection>()));
|
||||
webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: Mock.Of<IConnection>()));
|
||||
|
||||
Assert.Contains("Invalid transfer mode.", exception.Message);
|
||||
Assert.Equal("requestedTransferMode", exception.ParamName);
|
||||
Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
|
||||
Assert.Equal("transferFormat", exception.ParamName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
[InlineData(TransportType.All)]
|
||||
[InlineData((TransportType)0)]
|
||||
[InlineData(TransportType.LongPolling | TransportType.WebSockets)]
|
||||
public async Task NegotiateReturnsAvailableTransports(TransportType transports)
|
||||
public async Task NegotiateReturnsAvailableTransportsAfterFilteringByOptions(TransportType transports)
|
||||
{
|
||||
using (StartLog(out var loggerFactory, LogLevel.Debug))
|
||||
{
|
||||
|
|
@ -167,7 +167,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
var availableTransports = (TransportType)0;
|
||||
foreach (var transport in negotiateResponse["availableTransports"])
|
||||
{
|
||||
availableTransports |= (TransportType)Enum.Parse(typeof(TransportType), transport.Value<string>());
|
||||
var transportType = (TransportType)Enum.Parse(typeof(TransportType), transport.Value<string>("transport"));
|
||||
availableTransports |= transportType;
|
||||
}
|
||||
|
||||
Assert.Equal(transports, availableTransports);
|
||||
|
|
@ -820,10 +821,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransportType.LongPolling, TransferMode.Binary | TransferMode.Text)]
|
||||
[InlineData(TransportType.ServerSentEvents, TransferMode.Text)]
|
||||
[InlineData(TransportType.WebSockets, TransferMode.Binary | TransferMode.Text)]
|
||||
public async Task TransportCapabilitiesSet(TransportType transportType, TransferMode expectedTransportCapabilities)
|
||||
[InlineData(TransportType.LongPolling, null)]
|
||||
[InlineData(TransportType.ServerSentEvents, TransferFormat.Text)]
|
||||
[InlineData(TransportType.WebSockets, TransferFormat.Binary | TransferFormat.Text)]
|
||||
public async Task TransferModeSet(TransportType transportType, TransferFormat? expectedTransferFormats)
|
||||
{
|
||||
using (StartLog(out var loggerFactory, LogLevel.Debug))
|
||||
{
|
||||
|
|
@ -845,7 +846,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0);
|
||||
await dispatcher.ExecuteAsync(context, options, app);
|
||||
|
||||
Assert.Equal(expectedTransportCapabilities, connection.TransportCapabilities);
|
||||
if (expectedTransferFormats != null)
|
||||
{
|
||||
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
|
||||
Assert.Equal(expectedTransferFormats.Value, transferFormatFeature.SupportedFormats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ using System.Net.WebSockets;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets.Features;
|
||||
using Microsoft.AspNetCore.Sockets.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Internal.Transports;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
|
|
@ -71,9 +73,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransferMode.Text, WebSocketMessageType.Text)]
|
||||
[InlineData(TransferMode.Binary, WebSocketMessageType.Binary)]
|
||||
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferModeFeature(TransferMode transferMode, WebSocketMessageType expectedMessageType)
|
||||
[InlineData(TransferFormat.Text, WebSocketMessageType.Text)]
|
||||
[InlineData(TransferFormat.Binary, WebSocketMessageType.Binary)]
|
||||
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, WebSocketMessageType expectedMessageType)
|
||||
{
|
||||
using (StartLog(out var loggerFactory, LogLevel.Debug))
|
||||
{
|
||||
|
|
@ -82,7 +84,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
|||
|
||||
using (var feature = new TestWebSocketConnectionFeature())
|
||||
{
|
||||
var connectionContext = new DefaultConnectionContext(string.Empty, null, null) { TransferMode = transferMode };
|
||||
var connectionContext = new DefaultConnectionContext(string.Empty, null, null);
|
||||
connectionContext.ActiveFormat = transferFormat;
|
||||
var ws = new WebSocketsTransport(new WebSocketOptions(), connection.Application, connectionContext, loggerFactory);
|
||||
|
||||
// Give the server socket to the transport and run it
|
||||
|
|
|
|||
Loading…
Reference in New Issue