The HubConnection
class is the main entry point for a SignalR Core connection.
Protocols
SignalR Core supports different protocols to encode its messages like Json and MessagePack. The safest is to use json, as that’s the default encoder of the server. But if possible it’s recommended to use MessagePack. More can be read about this under the Encoders topic.
On this page the JsonProtocol
combined with the LitJsonEncoder
will be used, as these work out of the box.
A new HubConnection
object must be initialized with the uri of the server endpoint and with the protocol that the client want to communicate with:
hub = new HubConnection(new Uri("https://server/hub"), new JsonProtocol(new LitJsonEncoder()));
HubOptions
HubConnection
’s constructor can accept a HubOptions
instance too:
HubOptions options = new HubOptions();
hub = new HubConnection(new Uri("https://server/hub"), new JsonProtocol(new LitJsonEncoder()), options);
</br>
HubOptions
contains the following properties to set:
- SkipNegotiation: When this is set to true, the plugin will skip the negotiation request if the PreferedTransport is WebSocket. Its default value is false.
- PreferedTransport: The preferred transport to choose when more than one available. Its default value is TransportTypes.WebSocket. When the plugin can’t connect with the preferred transport it will try the next available (long polling). If all transport fails to connect, it will emit an OnError event.
- PingInterval: A ping message is only sent if the interval has elapsed without a message being sent. Its default value is 15 seconds.
- PingTimeoutInterval: If the client doesn’t see any message in this interval, considers the connection broken. Its default value is 30 seconds.
- MaxRedirects: The maximum count of redirect negoitiation result that the plugin will follow. Its default value is 100.
- ConnectTimeout: The maximum time that the plugin allowed to spend trying to connect. Its default value is 1 minute.
- WebsocketOptions: Customization options for the websocket transport. See the next, WebsocketOptions section for details.
WebsocketOptions
- ExtensionsFactory: A function to return with an array of
IExtension
. By default it returns with an array of one element, the per-message deflate extension. When returns withnull
, no extension will be negotiated with the server. It’s not available under WebGL. - PingIntervalOverride: With this property it’s possible to overwrite the default ping interval of the underlying websocket. When set to
TimeSpan.Zero
or lower, Websocket pings will be disabled. It’s not available under WebGL. It’s default value isTimeSpan.Zero
.
Events
- OnConnected: This event is called when successfully connected to the hub.
- OnError: This event is called when an unexpected error happen and the connection is closed.
- OnClosed: This event is called when the connection is gracefully terminated.
- OnMessage: This event is called for every server-sent message. When returns false, no further processing of the message is done by the plugin.
- OnReconnecting: Called when the HubConnection start its reconnection process after loosing its underlying connection.
- OnReconnected: Called after a succesfull reconnection.
- OnRedirected: This event is called when the connection is redirected to a new uri.
- OnTransportEvent: Called for transport related events:
- SelectedToConnect: Transport is selected to try to connect to the server
- FailedToConnect: Transport failed to connect to the server. This event can occur after SelectedToConnect, when already connected and an error occurs it will be a ClosedWithError one.
- Connected: The transport successfully connected to the server.
- Closed: Transport gracefully terminated.
- ClosedWithError: Unexpected error occured and the transport can’t recover from it.
Properties
- Uri: Uri of the Hub endpoint
- State: Current state of the connection.
- Transport: Current, active ITransport instance.
- Protocol: The IProtocol implementation that will parse, encode and decode messages.
- AuthenticationProvider: An IAuthenticationProvider implementation that will be used to authenticate the connection. Its default value is an instance of the
DefaultAccessTokenAuthenticator
class. - NegotiationResult: Negotiation response sent by the server.
- Options: Options that has been used to create the HubConnection.
- RedirectCount: How many times this connection is redirected.
- ReconnectPolicy: The reconnect policy that will be used when the underlying connection is lost. Its default value is null.
Connecting to the server
To start the protocol’s connection process the StartConnect
and ConnectAsync
functions can be used.
Invoking server methods
To invoke a method on a server that doesn’t return a value, the Send
and SendAsync
methods can be used.
Client code:
hub.Send("Send", "my message");
await hub.SendAsync("Send", "my message");
Or with a cancellation token:
using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(2)))
{
try
{
await hub.SendAsync("Send", source.Token, "my message");
}
catch(TaskCanceledException)
{
Debug.Log("Timed out!");
}
}
Its first parameter is the name of the method on the server, than a parameter list can be passed that will be sent to the server.
Related server code:
public class TestHub : Hub
{
public Task Send(string message)
{
return Clients.All.SendAsync("Send", $"{Context.ConnectionId}: {message}");
}
}
Invoking server functions
Invoking a server function can be done with the generic Invoke<TResult>
or InvokeAsync<TResult>
functions. TResult
is the expected type that the server function returns with.
Sample:
hub.Invoke<int>("Add", 10, 20)
.OnSuccess(result => Debug.log("10+20: " + result))
.OnError(error => Debug.log("Add(10, 20) failed to execute. Error: " + error));
var addResult = await hub.InvokeAsync<int>("Add", 10, 20);
AddText(string.Format("'<color=green>Add(10, 20)</color>' returned: '<color=yellow>{0}</color>'", addResult)).AddLeftPadding(20);
Invoke
returns with an IFuture<TResult>
that can be used to subscribe to various Invoke related events:
- OnSuccess: Callback passed for OnSuccess is called when the server side function is executed and the callback’s parameter will be function’s return value.
- OnError: Callback passed to this function will be called when there’s an error executing the function. The error can be a client or server error. The callback’s error parameter will contain information about the error.
- OnComplete: Callback passed to this function will be called after an OnSuccess or OnError callback.
InvokeAsync
returns with Task<TResult>
that can be awaited. As a second parameter a CancellationToken
can be added to cancel the call on client side.
using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(2)))
{
try
{
var addResult = await hub.InvokeAsync<int>("Add", source.Token, 10, 20);
// ...
}
catch(TaskCanceledException)
{
Debug.Log("Timed out!");
}
}
Related server code:
public class TestHub : Hub
{
public int Add(int x, int y)
{
return x + y;
}
}
Send
, Invoke
and theirs *Async counterparts are going to wait for a completion message from the server and their IFuture/Task completes when received it.Server callable client methods
Clients can define server-callable methods using the generic and non-generic On
method. The non-generic On
can be used when the server-callable method has no parameter and the generic one for methods with at least one parameter.
Samples:
// Generic On with one string argument.
hub.On("Send", (string arg) => Debug.log("Server-sent text: " + arg));
// Generic On, with one type:
hub.On<Person>("Person", (person) => Debug.log("Server-sent data: " + person.ToString()));
// Generic On, with two types:
hub.On<Person, Person>("TwoPersons", (person1, person2) => Debug.log("..."));
sealed class Person
{
public string Name { get; set; }
public long Age { get; set; }
public override string ToString()
{
return string.Format("[Person Name: '{0}', Age: '<color=yellow>{1}</color>']", this.Name, this.Age.ToString());
}
}
Related server code:
public class TestHub : Hub
{
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("Send", $"{Context.ConnectionId} joined");
await Clients.Client(Context.ConnectionId).SendAsync("Person", new Person { Name = "Person 007", Age = 35 });
await Clients.Client(Context.ConnectionId).SendAsync("TwoPersons", new Person { Name = "Person 008", Age = 36 }, new Person { Name = "Person 009", Age = 37 });
}
}
Server callable client functions
Server code that uses InvokeAsync
and expects an int
result:
public async Task SomeMethod(IHubContext<TestHub> context)
{
int min = 0, max = 10;
var randomValue = Random.Shared.Next(0, 10);
var result = await context.Clients.Client(Context.ConnectionId).InvokeAsync<int>("GetResult", $"Guess the value between {min} and {max}.", min, max);
if (result == randomValue)
{
await context.Clients.Client(Context.ConnectionId).SendAsync("EndResult", $"You guessed correctly ({result})!");
}
else
{
await context.Clients.Client(Context.ConnectionId).SendAsync("EndResult", $"You guessed incorrectly({result}), value was {randomValue}");
}
}
Client side code:
// Trigger SomeMethod on the server
hub.Send("SomeMethod");
// Function callback
hub.On<string, int, int, int>("GetResult", (string description, int min, int max) =>
{
UnityEngine.Debug.Log(description);
return UnityEngine.Random.Range(min, max);
});
// Final result called by the server
hub.On<string>("EndResult", (result) =>
{
UnityEngine.Debug.Log(result);
});
hub.Send("SomeMethod");
calls SomeMethod
on the server triggering the whole logic. The first callback for GetResult
receives the three arguments(description
, min
and max
) and must return with an int
result
When the server receives result
it continues its execution and compares it its own randomValue
calling EndResult
on the client.
Streaming from the server
When the server sends one return value at a time the client can call a callback for every item if the server function is called using the GetDownStreamController<TDown>
function.
Sample:
hub.GetDownStreamController<Person>("GetRandomPersons", 20, 2000)
.OnItem(result => Debug.log("New item arrived: " + result.ToString()))
.OnSuccess(_ => Debug.log("Streaming finished!"));
GetDownStreamController
’s return value is a DownStreamItemController<TDown>
that implements the IFuture<TResult>
interface. With DownStreamItemController’s OnItem function it can be subscribed for a callback that will be called for every downloaded item.
The instance of DownStreamItemController<TDown>
can be used to cancel the streaming:
var controller = hub.GetDownStreamController<int>("ChannelCounter", 10, 1000);
controller.OnItem(result => Debug.log("New item arrived: " + result.ToString()))
.OnSuccess(_ => Debug.log("Streaming finished!"))
.OnError(error => Debug.log("Error: " + error));
// A stream request can be cancelled any time by calling the controller's Cancel method
controller.Cancel();
Related server code:
public class TestHub : Hub
{
public ChannelReader<Person> GetRandomPersons(int count, int delay)
{
var channel = Channel.CreateUnbounded<Person>();
Task.Run(async () =>
{
Random rand = new Random();
for (var i = 0; i < count; i++)
{
await channel.Writer.WriteAsync(new Person { Name = "Name_" + rand.Next(), Age = rand.Next(20, 99) });
await Task.Delay(delay);
}
await Clients.Client(Context.ConnectionId).SendAsync("Person", new Person { Name = "Person 000", Age = 0 });
channel.Writer.TryComplete();
});
return channel.Reader;
}
}
Streaming to the server
To stream one or more parameters to a server function the GetUpStreamController
can be used:
private IEnumerator UploadWord()
{
var controller = hub.GetUpStreamController<string, string>("UploadWord");
controller.OnSuccess(result =>
{
Debug.log("Upload finished!");
});
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("Hello ");
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("World");
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("!!");
yield return new WaitForSeconds(_yieldWaitTime);
controller.Finish();
}
GetUpStreamController
is a generic function, its first type-parameter is the return type of the function then 1-5 types can be added as parameter types. The GetUpStreamController
call returns an UpStreamItemController
instance that can be used to upload parameters (UploadParam
), Finish
or Cancel
the uploading.
It also implements the IDisposable
interface so it can be used in a using statement and will call Finish when disposed. Here’s the previous sample using the IDisposable pattern:
using (var controller = hub.GetUpStreamController<string, string>("UploadWord"))
{
controller.OnSuccess(_ =>
{
Debug.log("Upload finished!");
});
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("Hello ");
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("World");
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam("!!");
yield return new WaitForSeconds(_yieldWaitTime);
}
The controller also implements the IFuture
interface to be able to subscribe to the OnSuccess
, OnError
and OnComplete
.
Related server code:
public class UploadHub : Hub
{
public async Task<string> UploadWord(ChannelReader<string> source)
{
var sb = new StringBuilder();
// receiving a StreamCompleteMessage should cause this WaitToRead to return false
while (await source.WaitToReadAsync())
{
while (source.TryRead(out var item))
{
Debug.WriteLine($"received: {item}");
Console.WriteLine($"received: {item}");
sb.Append(item);
}
}
// method returns, somewhere else returns a CompletionMessage with any errors
return sb.ToString();
}
}
Streaming to and from the server
After using GetDownStreamController
to stream results from the server and GetUpStreamController
to stream parameters to the server, there’s a third one to merge these two’s functionality, the GetUpAndDownStreamController
function. With its help we can stream parameters to a server-side function just like with GetUpStreamController
and stream down its result to the client like we can with GetDownStreamController
.
Here’s an example usage:
using (var controller = hub.GetUpAndDownStreamController<string, string>("StreamEcho"))
{
controller.OnSuccess(_ =>
{
Debug.log("Finished!");
});
controller.OnItem(item =>
{
Debug.log("On Item: " + item);
});
const int numMessages = 5;
for (int i = 0; i < numMessages; i++)
{
yield return new WaitForSeconds(_yieldWaitTime);
controller.UploadParam(string.Format("Message from client {0}/{1}", i + 1, numMessages));
}
yield return new WaitForSeconds(_yieldWaitTime);
}
GetUpAndDownStreamController
also returns with an UpStreamItemController
instance, but in this case its OnItem
can be used too. The callback passed to the OnItem
call will be called for every item the server sends back to the client.
Related server code:
public class UploadHub : Hub
{
public ChannelReader<string> StreamEcho(ChannelReader<string> source)
{
var output = Channel.CreateUnbounded<string>();
_ = Task.Run(async () =>
{
while (await source.WaitToReadAsync())
{
while (source.TryRead(out var item))
{
Debug.WriteLine($"Echoing '{item}'.");
await output.Writer.WriteAsync("echo:" + item);
}
}
output.Writer.Complete();
});
return output.Reader;
}
}
Send non-streaming parameter
GetUpStreamController
and GetUpAndDownStreamController
can send non-streaming parameters with theirs initial requests.
An example of sending multiple non-streaming and a streaming parameter:
public enum MyEnum
{
None,
One,
Two
}
public sealed class Metadata
{
public string strData;
public int intData;
public MyEnum myEnum;
}
using (var controller = hub.GetUpStreamController<int, Person>("MixedArgsTest", /*int: */ 1, /*string: */ "text test", new Metadata() { strData = "string data", intData = 5, myEnum = MyEnum.One }))
{
const int numMessages = 5;
for (int i = 0; i < numMessages; i++)
{
Person person = new Person()
{
Name = "Mr. Smith",
Age = 20 + i * 2
};
controller.UploadParam(person);
}
}
Server code:
public enum MyEnum
{
None,
One,
Two
}
public sealed class Metadata
{
public string strData;
public int intData;
public MyEnum myEnum;
}
public async Task<int> MixedArgsTest(ChannelReader<Person> source, int intParam, string stringParam, Metadata metadata)
{
int count = 0;
while (await source.WaitToReadAsync())
{
while (source.TryRead(out var item))
{
count++;
}
}
return count;
}