From cb3124be17c8d7f476e2f4eab3c1c9fbe213f23d Mon Sep 17 00:00:00 2001 From: Andrew Stanton-Nurse Date: Thu, 16 Nov 2017 09:45:13 -0800 Subject: [PATCH] First pass at Keep Alive (#1119) This adds the Ping message type and support for sending/receiving it in the Hub Protocols. It does not add the logic to transmit keep-alive frames. --- .../HubConnection.spec.ts | 46 ++++++--- .../MessagePackHubProtocol.spec.ts | 96 +++++++++++-------- .../HubConnection.ts | 13 ++- .../IHubProtocol.ts | 19 ++-- .../MessagePackHubProtocol.ts | 26 +++-- specs/HubProtocol.md | 56 ++++++++++- .../HubConnection.cs | 7 +- .../Internal/HubProtocolReaderWriter.cs | 2 +- .../Protocol/CancelInvocationMessage.cs | 6 +- .../Internal/Protocol/CompletionMessage.cs | 2 +- .../Internal/Protocol/HubInvocationMessage.cs | 15 +++ .../Internal/Protocol/HubMessage.cs | 9 +- .../Protocol/HubMethodInvocationMessage.cs | 2 +- .../Internal/Protocol/HubProtocolConstants.cs | 12 +++ .../Internal/Protocol/JsonHubProtocol.cs | 50 ++++++---- .../Protocol/MessagePackHubProtocol.cs | 41 ++++---- .../Internal/Protocol/PingMessage.cs | 14 +++ .../Internal/Protocol/StreamItemMessage.cs | 2 +- .../DefaultHubLifetimeManager.cs | 2 +- .../HubEndPoint.cs | 4 + .../RedisHubLifetimeManager.cs | 10 +- test/Common/TestClient.cs | 6 +- .../HubConnectionProtocolTests.cs | 36 ++++++- .../HubConnectionTests.cs | 6 +- .../Internal/Protocol/CompositeTestBinder.cs | 13 ++- .../Internal/Protocol/JsonHubProtocolTests.cs | 4 +- .../Protocol/MessagePackHubProtocolTests.cs | 21 +++- .../Internal/Protocol/TestBinder.cs | 2 +- .../TestHubMessageEqualityComparer.cs | 65 ++++++++----- .../HubEndpointTests.cs | 26 ++++- 30 files changed, 436 insertions(+), 177 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolConstants.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/PingMessage.cs diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts index cde28dac19..a35d124345 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts @@ -129,7 +129,7 @@ describe("HubConnection", () => { it("invocations ignored in callbacks not registered", async () => { let warnings: string[] = []; let logger = { - log: function(logLevel: LogLevel, message: string) { + log: function (logLevel: LogLevel, message: string) { if (logLevel === LogLevel.Warning) { warnings.push(message); } @@ -219,15 +219,15 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - hubConnection.off("_", () => {}); - hubConnection.on("message", t => {}); - hubConnection.on("message", () => {}); + hubConnection.off("_", () => { }); + hubConnection.on("message", t => { }); + hubConnection.on("message", () => { }); }); it("using null/undefined for methodName or method no-ops", async () => { let warnings: string[] = []; let logger = { - log: function(logLevel: LogLevel, message: string) { + log: function (logLevel: LogLevel, message: string) { if (logLevel === LogLevel.Warning) { warnings.push(message); } @@ -242,8 +242,8 @@ describe("HubConnection", () => { hubConnection.on(undefined, null); hubConnection.on("message", null); hubConnection.on("message", undefined); - hubConnection.on(null, () => {}); - hubConnection.on(undefined, () => {}); + hubConnection.on(null, () => { }); + hubConnection.on(undefined, () => { }); // invoke a method to make sure we are not trying to use null/undefined connection.receive({ @@ -260,8 +260,8 @@ describe("HubConnection", () => { hubConnection.off(undefined, null); hubConnection.off("message", null); hubConnection.off("message", undefined); - hubConnection.off(null, () => {}); - hubConnection.off(undefined, () => {}); + hubConnection.off(null, () => { }); + hubConnection.off(undefined, () => { }); }); }); @@ -351,13 +351,13 @@ describe("HubConnection", () => { hubConnection.stream("testMethod") .subscribe(observer); - connection.receive({ type: MessageType.Result, invocationId: connection.lastInvocationId, item: 1 }); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 }); expect(observer.itemsReceived).toEqual([1]); - connection.receive({ type: MessageType.Result, invocationId: connection.lastInvocationId, item: 2 }); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 }); expect(observer.itemsReceived).toEqual([1, 2]); - connection.receive({ type: MessageType.Result, invocationId: connection.lastInvocationId, item: 3 }); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 3 }); expect(observer.itemsReceived).toEqual([1, 2, 3]); connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); @@ -392,7 +392,7 @@ describe("HubConnection", () => { }); describe("onClose", () => { - it("it can have multiple callbacks", async () => { + it("can have multiple callbacks", async () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); let invocations = 0; @@ -424,6 +424,21 @@ describe("HubConnection", () => { // expect no errors }); }); + + describe("keepAlive", () => { + it("can receive ping messages", async () => { + // Receive the ping mid-invocation so we can see that the rest of the flow works fine + + let connection = new TestConnection(); + let hubConnection = new HubConnection(connection); + let invokePromise = hubConnection.invoke("testMethod", "arg", 42); + + connection.receive({ type: MessageType.Ping }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" }); + + expect(await invokePromise).toBe("foo"); + }) + }) }); class TestConnection implements IConnection { @@ -435,7 +450,10 @@ class TestConnection implements IConnection { send(data: any): Promise { let invocation = TextMessageFormat.parse(data)[0]; - this.lastInvocationId = JSON.parse(invocation).invocationId; + let invocationId = JSON.parse(invocation).invocationId; + if (invocationId) { + this.lastInvocationId = invocationId; + } if (this.sentData) { this.sentData.push(invocation); } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts index 8c908f33d6..717af6673e 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts @@ -20,27 +20,27 @@ describe("MessageHubProtocol", () => { }); ([ - [ [ 0x0b, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72], - { - type: MessageType.Completion, - invocationId: "abc", - error: "Err", - result: null - } as CompletionMessage ], - [ [ 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ], - { - type: MessageType.Completion, - invocationId: "abc", - error: null, - result: "OK" - } as CompletionMessage ], - [ [ 0x07, 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ], - { - type: MessageType.Completion, - invocationId: "abc", - error: null, - result: null - } as CompletionMessage ] + [[0x0b, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72], + { + type: MessageType.Completion, + invocationId: "abc", + error: "Err", + result: null + } as CompletionMessage], + [[0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b], + { + type: MessageType.Completion, + invocationId: "abc", + error: null, + result: "OK" + } as CompletionMessage], + [[0x07, 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02], + { + type: MessageType.Completion, + invocationId: "abc", + error: null, + result: null + } as CompletionMessage] ] as [[number[], CompletionMessage]]).forEach(([payload, expected_message]) => it("can read Completion message", () => { let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); @@ -48,12 +48,12 @@ describe("MessageHubProtocol", () => { })); ([ - [ [ 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ], - { - type: MessageType.Result, - invocationId: "abc", - item: 8 - } as ResultMessage ] + [[0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08], + { + type: MessageType.StreamItem, + invocationId: "abc", + item: 8 + } as ResultMessage] ] as [[number[], CompletionMessage]]).forEach(([payload, expected_message]) => it("can read Result message", () => { let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); @@ -61,31 +61,31 @@ describe("MessageHubProtocol", () => { })); ([ - [ [ 0x00 ], new Error("Invalid payload.") ], - [ [ 0x01, 0x90 ], new Error("Invalid payload.") ], - [ [ 0x01, 0xc2 ], new Error("Invalid payload.") ], - [ [ 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ], - [ [ 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ], - [ [ 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ], - [ [ 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ], - [ [ 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ] + [[0x00], new Error("Invalid payload.")], + [[0x01, 0x90], new Error("Invalid payload.")], + [[0x01, 0xc2], new Error("Invalid payload.")], + [[0x02, 0x91, 0x05], new Error("Invalid message type.")], + [[0x03, 0x91, 0xa1, 0x78], new Error("Invalid message type.")], + [[0x02, 0x91, 0x01], new Error("Invalid payload for Invocation message.")], + [[0x02, 0x91, 0x02], new Error("Invalid payload for stream Result message.")], + [[0x03, 0x92, 0x03, 0xa0], new Error("Invalid payload for Completion message.")], + [[0x05, 0x94, 0x03, 0xa0, 0x02, 0x00], new Error("Invalid payload for Completion message.")], + [[0x04, 0x93, 0x03, 0xa0, 0x01], new Error("Invalid payload for Completion message.")], + [[0x04, 0x93, 0x03, 0xa0, 0x03], new Error("Invalid payload for Completion message.")] ] as [[number[], Error]]).forEach(([payload, expected_error]) => it("throws for invalid messages", () => { expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer)) - .toThrow(expected_error); + .toThrow(expected_error); })); it("can read multiple messages", () => { let payload = [ 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08, - 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ]; + 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b]; let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); expect(messages).toEqual([ { - type: MessageType.Result, + type: MessageType.StreamItem, invocationId: "abc", item: 8 } as ResultMessage, @@ -97,4 +97,18 @@ describe("MessageHubProtocol", () => { } as CompletionMessage ]); }); + + it("can read ping message", () => { + let payload = [ + 0x02, + 0x91, // message array length = 1 (fixarray) + 0x06, // type = 6 = Ping (fixnum) + ]; + let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); + expect(messages).toEqual([ + { + type: MessageType.Ping, + } + ]) + }) }); \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index 4058054ac4..00cc491640 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -3,10 +3,10 @@ import { ConnectionClosed } from "./Common" import { IConnection } from "./IConnection" -import { HttpConnection} from "./HttpConnection" +import { HttpConnection } from "./HttpConnection" import { TransportType, TransferMode } from "./Transports" import { Subject, Observable } from "./Observable" -import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage } from "./IHubProtocol"; +import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage, HubInvocationMessage } from "./IHubProtocol"; import { JsonHubProtocol } from "./JsonHubProtocol"; import { TextMessageFormat } from "./Formatters" import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol" @@ -61,16 +61,19 @@ export class HubConnection { case MessageType.Invocation: this.invokeClientMethod(message); break; - case MessageType.Result: + case MessageType.StreamItem: case MessageType.Completion: - let callback = this.callbacks.get(message.invocationId); + let callback = this.callbacks.get((message).invocationId); if (callback != null) { if (message.type === MessageType.Completion) { - this.callbacks.delete(message.invocationId); + this.callbacks.delete((message).invocationId); } callback(message); } break; + case MessageType.Ping: + // Don't care about pings + break; default: this.logger.log(LogLevel.Warning, "Invalid message type: " + data); break; diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts index 23c1bbd9ac..f56d6ba197 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts @@ -3,32 +3,37 @@ export const enum MessageType { Invocation = 1, - Result, - Completion, - StreamInvocation + StreamItem = 2, + Completion = 3, + StreamInvocation = 4, + CancelInvocation = 5, + Ping = 6, } export interface HubMessage { readonly type: MessageType; +} + +export interface HubInvocationMessage extends HubMessage { readonly invocationId: string; } -export interface InvocationMessage extends HubMessage { +export interface InvocationMessage extends HubInvocationMessage { readonly target: string; readonly arguments: Array; readonly nonblocking?: boolean; } -export interface StreamInvocationMessage extends HubMessage { +export interface StreamInvocationMessage extends HubInvocationMessage { readonly target: string; readonly arguments: Array } -export interface ResultMessage extends HubMessage { +export interface ResultMessage extends HubInvocationMessage { readonly item?: any; } -export interface CompletionMessage extends HubMessage { +export interface CompletionMessage extends HubInvocationMessage { readonly error?: string; readonly result?: any; } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts index cccef0e93d..634da82dc7 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts @@ -30,15 +30,27 @@ export class MessagePackHubProtocol implements IHubProtocol { switch (messageType) { case MessageType.Invocation: return this.createInvocationMessage(properties); - case MessageType.Result: + case MessageType.StreamItem: return this.createStreamItemMessage(properties); case MessageType.Completion: return this.createCompletionMessage(properties); + case MessageType.Ping: + return this.createPingMessage(properties); default: throw new Error("Invalid message type."); } } + private createPingMessage(properties: any[]): HubMessage { + if (properties.length != 1) { + throw new Error("Invalid payload for Ping message."); + } + + return { + type: properties[0] + } as HubMessage; + } + private createInvocationMessage(properties: any[]): InvocationMessage { if (properties.length != 5) { throw new Error("Invalid payload for Invocation message."); @@ -59,7 +71,7 @@ export class MessagePackHubProtocol implements IHubProtocol { } return { - type: MessageType.Result, + type: MessageType.StreamItem, invocationId: properties[1], item: properties[2] } as ResultMessage; @@ -106,7 +118,7 @@ export class MessagePackHubProtocol implements IHubProtocol { return this.writeInvocation(message as InvocationMessage); case MessageType.StreamInvocation: return this.writeStreamInvocation(message as StreamInvocationMessage); - case MessageType.Result: + case MessageType.StreamItem: case MessageType.Completion: throw new Error(`Writing messages of type '${message.type}' is not supported.`); default: @@ -116,16 +128,16 @@ export class MessagePackHubProtocol implements IHubProtocol { private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer { let msgpack = msgpack5(); - let payload = msgpack.encode([ MessageType.Invocation, invocationMessage.invocationId, - invocationMessage.nonblocking, invocationMessage.target, invocationMessage.arguments]); + let payload = msgpack.encode([MessageType.Invocation, invocationMessage.invocationId, + invocationMessage.nonblocking, invocationMessage.target, invocationMessage.arguments]); return BinaryMessageFormat.write(payload.slice()); } private writeStreamInvocation(streamInvocationMessage: StreamInvocationMessage): ArrayBuffer { let msgpack = msgpack5(); - let payload = msgpack.encode([ MessageType.StreamInvocation, streamInvocationMessage.invocationId, - streamInvocationMessage.target, streamInvocationMessage.arguments]); + let payload = msgpack.encode([MessageType.StreamInvocation, streamInvocationMessage.invocationId, + streamInvocationMessage.target, streamInvocationMessage.arguments]); return BinaryMessageFormat.write(payload.slice()); } diff --git a/specs/HubProtocol.md b/specs/HubProtocol.md index c0f76c410d..7ab37f61d6 100644 --- a/specs/HubProtocol.md +++ b/specs/HubProtocol.md @@ -26,6 +26,7 @@ In the SignalR protocol, the following types of messages can be sent: * `StreamItem` Message - Indicates individual items of streamed response data from a previous Invocation message. * `Completion` Message - Indicates a previous Invocation or StreamInvocation has completed. Contains an error if the invocation concluded with an error or the result of a non-streaming method invocation. The result will be absent for `void` methods. In case of streaming invocations no further `StreamItem` messages will be received * `CancelInvocation` Message - Sent by the client to cancel a streaming invocation on the server. +* `Ping` Message - Sent by either party to check if the connection is active. After opening a connection to the server the client must send a `Negotiation` message to the server as its first message. The negotiation message is **always** a JSON message and contains the name of the format (protocol) that will be used for the duration of the connection. If the server does not support the protocol requested by the client or the first message received from the client is not a `Negotiation` message the server must close the connection. @@ -97,7 +98,18 @@ If either endpoint commits a Protocol Error (see examples below), the other endp * It is a protocol error for a Caller to send a `Completion` message carrying both a result and an error. * It is a protocol error for an `Invocation` or `StreamInvocation` message to have an `Invocation ID` that has already been used by *that* endpoint. However, it is **not an error** for one endpoint to use an `Invocation ID` that was previously used by the other endpoint (allowing each endpoint to track it's own IDs). -## Examples +## Ping (aka "Keep Alive") + +The SignalR Hub protocol supports "Keep Alive" messages used to ensure that the underlying transport connection remains active. These messages help ensure: + +1. Proxies don't close the underlying connection during idle times (when few messages are being sent) +2. If the underlying connection is dropped without being terminated gracefully, the application is informed as quickly as possible. + +Keep alive behavior is achieved via the `Ping` message type. **Either endpoint** may send a `Ping` message at any time. The receiving endpoint may choose to ignore the message, it has no obligation to respond in anyway. Most implementations will want to reset a timeout used to determine if the other party is present. + +Ping messages do not have any payload, they are completely empty messages (aside from the encoding necessary to identify the message as a `Ping` message). + +## Example Consider the following C# methods @@ -233,6 +245,12 @@ S->C: Completion { Id = 42 } // This can be ignored C->S: Invocation { Id = 42, Target = "NonBlocking", Arguments = [ "foo" ], NonBlocking = true } ``` +### Ping + +``` +C->S: Ping +``` + ## JSON Encoding In the JSON Encoding of the SignalR Protocol, each Message is represented as a single JSON object, which should be the only content of the underlying message from the Transport. All property names are case-sensitive. The underlying protocol is expected to handle encoding and decoding of the text, so the JSON string should be encoded in whatever form is expected by the underlying transport. For example, when using the ASP.NET Sockets transports, UTF-8 encoding is always used for text. @@ -381,6 +399,18 @@ Example } ``` +### Ping Message Encoding +A `Ping` message is a JSON object with the following properties: + +* `type` - A `Number` with the literal value `6`, indicating that this is a `Ping`. + +Example +```json +{ + "type": 6 +} +``` + ### JSON Payload Encoding Items in the arguments array within the `Invocation` message type, as well as the `item` value of the `StreamItem` message and the `result` value of the `Completion` message, encode values which have meaning to each particular Binder. A general guideline for encoding/decoding these values is provided in the "Type Mapping" section at the end of this document, but Binders should provide configuration to applications to allow them to customize these mappings. These mappings need not be self-describing, because when decoding the value, the Binder is expected to know the destination type (by looking up the definition of the method indicated by the Target). @@ -605,6 +635,30 @@ is decoded as follows: * `0x79` - `y` * `0x7a` - `z` +### Ping Message Encoding + +`Ping` messages have the following structure + +``` +[6] +``` + +* `6` - Message Type - `6` indicates this is a `Ping` message. + +Examples: + +#### Ping message + +The following payload: +``` +0x91 0x06 +``` + +is decoded as follows: + +* `0x91` - 1-element array +* `0x06` - `6` (Message Type - `Ping` message) + ## Protocol Buffers (ProtoBuf) Encoding **Protobuf encoding is currently not implemented** diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index 7faaf21488..898c4aff32 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.SignalR.Client public IDisposable On(string methodName, Type[] parameterTypes, Func handler, object state) { var invocationHandler = new InvocationHandler(parameterTypes, handler, state); - var invocationList = _handlers.AddOrUpdate(methodName, _ => new List { invocationHandler }, + var invocationList = _handlers.AddOrUpdate(methodName, _ => new List { invocationHandler }, (_, invocations) => { lock (invocations) @@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.SignalR.Client return SendHubMessage(invocationMessage, irq); } - private async Task SendHubMessage(HubMessage hubMessage, InvocationRequest irq) + private async Task SendHubMessage(HubInvocationMessage hubMessage, InvocationRequest irq) { try { @@ -328,6 +328,9 @@ namespace Microsoft.AspNetCore.SignalR.Client } DispatchInvocationStreamItemAsync(streamItem, irq); break; + case PingMessage _: + // Nothing to do on receipt of a ping. + break; default: throw new InvalidOperationException($"Unexpected message type: {message.GetType().FullName}"); } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs index c989712297..a684e5e40e 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs @@ -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.Collections.Generic; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CancelInvocationMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CancelInvocationMessage.cs index 2240d8f569..5256863efa 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CancelInvocationMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CancelInvocationMessage.cs @@ -1,12 +1,12 @@ -// 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. namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { - public class CancelInvocationMessage : HubMessage + public class CancelInvocationMessage : HubInvocationMessage { public CancelInvocationMessage(string invocationId) : base(invocationId) { } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CompletionMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CompletionMessage.cs index 911dc7b6f8..4932ea1bac 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CompletionMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/CompletionMessage.cs @@ -5,7 +5,7 @@ using System; namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { - public class CompletionMessage : HubMessage + public class CompletionMessage : HubInvocationMessage { public string Error { get; } public object Result { get; } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs new file mode 100644 index 0000000000..a447dd1bf1 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs @@ -0,0 +1,15 @@ +// 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 abstract class HubInvocationMessage : HubMessage + { + public string InvocationId { get; } + + protected HubInvocationMessage(string invocationId) + { + InvocationId = invocationId; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs index 9b15d50d6d..acea6fb898 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs @@ -1,15 +1,10 @@ -// 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. namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { + // This is basically just a marker type now so that HubProtocol can return a common base class other than object. public abstract class HubMessage { - public string InvocationId { get; } - - protected HubMessage(string invocationId) - { - InvocationId = invocationId; - } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs index 96eef9760f..02934044fe 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs @@ -7,7 +7,7 @@ using System.Runtime.ExceptionServices; namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { - public abstract class HubMethodInvocationMessage : HubMessage + public abstract class HubMethodInvocationMessage : HubInvocationMessage { private readonly ExceptionDispatchInfo _argumentBindingException; private readonly object[] _arguments; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolConstants.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolConstants.cs new file mode 100644 index 0000000000..09179c276f --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolConstants.cs @@ -0,0 +1,12 @@ +namespace Microsoft.AspNetCore.SignalR.Internal.Protocol +{ + internal static class HubProtocolConstants + { + public const int InvocationMessageType = 1; + public const int StreamItemMessageType = 2; + public const int CompletionMessageType = 3; + public const int StreamInvocationMessageType = 4; + public const int CancelInvocationMessageType = 5; + public const int PingMessageType = 6; + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index 52a043eb63..7151aa2988 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -22,12 +22,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private const string TargetPropertyName = "target"; private const string NonBlockingPropertyName = "nonBlocking"; private const string ArgumentsPropertyName = "arguments"; - - private const int InvocationMessageType = 1; - private const int ResultMessageType = 2; - private const int CompletionMessageType = 3; - private const int StreamInvocationMessageType = 4; - private const int CancelInvocationMessageType = 5; + private const string PayloadPropertyName = "payload"; // ONLY to be used for application payloads (args, return values, etc.) private JsonSerializer _payloadSerializer; @@ -108,16 +103,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol var type = JsonUtils.GetRequiredProperty(json, TypePropertyName, JTokenType.Integer); switch (type) { - case InvocationMessageType: + case HubProtocolConstants.InvocationMessageType: return BindInvocationMessage(json, binder); - case StreamInvocationMessageType: + case HubProtocolConstants.StreamInvocationMessageType: return BindStreamInvocationMessage(json, binder); - case ResultMessageType: - return BindResultMessage(json, binder); - case CompletionMessageType: + case HubProtocolConstants.StreamItemMessageType: + return BindStreamItemMessage(json, binder); + case HubProtocolConstants.CompletionMessageType: return BindCompletionMessage(json, binder); - case CancelInvocationMessageType: + case HubProtocolConstants.CancelInvocationMessageType: return BindCancelInvocationMessage(json); + case HubProtocolConstants.PingMessageType: + return PingMessage.Instance; default: throw new FormatException($"Unknown message type: {type}"); } @@ -150,6 +147,9 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol case CancelInvocationMessage m: WriteCancelInvocationMessage(m, writer); break; + case PingMessage m: + WritePingMessage(m, writer); + break; default: throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}"); } @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteCompletionMessage(CompletionMessage message, JsonTextWriter writer) { writer.WriteStartObject(); - WriteHubMessageCommon(message, writer, CompletionMessageType); + WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.CompletionMessageType); if (!string.IsNullOrEmpty(message.Error)) { writer.WritePropertyName(ErrorPropertyName); @@ -176,14 +176,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteCancelInvocationMessage(CancelInvocationMessage message, JsonTextWriter writer) { writer.WriteStartObject(); - WriteHubMessageCommon(message, writer, CancelInvocationMessageType); + WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.CancelInvocationMessageType); writer.WriteEndObject(); } private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer) { writer.WriteStartObject(); - WriteHubMessageCommon(message, writer, ResultMessageType); + WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.StreamItemMessageType); writer.WritePropertyName(ItemPropertyName); _payloadSerializer.Serialize(writer, message.Item); writer.WriteEndObject(); @@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer) { writer.WriteStartObject(); - WriteHubMessageCommon(message, writer, InvocationMessageType); + WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.InvocationMessageType); writer.WritePropertyName(TargetPropertyName); writer.WriteValue(message.Target); @@ -210,7 +210,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteStreamInvocationMessage(StreamInvocationMessage message, JsonTextWriter writer) { writer.WriteStartObject(); - WriteHubMessageCommon(message, writer, StreamInvocationMessageType); + WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.StreamInvocationMessageType); writer.WritePropertyName(TargetPropertyName); writer.WriteValue(message.Target); @@ -230,10 +230,22 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol writer.WriteEndArray(); } - private static void WriteHubMessageCommon(HubMessage message, JsonTextWriter writer, int type) + private void WritePingMessage(PingMessage m, JsonTextWriter writer) + { + writer.WriteStartObject(); + WriteMessageType(writer, HubProtocolConstants.PingMessageType); + writer.WriteEndObject(); + } + + private static void WriteHubInvocationMessageCommon(HubInvocationMessage message, JsonTextWriter writer, int type) { writer.WritePropertyName(InvocationIdPropertyName); writer.WriteValue(message.InvocationId); + WriteMessageType(writer, type); + } + + private static void WriteMessageType(JsonTextWriter writer, int type) + { writer.WritePropertyName(TypePropertyName); writer.WriteValue(type); } @@ -303,7 +315,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol } } - private StreamItemMessage BindResultMessage(JObject json, IInvocationBinder binder) + private StreamItemMessage BindStreamItemMessage(JObject json, IInvocationBinder binder) { var invocationId = JsonUtils.GetRequiredProperty(json, InvocationIdPropertyName, JTokenType.String); var result = JsonUtils.GetRequiredProperty(json, ItemPropertyName); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index 4d93cee7f7..7d62bcfc9d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -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; @@ -13,12 +13,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { public class MessagePackHubProtocol : IHubProtocol { - private const int InvocationMessageType = 1; - private const int StreamItemMessageType = 2; - private const int CompletionMessageType = 3; - private const int StreamInvocationMessageType = 4; - private const int CancelInvocationMessageType = 5; - private const int ErrorResult = 1; private const int VoidResult = 2; private const int NonVoidResult = 3; @@ -62,16 +56,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol switch (messageType) { - case InvocationMessageType: + case HubProtocolConstants.InvocationMessageType: return CreateInvocationMessage(unpacker, binder); - case StreamInvocationMessageType: + case HubProtocolConstants.StreamInvocationMessageType: return CreateStreamInvocationMessage(unpacker, binder); - case StreamItemMessageType: + case HubProtocolConstants.StreamItemMessageType: return CreateStreamItemMessage(unpacker, binder); - case CompletionMessageType: + case HubProtocolConstants.CompletionMessageType: return CreateCompletionMessage(unpacker, binder); - case CancelInvocationMessageType: + case HubProtocolConstants.CancelInvocationMessageType: return CreateCancelInvocationMessage(unpacker); + case HubProtocolConstants.PingMessageType: + return PingMessage.Instance; default: throw new FormatException($"Invalid message type: {messageType}."); } @@ -155,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol object result = null; var hasResult = false; - switch(resultKind) + switch (resultKind) { case ErrorResult: error = ReadString(unpacker, "error"); @@ -212,6 +208,9 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol case CancelInvocationMessage cancelInvocationMessage: WriteCancelInvocationMessage(cancelInvocationMessage, packer); break; + case PingMessage pingMessage: + WritePingMessage(pingMessage, packer); + break; default: throw new FormatException($"Unexpected message type: {message.GetType().Name}"); } @@ -220,7 +219,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteInvocationMessage(InvocationMessage invocationMessage, Packer packer) { packer.PackArrayHeader(5); - packer.Pack(InvocationMessageType); + packer.Pack(HubProtocolConstants.InvocationMessageType); packer.PackString(invocationMessage.InvocationId); packer.Pack(invocationMessage.NonBlocking); packer.PackString(invocationMessage.Target); @@ -230,7 +229,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteStreamInvocationMessage(StreamInvocationMessage streamInvocationMessage, Packer packer) { packer.PackArrayHeader(4); - packer.Pack(StreamInvocationMessageType); + packer.Pack(HubProtocolConstants.StreamInvocationMessageType); packer.PackString(streamInvocationMessage.InvocationId); packer.PackString(streamInvocationMessage.Target); packer.PackObject(streamInvocationMessage.Arguments, _serializationContext); @@ -239,7 +238,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteStreamingItemMessage(StreamItemMessage streamItemMessage, Packer packer) { packer.PackArrayHeader(3); - packer.Pack(StreamItemMessageType); + packer.Pack(HubProtocolConstants.StreamItemMessageType); packer.PackString(streamItemMessage.InvocationId); packer.PackObject(streamItemMessage.Item, _serializationContext); } @@ -253,7 +252,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol packer.PackArrayHeader(3 + (resultKind != VoidResult ? 1 : 0)); - packer.Pack(CompletionMessageType); + packer.Pack(HubProtocolConstants.CompletionMessageType); packer.PackString(completionMessage.InvocationId); packer.Pack(resultKind); switch (resultKind) @@ -270,10 +269,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private void WriteCancelInvocationMessage(CancelInvocationMessage cancelInvocationMessage, Packer packer) { packer.PackArrayHeader(2); - packer.Pack(CancelInvocationMessageType); + packer.Pack(HubProtocolConstants.CancelInvocationMessageType); packer.PackString(cancelInvocationMessage.InvocationId); } + private void WritePingMessage(PingMessage pingMessage, Packer packer) + { + packer.PackArrayHeader(1); + packer.Pack(HubProtocolConstants.PingMessageType); + } + private static string ReadInvocationId(Unpacker unpacker) { return ReadString(unpacker, "invocationId"); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/PingMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/PingMessage.cs new file mode 100644 index 0000000000..8c2cbbead2 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/PingMessage.cs @@ -0,0 +1,14 @@ +// 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 class PingMessage : HubMessage + { + public static readonly PingMessage Instance = new PingMessage(); + + private PingMessage() + { + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/StreamItemMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/StreamItemMessage.cs index dec753f948..624ab49a43 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/StreamItemMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/StreamItemMessage.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { - public class StreamItemMessage : HubMessage + public class StreamItemMessage : HubInvocationMessage { public object Item { get; } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs index b4051dfc9b..47bfd7e7e1 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs @@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.SignalR return Task.CompletedTask; } - private async Task WriteAsync(HubConnectionContext connection, HubMessage hubMessage) + private async Task WriteAsync(HubConnectionContext connection, HubInvocationMessage hubMessage) { while (await connection.Output.WaitToWriteAsync()) { diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs index f6c572ee37..5a31bcb138 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs @@ -303,6 +303,10 @@ namespace Microsoft.AspNetCore.SignalR } break; + case PingMessage _: + // We don't care about pings + break; + // Other kind of message we weren't expecting default: _logger.UnsupportedMessageReceived(hubMessage.GetType().FullName); diff --git a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs index d1437485bb..76d1e97932 100644 --- a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs @@ -370,7 +370,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis _ackHandler.Dispose(); } - private static async Task WriteAsync(HubConnectionContext connection, HubMessage hubMessage) + private static async Task WriteAsync(HubConnectionContext connection, HubInvocationMessage hubMessage) { while (await connection.Output.WaitToWriteAsync()) { @@ -404,7 +404,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis { _logger.ReceivedFromChannel(_channelNamePrefix); - var message = DeserializeMessage(data); + var message = DeserializeMessage(data); var tasks = new List(_connections.Count); @@ -519,7 +519,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis { try { - var message = DeserializeMessage(data); + var message = DeserializeMessage(data); await WriteAsync(connection, message); } @@ -540,7 +540,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis { try { - var message = DeserializeMessage(data); + var message = DeserializeMessage(data); await WriteAsync(connection, message); } @@ -558,7 +558,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis { try { - var message = DeserializeMessage(data); + var message = DeserializeMessage(data); var tasks = new List(group.Connections.Count); foreach (var groupConnection in group.Connections) diff --git a/test/Common/TestClient.cs b/test/Common/TestClient.cs index 42a7f95ba3..2acecdd66c 100644 --- a/test/Common/TestClient.cs +++ b/test/Common/TestClient.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests throw new InvalidOperationException("Connection aborted!"); } - if (!string.Equals(message.InvocationId, invocationId)) + if (message is HubInvocationMessage hubInvocationMessage && !string.Equals(hubInvocationMessage.InvocationId, invocationId)) { throw new NotSupportedException("TestClient does not support multiple outgoing invocations!"); } @@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests throw new InvalidOperationException("Connection aborted!"); } - if (!string.Equals(message.InvocationId, invocationId)) + if (message is HubInvocationMessage hubInvocationMessage && !string.Equals(hubInvocationMessage.InvocationId, invocationId)) { throw new NotSupportedException("TestClient does not support multiple outgoing invocations!"); } @@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { var payload = _protocolReaderWriter.WriteMessage(message); await Application.Writer.WriteAsync(payload); - return message.InvocationId; + return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null; } public async Task ReadAsync() diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index 1c6e121f53..8b4d0995c6 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -5,13 +5,12 @@ using System; using System.Globalization; using System.IO; using System.Text; -using System.Threading.Tasks; using System.Threading.Channels; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Tests.Common; using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.Logging; -using Moq; using Newtonsoft.Json; using Xunit; @@ -391,5 +390,38 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.DisposeAsync().OrTimeout(); } } + + [Fact] + public async Task AcceptsPingMessages() + { + var connection = new TestConnection(TransferMode.Text); + var hubConnection = new HubConnection(connection, + new JsonHubProtocol(), new LoggerFactory()); + + try + { + await hubConnection.StartAsync().OrTimeout(); + + // Ignore negotiate message + await connection.ReadSentTextMessageAsync().OrTimeout(); + + // Send an invocation + var invokeTask = hubConnection.InvokeAsync("Foo"); + + // Receive the ping mid-invocation so we can see that the rest of the flow works fine + await connection.ReceiveJsonMessage(new { type = 6 }).OrTimeout(); + + // Receive a completion + await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); + + // Ensure the invokeTask completes properly + await invokeTask.OrTimeout(); + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + await connection.DisposeAsync().OrTimeout(); + } + } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index 0d13efc2e1..a8671a09a7 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -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; @@ -189,13 +189,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests // Moq really doesn't handle out parameters well, so to make these tests work I added a manual mock -anurse private class MockHubProtocol : IHubProtocol { - private HubMessage _parsed; + private HubInvocationMessage _parsed; private Exception _error; public int ParseCalls { get; private set; } = 0; public int WriteCalls { get; private set; } = 0; - public static MockHubProtocol ReturnOnParse(HubMessage parsed) + public static MockHubProtocol ReturnOnParse(HubInvocationMessage parsed) { return new MockHubProtocol { diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs index 695e5c1786..3e3c90be06 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs @@ -1,7 +1,8 @@ -// 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; +using System.Linq; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal.Protocol; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol public CompositeTestBinder(HubMessage[] hubMessages) { - _hubMessages = hubMessages; + _hubMessages = hubMessages.Where(IsBindableMessage).ToArray(); } public Type[] GetParameterTypes(string methodName) @@ -28,5 +29,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol index++; return new TestBinder(_hubMessages[index - 1]).GetReturnType(invocationId); } + + private bool IsBindableMessage(HubMessage arg) + { + return arg is CompletionMessage || + arg is InvocationMessage || + arg is StreamItemMessage || + arg is StreamInvocationMessage; + } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index 3b8758dc2b..ff7f763726 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -56,7 +56,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), false, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}" }, new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), true, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}" }, - new object[] { new CancelInvocationMessage("123"), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":5}" } + new object[] { new CancelInvocationMessage("123"), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":5}" }, + + new object[] { PingMessage.Instance, true, NullValueHandling.Ignore, "{\"type\":6}" }, }; [Theory] diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index fa21f9cfb1..0d7e47c2dd 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -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; @@ -51,6 +51,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, new[] { new CustomObject(), new CustomObject() }) } }, new object[] { new[] { new CancelInvocationMessage("xyz") } }, + + new object[] { new[] { PingMessage.Instance } }, new object[] { @@ -59,8 +61,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, 42, "string", new CustomObject()), new CompletionMessage("xyz", error: null, result: 42, hasResult: true), new StreamItemMessage("xyz", null), + PingMessage.Instance, new StreamInvocationMessage("xyz", "method", null, 42, "string", new CustomObject()), - new CompletionMessage("xyz", error: null, result: new CustomObject(), hasResult: true) + new CompletionMessage("xyz", error: null, result: new CustomObject(), hasResult: true), } } }; @@ -273,12 +276,22 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol { 0x04, 0x92, 0x05, 0xa1, 0x30 } - } + }, + new object[] + { + PingMessage.Instance, + new byte[] + { + 0x02, + 0x91, // message array length = 1 (fixarray) + 0x06, // type = 6 = Ping (fixnum) + } + }, }; [Theory] [MemberData(nameof(MessageAndPayload))] - public void UserObjectAreSerializedAsMaps(HubMessage message, byte[] expectedPayload) + public void SerializeMessageTest(HubMessage message, byte[] expectedPayload) { using (var memoryStream = new MemoryStream()) { diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs index 2117d54ded..bf4fbdef44 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs index 3993f9dd80..6f3bf45fee 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs @@ -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; @@ -16,47 +16,60 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol public bool Equals(HubMessage x, HubMessage y) { - if (!string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal)) + // Types should be equal + if (!Equals(x.GetType(), y.GetType())) { return false; } - return InvocationMessagesEqual(x, y) || StreamItemMessagesEqual(x, y) || CompletionMessagesEqual(x, y) - || StreamInvocationMessagesEqual(x, y) || CancelInvocationMessagesEqual(x, y); + switch (x) + { + case InvocationMessage invocationMessage: + return InvocationMessagesEqual(invocationMessage, (InvocationMessage)y); + case StreamItemMessage streamItemMessage: + return StreamItemMessagesEqual(streamItemMessage, (StreamItemMessage)y); + case CompletionMessage completionMessage: + return CompletionMessagesEqual(completionMessage, (CompletionMessage)y); + case StreamInvocationMessage streamInvocationMessage: + return StreamInvocationMessagesEqual(streamInvocationMessage, (StreamInvocationMessage)y); + case CancelInvocationMessage cancelItemMessage: + return string.Equals(cancelItemMessage.InvocationId, ((CancelInvocationMessage)y).InvocationId, StringComparison.Ordinal); + case PingMessage pingMessage: + // If the types are equal (above), then we're done. + return true; + default: + throw new InvalidOperationException($"Unknown message type: {x.GetType().FullName}"); + } } - private bool CompletionMessagesEqual(HubMessage x, HubMessage y) + private bool CompletionMessagesEqual(CompletionMessage x, CompletionMessage y) { - return x is CompletionMessage left && y is CompletionMessage right && - string.Equals(left.Error, right.Error, StringComparison.Ordinal) && - left.HasResult == right.HasResult && - (Equals(left.Result, right.Result) || SequenceEqual(left.Result, right.Result)); + return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && + string.Equals(x.Error, y.Error, StringComparison.Ordinal) && + x.HasResult == y.HasResult && + (Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result)); } - private bool StreamItemMessagesEqual(HubMessage x, HubMessage y) + private bool StreamItemMessagesEqual(StreamItemMessage x, StreamItemMessage y) { - return x is StreamItemMessage left && y is StreamItemMessage right && - (Equals(left.Item, right.Item) || SequenceEqual(left.Item, right.Item)); + return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && + (Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item)); } - private bool InvocationMessagesEqual(HubMessage x, HubMessage y) + private bool InvocationMessagesEqual(InvocationMessage x, InvocationMessage y) { - return x is InvocationMessage left && y is InvocationMessage right && - string.Equals(left.Target, right.Target, StringComparison.Ordinal) && - ArgumentListsEqual(left.Arguments, right.Arguments) && - left.NonBlocking == right.NonBlocking; + return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && + string.Equals(x.Target, y.Target, StringComparison.Ordinal) && + ArgumentListsEqual(x.Arguments, y.Arguments) && + x.NonBlocking == y.NonBlocking; } - private bool StreamInvocationMessagesEqual(HubMessage x, HubMessage y) + private bool StreamInvocationMessagesEqual(StreamInvocationMessage x, StreamInvocationMessage y) { - return x is StreamInvocationMessage left && y is StreamInvocationMessage right && - string.Equals(left.Target, right.Target, StringComparison.Ordinal) && - ArgumentListsEqual(left.Arguments, right.Arguments) && - left.NonBlocking == right.NonBlocking; - } - private bool CancelInvocationMessagesEqual(HubMessage x, HubMessage y) - { - return x is CancelInvocationMessage && y is CancelInvocationMessage; + return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && + string.Equals(x.Target, y.Target, StringComparison.Ordinal) && + ArgumentListsEqual(x.Arguments, y.Arguments) && + x.NonBlocking == y.NonBlocking; } private bool ArgumentListsEqual(object[] left, object[] right) diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs index a9c806748f..6c05efc117 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs @@ -7,8 +7,8 @@ using System.Linq; using System.Runtime.Serialization; using System.Security.Claims; using System.Threading; -using System.Threading.Tasks; using System.Threading.Channels; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR.Internal; @@ -1309,6 +1309,30 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task AcceptsPingMessages() + { + var serviceProvider = CreateServiceProvider(); + var endPoint = serviceProvider.GetService>(); + + using (var client = new TestClient(false, new JsonHubProtocol())) + { + var endPointLifetime = endPoint.OnConnectedAsync(client.Connection).OrTimeout(); + await client.Connected.OrTimeout(); + + // Send a ping + await client.SendHubMessageAsync(PingMessage.Instance).OrTimeout(); + + // Now do an invocation to make sure we processed the ping message + var completion = await client.InvokeAsync(nameof(MethodHub.ValueMethod)).OrTimeout(); + Assert.NotNull(completion); + + client.Dispose(); + + await endPointLifetime.OrTimeout(); + } + } + private static void AssertHubMessage(HubMessage expected, HubMessage actual) { // We aren't testing InvocationIds here