C#-网络通讯框架(三)-SignalR(SignalRCore版)_Net7
视频演示地址:
一、概念
1、什么是 SignalR?
ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。
2、适合 SignalR 的场景
- 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
- 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。
- 协作应用。 协作应用的示例包括白板应用和团队会议软件。
- 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。
3、SignalR实时通信使用的传输技术(以下为使用的先后选择顺序,自动选择服务器和客户端能力范围内的最佳传输方法)
- WebSockets
- Server-Sent Events
- 长轮询
4、SignalR支持的功能
- 自动处理连接管理。
- 同时向所有连接的客户端发送消息。 例如聊天室。
- 向特定客户端或客户端组发送消息。
- 对其进行缩放,以处理不断增加的流量。
- SignalR中心协议(SignalR 使用 Hubs(中心协议) 在客户端和服务器之间进行通信;内置中心协议:基于JSON的文本协议 和 基于MessagePack 的二进制协议(JSON的代替方案,消息更小), 旧版浏览器必须支持XHR级别2 才能提供 MessagePack 协议支持。)
5、SignalR服务器与客户端标准
服务器标准:支持 ASP.NET Core 支持的任何服务器平台;如果服务器运行 IIS中,则 WebSockets 传输需要 Windows Server 2012 或更高版本上的 IIS 8.0 或更高版本。
Web客户端标准:SignalR 面向 ES6。 对于不支持 ES6 的浏览器,请将库转译为 ES5。 有关详细信息,请参阅使用ES6入门 – 使用 Traceur和Babel 将 ES6转为ES5。
.Net客户端标准:.NET 客户端可在 ASP.NET Core 支持的任何平台上运行(Xamarin.Android版本>8.4.0.1;Xamarin.iOS版本>11.14.0.4)。
Java 客户端标准:支持 Java 8 及更高版本。
二、知识点
1、创建SignalR Server
Microsoft.AspNetCore.App已集成了Microsoft.AspNetCore.SignalR包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包
(1)添加SignalR服务
var builder = WebApplication.CreateBuilder(args);
// builder.Services.AddRazorPages(); // Razor应用时,AddSignalR在AddRazorPages后
builder.Services.AddSignalR(); // 1、添加SignalR服务
(2)启用SignalR服务
app.MapRazorPages();
app.MapHub<ChatHub>("/Chat"); // 2、启用SignalR服务
app.Run();
(3)创建并使用自定义Hub-ChatHub
- 自定义方法名:使用[HubMethodName("方法名")]特性标识方法;
- 可以使用DI中的服务作为参数;
- 可以使用OnConnectedAsync 和 OnDisconnectedAsync 虚拟方法来管理和跟踪连接;
- 中心方法中引发的异常将发送到调用该方法的客户端;如下图的SendMessage被客户端调用,发生的异常会反馈给客户端。
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat_Js.Hubs
{
/// <summary>
/// 聊天Hub(管理聊天用胡的连接、组和消息)
/// 继承Hub(管理连接、组和消息)
/// SignalR 代码是异步模式,可提供最大的可伸缩性
/// </summary>
public class ChatHub : Hub
{
/// <summary>
/// 发送信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
/// <returns></returns>
//[HubMethodName("SendMessageToUser")] // 自定义方法名使用:[HubMethodName("方法名")]
public async Task SendMessage(string user, string message,IDatabaseService dbService) // 可以使用DI中的服务作为参数;services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
{
await Clients.All.SendAsync("ReceiveMessage", user, message); // 调用ReceiveMessage方法
}
// 连接成功事件示例-将其ConnectionId添加到组SignalR Users
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}
// 连接断开事件示例-记录异常
public override async Task OnDisconnectedAsync(Exception? exception)
{
// Log(exception.Message); // 记录异常
await base.OnDisconnectedAsync(exception);
}
}
}
(4)服务器中(ChatHub之外)使用ChatHub(通过IHubContext调用)
- 可以将
IHubContext
实例注入控制器、中间件或其他 DI 服务。 - 当从
Hub
类外部调用客户端方法时,没有与该调用关联的调用方。 因此,无法访问ConnectionId
、Caller
和Others
属性。 - 注入强类型 HubContext时(见‘(5)强类型改造’),请确保中心继承自
Hub<T>
。 使用IHubContext<THub, T>
接口而不是IHubContext<THub>
进行注入。 - 在泛型代码中使用 IHubContext时,注入的 IHubContext<THub> 实例可以强制转换为 IHubContext,而无需指定泛型 Hub 类型。
// Controller控制器中
/// <summary>
/// 公告通知
/// </summary>
public class SysNoticeController : Controller
{
private readonly IHubContext<MessageHub> _hubContext;
public SysNoticeController(IHubContext<MessageHub> hubContext)
{
_hubContext = hubContext;
}
/// <summary>
/// 发送通知
/// </summary>
public void SendNotice(msg)
{
_hubContext.Clients.All.SendAsync("ReceiveMessage", "系统通知", message); // 调用ReceiveMessage方法
}
}
// 中间件管道中
app.Use(async (context, next) =>
{
var hubContext = context.RequestServices
.GetRequiredService<IHubContext<ChatHub>>();
//...
if (next != null)
{
await next.Invoke();
}
});
// Controller控制器-强类型
public class ChatController : Controller
{
public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }
public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)
{
_strongChatHubContext = chatHubContext;
}
public async Task SendMessage(string user, string message)
{
await _strongChatHubContext.Clients.All.ReceiveMessage(user, message);
}
}
/*
* 在泛型代码中使用 IHubContext
* 注入的 IHubContext<THub> 实例可以强制转换为 IHubContext,而无需指定泛型 Hub 类型。
*/
class MyHub : Hub
{ }
class MyOtherHub : Hub
{ }
app.Use(async (context, next) =>
{
var myHubContext = context.RequestServices
.GetRequiredService<IHubContext<MyHub>>();
var myOtherHubContext = context.RequestServices
.GetRequiredService<IHubContext<MyOtherHub>>();
await CommonHubContextMethod((IHubContext)myHubContext);
await CommonHubContextMethod((IHubContext)myOtherHubContext);
await next.Invoke();
}
async Task CommonHubContextMethod(IHubContext context)
{
await context.Clients.All.SendAsync("clientMethod", new Args());
}
2、创建SignalR Client
- 客户端断线重连后,会被赋予一个新的
ConnectionId
,可通过Reconnected
事件获取。(注:如果 HubConnection 已配置为跳过协商,则Reconnected 事件处理程序的 connectionId 参数会为 null) - 若使用重连方法,推荐异步进行。
(1).Net版
见“三-5”示例
(2)Javascript版
见“三-1”示例
3、Hub知识点
(1)Hub.Context(用户标识:Context.UserIdentifier)
类 Hub 包含一个 Context 属性,该属性包含以下属性以及有关连接的信息:
属性 | 说明 |
---|---|
ConnectionId | 获取连接的唯一 ID(由 SignalR 分配)。 每个连接都有一个连接 ID。 |
UserIdentifier | 获取用户标识符。 默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。用户标识符区分大小写。 |
User | 获取与当前用户关联的 ClaimsPrincipal。 |
Items | 获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。 |
Features | 获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。 |
ConnectionAborted | 获取一个 CancellationToken,它会在连接中止时发出通知。 |
Hub.Context 还包含以下方法:
方法 | 说明 |
---|---|
GetHttpContext | HttpContext返回连接的 ;如果连接未与 HTTP 请求关联,null 则返回 。 对于 HTTP 连接,请使用此方法获取 HTTP 标头和查询字符串等信息。 |
Abort | 中止连接。 |
(2)Hub.Clients
- 当从
Hub
类外部调用客户端方法时,没有与该调用关联的调用方。 因此,无法访问ConnectionId
、Caller
和Others
属性。 - 用户组
- 对组进行操作可以使用Client.Group()或Groups.AddToGroupAsync(Context.ConnectionId, groupName)、Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
- 重新连接时不会保留组成员身份。 重新建立连接后,需要重新加入组。
- 无法计算组的成员数,因为如果将应用程序扩展到多台服务器,则无法获取此信息。需要自己实现。
- 组不是一项安全功能。 身份验证声明具有组不具备的功能,例如到期和撤销。 如果撤销用户对组的访问权限,应用必须从组中显式删除该用户。
- 组名称区分大小写。
类 Hub 包含一个 Clients 属性,该属性包含以下属性,用于服务器和客户端之间的通信:
Hub.Clients 还包含以下方法:
方法 | 说明 |
---|---|
AllExcept | 对所有连接的客户端调用方法(指定连接除外) |
Client | 对连接的一个特定客户端调用方法 |
Clients | 对连接的多个特定客户端调用方法 |
Group | 对指定组中的所有连接调用方法 |
GroupExcept | 对指定组中的所有连接调用方法(指定连接除外) |
Groups | 对多个连接组调用方法 |
OthersInGroup | 对一个连接组调用方法(不包括调用了中心方法的客户端) |
User | 对与一个特定用户关联的所有连接调用方法 |
Users | 对与多个指定用户关联的所有连接调用方法 |
(3)向客户端发送消息(SendAsync)
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message); // 使用 Clients.All 将消息发送到所有连接的客户端
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.SendAsync("ReceiveMessage", user, message); // 使用 Clients.Caller 将消息发送回调用方
public async Task SendMessageToGroup(string userGroup,string user, string message)
=> await Clients.Group(userGroup).SendAsync("ReceiveMessage", user, message); // 将消息发送给 SignalR Users 组中的所有客户端
public async Task SendMessageToGroup(string receiveUser,string user, string message)
=> await Clients.User(receiveUser).SendAsync("ReceiveMessage", user, message); // 将消息发送给 user用户
(4)从客户端请求结果(示例为GetMessage方法)
① 服务器端
这要求服务器使用 ISingleClientProxy.InvokeAsync
,并且客户端从其 .On
处理程序返回结果。示例如下:
方式一:接口中使用(推荐)
// 接口中调用
async void SomeMethod(IHubContext<MyHub> context)
{
string result = await context.Clients.Client(connectionID).InvokeAsync<string>("GetMessage"); // 通过InvokeAsync调用。
}
方式二:写在自定义的Hub中
public class ChatHub : Hub
{
public async Task<string> WaitForMessage(string connectionId)
{
var message = await Clients.Client(connectionId).InvokeAsync<string>("GetMessage");
return message;
}
}
② 客户端
// C#版
hubConnection.On("GetMessage", async () =>
{
Console.WriteLine("Enter message:");
var message = await Console.In.ReadLineAsync();
return message;
});
// javascript版
hubConnection.on("GetMessage", async () => {
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("message");
}, 100);
});
return promise;
});
// Java版
hubConnection.onWithResult("GetMessage", () -> {
return Single.just("message");
});
(5)对SendAsync中“调用的方法名”进行强类型改造(示例为ReceiveMessage
方法)
使用 SendAsync
的缺点是它依赖于字符串来指定要调用的客户端方法。 如果客户端中的方法名称拼写错误或缺失,则这会使代码可能出现运行时错误。我们可以使用 Hub<T>强类型Hub类来约束自定义的Hub类(使用强类型 Hub<T> 会禁止使用 SendAsync,只提供对 接口中定义的方法的访问),如下:
/// <summary>
/// 聊天Hub(管理聊天用胡的连接、组和消息)
/// 继承Hub(管理连接、组和消息)
/// SignalR 代码是异步模式,可提供最大的可伸缩性
/// </summary>
public class ChatHub : Hub<IChatClient> // 使用强类型 Hub<T> 会禁止使用 SendAsync,只提供对 接口中定义的方法的访问。
{
//旧的方法
//public async Task SendMessage(string user, string message)
// => await Clients.All.SendAsync("ReceiveMessage", user, message); // 调用ReceiveMessage方法
/// <summary>
/// 发送信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
/// <returns></returns>
public async Task SendMessage(string user, string message)
=> await Clients.All.ReceiveMessage(user, message);
/// <summary>
/// 发送信息-将消息发送回调用方
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
/// <returns></returns>
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.ReceiveMessage(user, message);
/// <summary>
/// 发送信息-将消息发送给 SignalR Users 组中的所有客户端
/// </summary>
/// <param name="user">用户组</param>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
/// <returns></returns>
public async Task SendMessageToGroup(string userGroup,string user, string message)
=> await Clients.Group(userGroup).ReceiveMessage(user, message);
}
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}
(6)自定义传输对象(方法参数;增强历史版本的兼容性)
① 修改前(服务器自定义的方法)
以如下所示的服务器端 API 为例:
public async Task<string> GetTotalLength(string param1)
{
return param1.Length;
}
JavaScript 客户端使用 invoke
调用此方法,如下所示:
connection.invoke("GetTotalLength", "value1");
如果稍后将第二个参数添加到服务器方法,旧客户端不会提供此参数值。 例如:
public async Task<string> GetTotalLength(string param1, string param2)
{
return param1.Length + param2.Length;
}
当旧客户端尝试调用此方法时,会收到如下错误:
Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength' due to an error on the server.
在服务器上,你将看到如下日志消息:
System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 2.
旧客户端只发送了一个参数,但新的服务器 API 需要两个参数。 使用自定义对象作为参数可提供更大的灵活性。 让我们重新设计原始 API 以使用自定义对象。
② 修改后(服务器自定义的方法)
单参
public class TotalLengthRequest
{
public string Param1 { get; set; }
}
public async Task GetTotalLength(TotalLengthRequest req)
{
return req.Param1.Length;
}
现在,客户端使用对象来调用方法:
connection.invoke("GetTotalLength", { param1: "value1" });
添加参数
public class TotalLengthRequest
{
public string Param1 { get; set; }
public string Param2 { get; set; }
}
public async Task GetTotalLength(TotalLengthRequest req)
{
var length = req.Param1.Length;
if (req.Param2 != null)
{
length += req.Param2.Length;
}
return length;
}
当旧客户端发送单个参数时,额外的 Param2
属性将保留为 null
。 你可以通过检查 Param2
是否为 null
来检测旧客户端发送的消息并应用默认值。 新客户端可以发送这两个参数。
connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });
③ 补充-客户端上自定义的方法(new {内容})
从服务器端发送自定义对象:
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("ReceiveMessage", new
{
Message = message
});
}
在客户端,访问 Message
属性而不是使用参数:
connection.on("ReceiveMessage", (req) => {
let msg=req.message;
});
如果稍后决定将消息的发送方添加到有效负载中,请向对象添加一个属性:
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("ReceiveMessage", new
{
Sender = Context.User.Identity.Name,
Message = message
});
}
旧客户端不需要 Sender
值,因此会忽略它。 新客户端可以通过更新为读取新属性来接受它:
connection.on("ReceiveMessage", (req) => {
let message = req.message;
if (req.sender) {
message = req.sender + ": " + message;
}
});
在这种情况下,新客户端也可以容忍不提供 Sender
值的旧服务器。 由于旧服务器不提供 Sender
值,因此客户端在访问它之前会检查它是否存在。
(7)中心筛选器
- 在 ASP.NET Core 5.0 或更高版本中可用。
- 允许在客户端调用中心方法之前和之后运行逻辑。
- 中心筛选器可以全局应用或按中心类型应用。
- 筛选器的添加顺序就是其运行顺序。 全局中心筛选器在本地中心筛选器之前运行。
① 配置中心筛选器
builder.Services.AddSignalR(options =>
{
// 全局筛选器将首先运行
options.AddFilter<CustomFilter>();
}).AddHubOptions<ChatHub>(options =>
{
// 本地筛选器在全局筛选器后运行
options.AddFilter<LocCustomFilter>();
}).AddHubOptions<ChatHub2>(options =>
{
// 本地筛选器按照顺序运行
options.AddFilter<LocCustomFilter2>();
});
中心筛选器有三种添加方式:
// 1、按具体类型添加筛选器:
hubOptions.AddFilter<TFilter>(); // 将通过依赖项注入 (DI) 或激活的类型来解析。
// 2、按运行时类型添加筛选器:
hubOptions.AddFilter(typeof(TFilter)); // 将通过依赖项注入 (DI) 或激活的类型来解析。
// 3、按实例添加筛选器:
hubOptions.AddFilter(new MyFilter()); // 将像单一实例一样使用此实例;所有中心方法调用都将使用相同的实例。
② 创建中心筛选器
- 通过声明从
IHubFilter
继承的类来创建筛选器,并添加InvokeMethodAsync
方法。 - 还可以选择实现
OnConnectedAsync
和OnDisconnectedAsync
,以分别包装OnConnectedAsync
和OnDisconnectedAsync
中心方法。 - 使用
next
方法调用下一个筛选器。 - 若要跳过筛选器中的中心方法调用,请引发
HubException
类型的异常,而不是调用next
。 如果客户端需要结果,则会收到错误。
// 继承IHubFilter
public class CustomFilter : IHubFilter
{
// 添加InvokeMethodAsync方法
public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
{
Console.WriteLine($"Calling hub method '{invocationContext.HubMethodName}'");
try
{
return await next(invocationContext);
}
catch (Exception ex)
{
Console.WriteLine($"Exception calling '{invocationContext.HubMethodName}': {ex}");
throw;
}
}
// 实现连接成功事件
public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
{
return next(context);
}
// 实现断开连接事件
public Task OnDisconnectedAsync(HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
{
return next(context, exception);
}
}
③ 使用中心筛选器的示例
- 编写筛选器逻辑时,请尝试通过在中心方法上使用属性而不是检查中心方法名称,使其成为泛型逻辑。
以某个筛选器为例,该筛选器将检查中心方法参数中是否有被禁短语,并将找到的任何短语替换为 ***
。 对于此示例,假设定义了LanguageFilterAttribute
类。 该类有一个名为FilterArgument
的属性,可以在使用该属性时对其进行设置。
a. 将该属性放在需要清理字符串参数的中心方法上:
public class ChatHub
{
[LanguageFilter(filterArgument = 0)]
public async Task SendMessage(string message, string username)
{
await Clients.All.SendAsync("SendMessage", $"{username} says: {message}");
}
}
b. 定义一个中心筛选器,以检查该属性并将中心方法参数中的被禁短语替换为 ***
:
public class LanguageFilter : IHubFilter
{
// populated from a file or inline
private List<string> bannedPhrases = new List<string> { "async void", ".Result" };
public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext,
Func<HubInvocationContext, ValueTask<object>> next)
{
var languageFilter = (LanguageFilterAttribute)Attribute.GetCustomAttribute(
invocationContext.HubMethod, typeof(LanguageFilterAttribute));
if (languageFilter != null &&
invocationContext.HubMethodArguments.Count > languageFilter.FilterArgument &&
invocationContext.HubMethodArguments[languageFilter.FilterArgument] is string str)
{
foreach (var bannedPhrase in bannedPhrases)
{
str = str.Replace(bannedPhrase, "***");
}
var arguments = invocationContext.HubMethodArguments.ToArray();
arguments[languageFilter.FilterArgument] = str;
invocationContext = new HubInvocationContext(invocationContext.Context,
invocationContext.ServiceProvider,
invocationContext.Hub,
invocationContext.HubMethod,
arguments);
}
return await next(invocationContext);
}
}
c. 在Startup.ConfigureServices
方法中注册中心筛选器。 为了避免每次调用都重新初始化被禁短语列表,中心筛选器将注册为单一实例:
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR(hubOptions =>
{
hubOptions.AddFilter<LanguageFilter>();
});
services.AddSingleton<LanguageFilter>();
}
④ HubInvocationContext 对象
HubInvocationContext
包含当前中心方法调用的信息。
属性 | 说明 | 类型 |
---|---|---|
Context |
HubCallerContext 包含有关连接的信息。 |
HubCallerContext |
Hub |
用于此中心方法调用的中心实例。 | Hub |
HubMethodName |
正在调用的中心方法的名称。 | string |
HubMethodArguments |
传递给中心方法的参数列表。 | IReadOnlyList<string> |
ServiceProvider |
用于此中心方法调用的已限定范围的服务提供程序。 | IServiceProvider |
HubMethod |
中心方法信息。 | MethodInfo |
⑤ HubLifetimeContext 对象
HubLifetimeContext
包含 OnConnectedAsync
和 OnDisconnectedAsync
中心方法的信息。
属性 | 说明 | 类型 |
---|---|---|
Context |
HubCallerContext 包含有关连接的信息。 |
HubCallerContext |
Hub |
用于此中心方法调用的中心实例。 | Hub |
ServiceProvider |
用于此中心方法调用的已限定范围的服务提供程序。 | IServiceProvider |
三、示例(聊天室):
官方示例地址:SignalR-samples(Android、WinFrom、Xamarin、UWP、Js等)
1、JavaScript使用SignalR教程
教程见:ASP.NET Core SignalR 入门(Microsoft.AspNetCore.App已集成的包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包)
代码地址:csharp_networkprotocol_signalr/SignalRChat_Js
2、TypeScript使用SignalR教程
教程见:使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR
3、Blazor使用SignalR教程
教程见:结合使用 ASP.NET Core SignalR 和 Blazor
4、WebAPI+Vue3使用SignalR教程
代码地址:csharp_networkprotocol_signalr/SignalRServer_WebAPI与csharp_networkprotocol_signalr\signalrvue。
(1)后台代码(WebAPI)
Microsoft.AspNetCore.App已集成的包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包
① 启用SignalR(Program.cs)
using Microsoft.AspNetCore.SignalR;
...
// 1、添加Cors跨域
builder.Services.AddCors(options => // 配置Cors跨域
{
options.AddPolicy(name: "MyPolicy",
policy =>
{
policy
.WithOrigins("http://localhost:8080") //.WithOrigins("http://localhost:8080/", "https://localhost:8080/", "http://localhost:8080", "http://localhost:5000/chatHub")
//.AllowAnyOrigin() // 允许所有域
.AllowAnyHeader() // 约束HTTP请求头-允许所有请求头;.WithHeaders("Content-Language","Content-Type");
.AllowCredentials()//允许cookie
.AllowAnyMethod(); // 约束HTTP方法-允许所有方法;.WithMethods("PUT", "DELETE", "GET","POST")
});
});
//2、添加SignalR服务,默认为json传输
builder.Services.AddSignalR(); //
//builder.Services.AddSignalR(options =>
//{
// //客户端发保持连接请求到服务端最长间隔,默认30秒,改成4分钟,网页需跟着设置connection.keepAliveIntervalInMilliseconds = 12e4;即2分钟
// //options.ClientTimeoutInterval = TimeSpan.FromMinutes(4);
// //服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.serverTimeoutInMilliseconds = 24e4;即4分钟
// //options.KeepAliveInterval = TimeSpan.FromMinutes(2);
//});
app.UseCors("MyPolicy"); // 3、启用Cors跨域
app.MapHub<ChatHub>("/chatHub"); // 4、启用SignalR服务
② ChatHub.cs
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat_Js.Hubs
{
/// <summary>
/// 聊天Hub(管理聊天用胡的连接、组和消息)
/// 继承Hub(管理连接、组和消息)
/// SignalR 代码是异步模式,可提供最大的可伸缩性
/// </summary>
public class ChatHub : Hub
{
/// <summary>
/// 发送信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
/// <returns></returns>
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
③ 接口中使用
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SignalRChat_Js.Hubs;
namespace SignalRServer_WebAPI.Controllers
{
/// <summary>
/// 公告通知
/// </summary>
public class SysNoticeController : Controller
{
private readonly IHubContext<ChatHub> _hubContext;
public SysNoticeController(IHubContext<ChatHub> hubContext)
{
_hubContext = hubContext;
}
/// <summary>
/// 发送通知
/// </summary>
public void SendNotice(string message)
{
_hubContext.Clients.All.SendAsync("ReceiveMessage", "系统通知", message); // 调用ReceiveMessage方法
}
}
}
(2)前台代码(Vue)
① 引用signalr库
npm install @microsoft/signalr
② Chat/Index.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<!--
<p>
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
-->
<div class="container">
<div class="row p-1">
<div class="col-1">用户名</div>
<div class="col-5"><input type="text" id="userInput" /></div>
</div>
<div class="row p-1">
<div class="col-1">消息内容</div>
<div class="col-5">
<input type="text" class="w-100" id="messageInput" />
</div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input
type="button"
id="sendButton"
value="发送"
@click="sendMessageClicked"
/>
</div>
</div>
<div class="row p-1">
<div class="col-6">
<hr />
</div>
</div>
<div class="row p-1">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
</div>
</template>
<script>
// npm install @microsoft/signalr
//import { signalR } from "@/js/signalr/dist/browser/signalr.js";
import * as signalR from "@microsoft/signalr";
let connection;
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
// connection: undefined,
};
},
created() {},
mounted() {
this.OnLoad();
},
methods: {
OnLoad() {
// 1、创建并启动连接,连接地址为/chatHub
connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect() // 自动重连
.withUrl("http://localhost:5000/chatHub")
.build();
// 2、分类为ReceiveMessage
connection.on("ReceiveMessage", function (user, message) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
li.textContent = `【${user}】 说: ${message}`;
});
// 重连失败后的关闭事件
connection.onclose((error) => {
// 展示断开的信息
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
li.textContent = `连接断开,重连中!错误信息:` + error.message;
connection.start().catch(function (err) {
console.log(err.toString());
});
});
// 3、连接成功后,发送 按钮可用
connection
.start()
.then(function () {
document.getElementById("sendButton").disabled = false;
})
.catch(function (err) {
console.log(err.toString());
});
},
// 发送事件
sendMessageClicked() {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection
.invoke("SendMessage", "SignalRVue_"+user, message)
.catch(function (err) {
return console.log("错误信息"+err.toString());
});
//this.$message.success("消息发送成功");
console.log("消息发送成功");
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
5、.Net客户端使用教程SignalR-WinForm/MAUI
代码地址:
-
- 服务器端:csharp_networkprotocol_signalr/SignalRServer_WebAPI
- MAUI客户端:csharp_networkprotocol_signalr/SignalRClient_MauiApp
- WinForm客户端:csharp_networkprotocol_signalr/SignalRClient_WinForm
(1)服务端:
见4-(1)-WebAPI代码。
(2)客户端(WinForm版;MAUI版略,直接看代码):
① 安装Negut包:
Install-Package Microsoft.AspNetCore.SignalR.Client
② 连接/断开、接收功能。
/// <summary>
/// 连接/断开
/// </summary>
private async void btnOpen_Click(object sender, EventArgs e)
{
try
{
if (!IsConnection) // 打开连接
{
adress = txtAdress.Text;
_connection = new HubConnectionBuilder()
.WithUrl(adress) // "http://localhost:5000/chatHub"
.WithAutomaticReconnect() // 自动重连(默认不会自动重连);在没有任何参数的情况下,WithAutomaticReconnect() 将客户端配置为在每次尝试重新连接之前分别等待 0、2、10 和 30 秒,在四次尝试失败后停止。
.Build();
_connection.Reconnecting += ReconnectingEvent; // 重连事件
_connection.Reconnected += ReconnectedEvent; // 重连成功事件
_connection.Closed += ClosedEvent; // 重连失败后的断开事件
_connection.On<string, string>("ReceiveMessage", (s1, s2) => OnSend(s1, s2)); // 注册监听事件
await _connection.StartAsync();
IsConnection = true;
UpdateState(IsConnection);
Log("连接成功!地址:" + adress);
txtMsg.Focus();
}
else // 关闭连接
{
IsConnection = false;
await _connection.StopAsync();
UpdateState(false);
}
}
catch (Exception ex)
{
IsConnection = false;
UpdateState(IsConnection);
Log(ex.ToString());
}
}
④ 发送功能
/// <summary>
/// 发送
/// </summary>
private async void btnSend_Click(object sender, EventArgs e)
{
try
{
string userStr = txtUser.Text;
string msgStr = txtMsg.Text;
await _connection.InvokeAsync("SendMessage", "WinFormsApp_"+ userStr, msgStr); // 调用Server的SendMessage方法
}
catch (Exception ex)
{
Log(ex.ToString());
}
}
⑤ 事件
/// <summary>
/// 接收事件
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">信息</param>
private void OnSend(string user, string message)
{
Log($"【{user}】说:{message}");
}
/// <summary>
/// 自动重连事件
/// </summary>
private Task ReconnectingEvent(Exception? arg)
{
// 可记录本次异常
return Task.CompletedTask;
}
/// <summary>
/// 重连成功事件
/// </summary>
private Task ReconnectedEvent(string? arg)
{
// 会生成新的ConnectionId;如果 HubConnection 已配置为跳过协商,则Reconnected 事件处理程序的 connectionId 参数会为 null。
string newConnectionId = arg;
return Task.CompletedTask;
}
/// <summary>
/// 重连失败后的断开事件
/// </summary>
private async Task ClosedEvent(Exception? arg)
{
// 展示断开的信息
Log("断开成功!地址:" + adress + arg!=null?";错误信息:"+ arg?.Message: "");
// 可判断是人为断开还是异常断开
if(IsConnection)
{
// 重连逻辑
await Task.Delay(new Random().Next(0, 5) * 1000); // 加间隔时间
await _connection.StartAsync();
}
}
6、Java版
教程见:ASP.NET Core SignalR Java 客户端
四、分布式环境中使用SignalR
1、方式一:使用Azure SignalR 服务
见:将 ASP.NET Core SignalR 应用发布到 Azure 应用服务。
2、方式二:使用Redis集群中发布/订阅功能
见:设置 Redis 集群以实现 ASP.NET Core SignalR 横向扩展。
五、搭建SignalR流式服务器(流式传输)
见:在 ASP.NET Core SignalR 中使用流式传输。
六、SignalR中使用MessagePack(更小的传输载体)
见:在 ASP.NET Core SignalR 中使用 MessagePack 中心协议。
七、常见问题:
1、报错“An attempt was made to access a socket in a way forbidden by its access permissions...”
SignalR 连接是持久的,会占用服务器的 TCP 连接数量。当TCP 连接数量用完时,你会看到这个错误(随机套接字错误和连接重置错误)。
处理对策如下:① 修改服务器的TCP连接数量限制;② 将SignalR与其他 Web 应用部署在不同的服务器上。
本文来自博客园,作者:꧁执笔小白꧂,转载请注明原文链接:https://www.cnblogs.com/qq2806933146xiaobai/p/17513359.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战