environment
.net core 3.1
前言
項目要求弄一個即時通訊
由於.net 已經集成了websocket通訊中間件-signalR,並且運作的效率還可以,爲減少開發週期便使用了signalR作爲websocket連接管理中間件。
既然是通訊,那就要解決最基本的連接問題。
如何連?以什麼作爲憑證?
既然是用戶與用戶之間的通信,那邊應該用用戶標識作爲憑證進行連接,無標識的連接(遊客)將毫無意義。
然後一般情況下(參考qq/微信),通訊時是不允許有一個用戶多個通訊連接的。既然如此,那便要考慮一個用戶二次連接的問題:
在這裏,我們選擇了第二次登錄,將之前登錄的用戶強制退出。
退出的方式有兩種:
- 客戶端自己斷開
- 服務端強制客戶斷開
隨意一點的(自己玩的那種)就客戶端自己斷開就足夠了,但如果是正式的項目的話還是要強制斷開,保證操作的完整性。
好,既然要強制斷開,那便是服務端移除連接.
先說結果:
一、在會話中斷開
會話中是指服務端已獲取到連接
使用場景:指客戶發送一個[關閉指令],然後服務端自動關閉連接
在Microsoft.AspNetCore.SignalR.Hub
中有一個public HubCallerContext Context { get; set; }
表示調用方的上下文。
然後在Microsoft.AspNetCore.SignalR.HubCallerContext
中提供了一個方法:
//
// Summary:
// Aborts the connection.
public abstract void Abort();// --> 使中止,中斷連接
故只需要在對應的方法塊中使用Context.Abort();
便可斷開連接
二、在會話外斷開
會話外是指服務端還未獲取到連接
使用場景:用戶在小米登錄了賬號然後又在華爲登錄了賬號,此時小米的賬號應該被強制下線。
根據場景觸發事件是華爲登錄了賬號,此時你不清楚小米的連接
於是我們要使用一個集合保留設備-》連接的映射關係:
ConcurrentDictionary<string, HubCallerContext> _connections // 此處key爲連接憑證,value爲此連接對應的上下文
注:HubCallerContext
是一個單例對象,即一個連接中的所有HubCallerContext
都是一樣的,故此方法可行
然後在連接開啓時即Hub.OnConnectedAsync
時維護此集合,若是存在一個用戶對應多個連接,你還需要維護一個用戶->連接憑證的集合
然後在華爲連接觸發OnConnectedAsync
時,檢查此集合是否已存在此憑證,若存在則取出對應上下文-HubCallerContext
調用Abort
進行強制退出
三、源碼分析
ps:若是你只是想知道服務端怎麼強制斷開連接的話,下面就不用看了
由於百度、Google都沒搜到需要的結果,只好自己來了...
強制斷開即是服務端移除連接
首先,想要釋放便得知道連接保存在哪
自己寫過websocket的應該都知道,當連接建立後,服務端需要將連接進行保存避免自動釋放,那麼signalR既然是封裝了websocket,那麼必然也存在類似的操作
貼一下signalR service註冊部分: Microsoft.Extensions.DependencyInjection.SignalRDependencyInjectionExtensions
services.TryAddSingleton<SignalRMarkerService>();
services.TryAddSingleton<SignalRCoreMarkerService>();
services.TryAddSingleton(typeof(HubLifetimeManager<>), typeof(DefaultHubLifetimeManager<>));
services.TryAddSingleton(typeof(IHubProtocolResolver), typeof(DefaultHubProtocolResolver));
services.TryAddSingleton(typeof(IHubContext<>), typeof(HubContext<>));
services.TryAddSingleton(typeof(IHubContext<, >), typeof(HubContext<, >));
services.TryAddSingleton(typeof(HubConnectionHandler<>), typeof(HubConnectionHandler<>));
services.TryAddSingleton(typeof(IUserIdProvider), typeof(DefaultUserIdProvider));
services.TryAddSingleton(typeof(HubDispatcher<>), typeof(DefaultHubDispatcher<>));
services.TryAddScoped(typeof(IHubActivator<>), typeof(DefaultHubActivator<>));
services.AddAuthorization();
SignalRServerBuilder signalRServerBuilder = new SignalRServerBuilder(services);
signalRServerBuilder.AddJsonProtocol();
先看app使用hub的地方:
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<XxxHub>("/xxxHub");
});
navigation->Microsoft.AspNetCore.Builder.HubEndpointRouteBuilderExtensions.HubEndpointRouteBuilderExtensions
public static class HubEndpointRouteBuilderExtensions
{
public static HubEndpointConventionBuilder MapHub<THub>(this IEndpointRouteBuilder endpoints, string pattern) where THub : Hub
{
return endpoints.MapHub<THub>(pattern, null);
}
public static HubEndpointConventionBuilder MapHub<THub>(this IEndpointRouteBuilder endpoints, string pattern, Action<HttpConnectionDispatcherOptions> configureOptions) where THub : Hub
{
if (endpoints.ServiceProvider.GetService<SignalRMarkerService>() == null)
{
throw new InvalidOperationException("Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code.");
}
HttpConnectionDispatcherOptions httpConnectionDispatcherOptions = new HttpConnectionDispatcherOptions();
configureOptions?.Invoke(httpConnectionDispatcherOptions);
ConnectionEndpointRouteBuilder connectionEndpointRouteBuilder = endpoints.MapConnections(pattern, httpConnectionDispatcherOptions, delegate(IConnectionBuilder b)
{
b.UseHub<THub>();
});
object[] attributes = typeof(THub).GetCustomAttributes(inherit: true);
connectionEndpointRouteBuilder.Add(delegate(EndpointBuilder e)
{
object[] array = attributes;
foreach (object item in array)
{
e.Metadata.Add(item);
}
e.Metadata.Add(new HubMetadata(typeof(THub)));
});
return new HubEndpointConventionBuilder(connectionEndpointRouteBuilder);
}
}
key code : b.UseHub<THub>();
navigation -> Microsoft.AspNetCore.SignalR.SignalRConnectionBuilderExtensions.SignalRConnectionBuilderExtensions
public static class SignalRConnectionBuilderExtensions
{
public static IConnectionBuilder UseHub<THub>(this IConnectionBuilder connectionBuilder) where THub : Hub
{
if (connectionBuilder.ApplicationServices.GetService(typeof(SignalRCoreMarkerService)) == null)
{
throw new InvalidOperationException("Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code.");
}
return connectionBuilder.UseConnectionHandler<HubConnectionHandler<THub>>();
}
}
navigation -> Microsoft.AspNetCore.Connections.ConnectionBuilderExtensions.ConnectionBuilderExtensions
public static IConnectionBuilder UseConnectionHandler<TConnectionHandler>(this IConnectionBuilder connectionBuilder) where TConnectionHandler : ConnectionHandler
{
TConnectionHandler handler = ActivatorUtilities.GetServiceOrCreateInstance<TConnectionHandler>(connectionBuilder.ApplicationServices);
return connectionBuilder.Run((ConnectionContext connection) => handler.OnConnectedAsync(connection));
}
OnConnectedAsync!!!,見名思以當連接打開時觸發,這個應該就是關鍵點了
navigation -> Microsoft.AspNetCore.Connections.ConnectionHandler
public override async Task OnConnectedAsync(ConnectionContext connection)
{
IList<string> list = _hubOptions.SupportedProtocols ?? _globalHubOptions.SupportedProtocols;
if (list == null || list.Count == 0)// 未配置連接協議
{
throw new InvalidOperationException("There are no supported protocols");
}
// 超時時間
TimeSpan timeout = _hubOptions.HandshakeTimeout ?? _globalHubOptions.HandshakeTimeout ?? HubOptionsSetup.DefaultHandshakeTimeout;
// 連接上下文配置
HubConnectionContextOptions contextOptions = new HubConnectionContextOptions
{
KeepAliveInterval = (_hubOptions.KeepAliveInterval ?? _globalHubOptions.KeepAliveInterval ?? HubOptionsSetup.DefaultKeepAliveInterval),
ClientTimeoutInterval = (_hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval),
StreamBufferCapacity = (_hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? 10),
MaximumReceiveMessageSize = _maximumMessageSize
};
Log.ConnectedStarting(_logger);
// **** 構建連接對象
HubConnectionContext connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory);
IReadOnlyList<string> supportedProtocols = (list as IReadOnlyList<string>) ?? list.ToList();
// 然後進行握手連接
if (await connectionContext.HandshakeAsync(timeout, supportedProtocols, _protocolResolver, _userIdProvider, _enableDetailedErrors))
{ // 握手成功
try
{
await _lifetimeManager.OnConnectedAsync(connectionContext);
await RunHubAsync(connectionContext);
}
finally
{
Log.ConnectedEnding(_logger);
await _lifetimeManager.OnDisconnectedAsync(connectionContext);
}
}
}
主要看握手成功之後的內容:
try
{
await _lifetimeManager.OnConnectedAsync(connectionContext);
await RunHubAsync(connectionContext);
}
finally
{
Log.ConnectedEnding(_logger);
await _lifetimeManager.OnDisconnectedAsync(connectionContext);
}
首先可以看到在finally
中調用了OnDisconnectedAsync
,見名思以我覺得它應該就是我們要找的釋放連接,查看定義:
private readonly HubLifetimeManager<THub> _lifetimeManager;
而且通過之前的註冊來看此成員是一個單例,感覺非常符合,繼續查看定義: Microsoft.AspNetCore.SignalR.HubLifetimeManager
public abstract class HubLifetimeManager<THub> where THub : Hub
{
public abstract Task OnConnectedAsync(HubConnectionContext connection);
public abstract Task OnDisconnectedAsync(HubConnectionContext connection);
public abstract Task SendAllAsync(string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendConnectionAsync(string connectionId, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendGroupAsync(string groupName, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendGroupsAsync(IReadOnlyList<string> groupNames, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task SendUsersAsync(IReadOnlyList<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default(CancellationToken));
public abstract Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default(CancellationToken));
}
到此有點小懵逼了???,這裏的方法都是返回Task
,但我釋放連接需要HubConnectionContext
,豈不是無解???
雖然很像但是不是就很鬱悶,既然_lifetimeManager
做不了,就只能去看:
await RunHubAsync(connectionContext);
private async Task RunHubAsync(HubConnectionContext connection)
{
try
{
await _dispatcher.OnConnectedAsync(connection);
}
catch (Exception exception)
{
Log.ErrorDispatchingHubEvent(_logger, "OnConnectedAsync", exception);
await SendCloseAsync(connection, exception, allowReconnect: false);
return;
}
try
{
await DispatchMessagesAsync(connection);
}
catch (OperationCanceledException)
{
}
catch (Exception exception2)
{
Log.ErrorProcessingRequest(_logger, exception2);
await HubOnDisconnectedAsync(connection, exception2);
return;
}
await HubOnDisconnectedAsync(connection, null);
}
一個一個來,先看await _dispatcher.OnConnectedAsync(connection);
:
navigation -> Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
IServiceScope scope = null;
try
{
// 通過 service 拿到了THub
scope = _serviceScopeFactory.CreateScope();
IHubActivator<THub> hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
THub hub = hubActivator.Create();
try
{
// 然後通過hub 和 連接進行初始化
InitializeHub(hub, connection);
await hub.OnConnectedAsync();
}
finally
{
hubActivator.Release(hub);
}
}
finally
{
await scope.DisposeAsync();
}
}
private void InitializeHub(THub hub, HubConnectionContext connection)
{
hub.Clients = new HubCallerClients(_hubContext.Clients, connection.ConnectionId); // 只用到了ConnectionId顯然不是
hub.Context = connection.HubCallerContext; // 這個就有點可疑了
hub.Groups = _hubContext.Groups;// 只用到了分組應該也不是
}
查看HubConnectionContext
的構造方法看看HubCallerContext
是如何被構造的:
public HubConnectionContext(ConnectionContext connectionContext, HubConnectionContextOptions contextOptions, ILoggerFactory loggerFactory)
{
...
HubCallerContext = new DefaultHubCallerContext(this);
...
}
navigation -> Microsoft.AspNetCore.SignalR.Internal.DefaultHubCallerContext
internal class DefaultHubCallerContext : HubCallerContext
{
private readonly HubConnectionContext _connection;
public override string ConnectionId => _connection.ConnectionId;
public override string UserIdentifier => _connection.UserIdentifier;
public override ClaimsPrincipal User => _connection.User;
public override IDictionary<object, object> Items => _connection.Items;
public override IFeatureCollection Features => _connection.Features;
public override CancellationToken ConnectionAborted => _connection.ConnectionAborted;
public DefaultHubCallerContext(HubConnectionContext connection)
{
_connection = connection;
}
public override void Abort()
{
// ************************
_connection.Abort();
}
}
Abort -> 使中止 推測是中止連接,而且通過源碼可知調的是HubConnectionContext.Abort
.
Hub中的定義:
public HubCallerContext Context
{
get
{
CheckDisposed();
return _context;
}
set
{
CheckDisposed();
_context = value;
}
}
通過測試結果可知,這個便是服務器中斷連接的方法了
[Over~]