
.net6 Signalr+Vue3 的运用(上)
什么是 SignalR?
ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。
什么是 Hubs?
SignalR 使用Hubs在客户端和服务器之间通信。
Hub 是一种高级管道,允许客户端和服务器相互调用方法。
SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。
可以将强类型参数传递给方法,从而支持模型绑定。
SignalR 提供两种内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。
也叫做集线器。
SignalR后端项目
创建项目
创建signalrtest项目,我在创建时选择了OpenAPI,没有选择Https。
创建强类型Hubs
这样做的好处是:方便客户端调用服务器的方法时,拼音不会报错。
接下来我们在HubClients
目录下定义客户端的调用方法的接口IChatClient
,并且只定义SendAll
方法。
public interface IChatClient
{
Task SendAll(object message);
}
然后我们在Hubs
的目录下创建ChatHub
集线器。
并定义了一个SendMessage
的方法向所有的用户发送消息,并对客户端连接和断开状态做了一个日志记录。
public class ChatHub : Hub<IChatClient>
{
ILogger<ChatHub> _logger;
public ChatHub(ILogger<ChatHub> logger, CommonService common)
{
_logger = logger;
_common = common;
}
readonly CommonService _common;
/// <summary>
/// 客户端连接服务端
/// </summary>
/// <returns></returns>
public override Task OnConnectedAsync()
{
var id = Context.ConnectionId;
_logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!");
return base.OnConnectedAsync();
}
/// <summary>
/// 客户端断开连接
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override Task OnDisconnectedAsync(Exception exception)
{
var id = Context.ConnectionId;
_logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!");
return base.OnDisconnectedAsync(exception);
}
/**
* 测试
* */
/// <summary>
/// 给所有客户端发送消息
/// </summary>
/// <returns></returns>
public async Task SendMessage(string data)
{
Console.WriteLine("Have one Data!");
await Clients.All.SendAll(_common.SendAll(data));
}
}
这里有一个CommonService
它定义在HubService
的目录下面,里面只有一个SendAll
方法,该方法只是在原有的消息基础上添加Hello
和随机数。
内容如下所示:
public class CommonService
{
internal object SendAll(string data)
{
return $"Hello {new Random().Next(0, 100)} {data} ";
}
}
配置SignalR
我们可以通过AddSignalR
方法来注册SignalR相关服务,并通过AddJsonProtocol
启用SignalR 的 JSON 协议。PayloadSerializerOptions
是一个System.Text.JsonJsonSerializerOptions
对象,PropertyNamingPolicy
属性为null
表示保持属性名称不变(是否区分大小写,无所谓)。
builder.Services
.AddSignalR()
.AddJsonProtocol(options => {
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
})
;
builder.Services.TryAddSingleton(typeof(CommonService));
如果想使用NewtonsoftJson
,可以将AddJsonProtocol
方法改为AddNewtonsoftJsonProtocol
。
然后我们通过MapHub
方法,加载路由路径/ChatHub
由ChatHub
处理,并设置传输的方式可以使用WebSockets
与LongPolling
。
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapHub<ChatHub>("/ChatHub", options =>
{
options.Transports =
HttpTransportType.WebSockets |
HttpTransportType.LongPolling;
});
SignalR前端地址项目
首先我们创建一个Vue3.0的项目,并安装@aspnet/signalr
包。
vue create signalrtestvue
cd signalrtestvue
npm install @aspnet/signalr
目前aspnet/signalr
包已经弃用了,推荐使用@microsoft/signalr
包,更多请参考
然后我们在src/utils
目录下编写signalR.js
文件。
请修改你本地的signalr服务器的连接地址。
import * as signalR from '@aspnet/signalr'
const url = "http://localhost:5102/ChatHub"
const signal = new signalR.HubConnectionBuilder()
.withUrl(url, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.configureLogging(signalR.LogLevel.Information)
.build()
signal.on('SendAll', (res) => {
console.log(res, '收到消息')
})
signal.start().then(() => {
if (window.Notification) {
if (Notification.permission === 'granted') {
console.log('允许通知')
} else if (Notification.permission !== 'denied') {
console.log('需要通知权限')
Notification.requestPermission((permission) => { console.log("权限通知",permission) })
} else if (Notification.permission === 'denied') {
console.log('拒绝通知')
}
} else {
console.error('浏览器不支持Notification')
}
console.log('连接成功')
})
signal.onclose((err)=>{
console.log("连接已经断开 执行函数onclose",err)
})
export default {
signal
}
通过HubConnectionBuilder
来连接到我们的Hubs。withUrl
设置连接地址。configureLogging
设置日志记录的级别。
接下来讲将相关的事件方法。
事件名 | 描述 |
---|---|
on |
接收服务器返回的方法进行处理。 |
start |
启动Hubs的服务器连接。 |
onclose |
服务器关闭触发的事件回调。 |
stop |
关闭Hubs的服务器连接。 |
invoke |
调用服务器的方法。 |
然后我们在main.js
下进行全局启用signalr
。
import { createApp } from 'vue'
import App from './App.vue'
import signalr from './utils/signalR'
const app = createApp(App)
app.config.globalProperties.$signalr = signalr.signal;
app.mount('#app')
在components
目录下找到HellloWorld.vue
并添加一个按钮向服务器端的SendMessage
发送消息。
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button @click="onClickButton" >获取</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
};
},
methods: {
onClickButton() {
console.log(this.$signalr)
this.$signalr
.invoke('SendMessage', "hmy")
.catch(function(err) {return console.error(err) })
}
}
}
</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>
运行测试
npm run serve
通过点击获取按钮来触发事件。
由服务器端发向客户端
我们这里添加一个ClientHubController
控制器并通过请求发送SendMessage
消息。
[ApiController]
[Route("[controller]")]
public class ClientHubController : ControllerBase
{
private readonly ILogger<ClientHubController> _logger;
public ClientHubController(
ILogger<ClientHubController> logger
)
{
_logger = logger;
}
[HttpGet(Name = "SendMessage")]
public async Task SendMessage(string date, [FromServices] IHubContext<ChatHub, IChatClient> hubContext)
{
await hubContext.Clients.All.SendAll(date);
}
}
这里我使用的是强类型IHubContext<,>
,如果是弱类型直接IHubContext
也是可以的。
运行测试。
如果我们希望在第一个页面点击获取按钮,并返回一个发送消息的结果,我们可以通过Clients.Caller
来将消息返回给调用方。
首先在CommonService
中创建一个发送给调用方的消息。
internal object SendCaller() => "Send Successful!";
再在SendMessage中进行调用。
public async Task SendMessage(string data)
{
Console.WriteLine("Have one Data!");
await Clients.All.SendAll(_common.SendAll(data));
await Clients.Caller.SendAll(_common.SendCaller(data));
}
指定客户端发送消息
如果我们想给指定的客户端发送消息,首先我们需要获取所有连接服务器的ID,这里我做一个简易的集合进行存储,并且在OnDisconnectedAsync
与OnConnectedAsync
事件中进行增加与删除。
public static class UserIdsStore
{
static HashSet<string> Ids = new HashSet<string>();
}
/// <summary>
/// 客户端连接服务端
/// </summary>
/// <returns></returns>
public override Task OnConnectedAsync()
{
var id = Context.ConnectionId;
// 添加用户ID
UserIdsStore.Ids.Add(id);
_logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!");
return base.OnConnectedAsync();
}
/// <summary>
/// 客户端断开连接
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override Task OnDisconnectedAsync(Exception exception)
{
var id = Context.ConnectionId;
// 删除用户ID
UserIdsStore.Ids.Remove(id);
_logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!");
return base.OnDisconnectedAsync(exception);
}
然后我们可以在IChatClient
中添加SendCustomUserMessage
接口方便客户端接收。
Task SendCustomUserMessage(object message);
在ClientHubController
中添加两个接口。
/// <summary>
/// 获取所有的用户
/// </summary>
/// <returns></returns>
[HttpGet("GetAllUserIds", Name = "GetAllUserIds")]
public string[] GetAllUserIds()
{
return UserIdsStore.Ids.ToArray();
}
/// <summary>
/// 发送指定的消息给指定的客户端
/// </summary>
/// <param name="userid"></param>
/// <param name="date"></param>
/// <param name="hubContext"></param>
/// <returns></returns>
[HttpGet("SendCustomUserMessage", Name = "SendCustomUserMessage")]
public async Task<IActionResult> SendCustomUserMessage(
string userid,
string date,
[FromServices] IHubContext<ChatHub, IChatClient> hubContext
)
{
await hubContext.Clients.Client(userid).SendCustomUserMessage(date);
return Ok("Send Successful!");
}
最后我们需要在我们的前端客户端中的SignalR.js
文件,创建SendCustomUserMessage
事件的处理。
signal.on('SendCustomUserMessage', (res) => {
console.log(res, '收到消息')
})
接下来运行项目,重新启动一下前端项目进行测试。
首先获取一下所有的ID。
然后我们只取其中的ID调用SendCustomUserMessage
接口来进行发送消息。
授权访问服务器
在我们将SignalR的用户之前,首先需要授权,这里我们可以搞一个JWT的方便。
首先安装Microsoft.AspNetCore.Authentication.JwtBearer
包
添加MyJWTBearer
类,自定义我们的JWT的Token生成。
public static class MyJWTBearer
{
public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
public static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
public static string GenerateToken(HttpContext httpContext)
{
// 请求时传入的用户参数为NameIdentifier claim的值
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, httpContext.Request.Query["user"]) };
// 签名凭据
var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
// 生成JWT Token
var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.UtcNow.AddSeconds(60), signingCredentials: credentials);
return JwtTokenHandler.WriteToken(token);
}
public static void AddMyJWTBearerAuth(this IServiceCollection services)
{
// 添加自定义授权
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = MyJWTBearer.SecurityKey
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// 当我们收到消息时,去获取请求中的access_token字段
var accessToken = context.Request.Query["access_token"];
// 如果没有就去头上找,找到了就放入我们context.token中
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
}
这里在生成的时候多了一个NameIdentifier
Claim Type的类型,这个类型是当我们发消息调用User()
方法时需要验证的时候需要的,当然后续我们可以自定义其中的逻辑进行判断。(待会再讲)
接下来我们配置一下自定义的授权。
// 添加授权服务
builder.Services.AddMyJWTBearerAuth();
...
app.UseAuthentication();
app.UseAuthorization();
// 授权路径
app.MapGet("generatetoken", c => c.Response.WriteAsync(MyJWTBearer.GenerateToken(c)));
测试一下
http://localhost:5102/generatetoken?user=bob
接下来我们修改一下前端项目,我们想在连接Signalr调用前先调用Token,再使用Token进行连接我们的服务器。
所以首先安装一下axios
。
npm install axios
然后我们需要修改一下main.js
,删除以前自动连接的signalR.js
的引用,并且的添加axios
的引用。
import { createApp } from 'vue'
import App from './App.vue'
import axios from 'axios'
const app = createApp(App)
axios.defaults.baseURL = "http://localhost:5102"
app.config.globalProperties.$http = axios;
app.mount('#app')
然后修改HelloWorld.vue
页面,在填写好用户名后登录。
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<div>
UserName: <input type="text" v-model="username" >
<button @click="onConnectionClickButton" >连接</button>
</div>
<div>
Message: <input type="text" v-model="message" >
<button @click="onClickButton" >发送</button>
</div>
</div>
</template>
<script>
import * as signalR from '@aspnet/signalr'
const url = "http://localhost:5102/ChatHub"
const signal = new signalR.HubConnectionBuilder()
.withUrl(url, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => ""
})
.configureLogging(signalR.LogLevel.Information)
.build()
signal.on('SendCustomUserMessage', (res) => {
console.log(res, '收到消息')
})
signal.on('SendAll', (res) => {
console.log(res, '收到消息')
})
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
username: "",
message: ""
}
},
methods: {
onClickButton() {
var e = this
signal
.invoke('SendMessage', e.message)
.catch(function(err) {return console.error(err) })
},
async onConnectionClickButton() {
// 首先我们去获取Token
let name = this.username
let result = await this.$http.get(`generatetoken?user=${name}`)
if (result.status !== 200) {
console.error("Token 请求失败")
return
}
var token = result.data
console.log("获得Token",token)
// 放入token
signal.connection.options.accessTokenFactory = () => token
// 然后我们请求连接signalr
signal.start().then(() => {
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>
跨域问题
这里涉及到跨域问题。
string MyAllowSpecificOrigins = "_signalrtestcores";
builder.Services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS")
)
;
});
...
app.UseCors(MyAllowSpecificOrigins);
然后再为ChatHub
添加授权特性Authorize
,表示访问该资源需要进行授权。
[Authorize]
public class ChatHub : Hub<IChatClient>
接下来我们运行测试一下,输入用户名后,点击连接,同时也是可以发送消息的。
可以看到我们已经成功的进行了授权。
断开后重连
我们与Signalr服务器断开连接后,我们希望进行重新连接,不用每次都刷新页面。可以在onclose
事件里面进行设置。
<script>
import * as signalR from '@aspnet/signalr'
const url = "http://localhost:5102/ChatHub"
const signal = new signalR.HubConnectionBuilder()
.withUrl(url, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => ""
})
.configureLogging(signalR.LogLevel.Information)
.build()
signal.on('SendCustomUserMessage', (res) => {
console.log(res, '收到消息')
})
signal.on('SendAll', (res) => {
console.log(res, '收到消息')
})
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
username: "bob",
message: "",
timer: null,
connectionstatus: "init"
}
},
mouted() {
this.timer = setInterval(()=>{},500)
},
destoryed() {
this.clearInterval(this.timer)
},
methods: {
onClickButton() {
var e = this
signal
.invoke('SendMessage', e.message)
.catch(function(err) {return console.error(err) })
},
async onConnectionClickButton() {
try{
// 首先我们去获取Token
let name = this.username
let result = await this.$http.get(`generatetoken?user=${name}`)
if (result.status !== 200) {
console.error("Token 请求失败")
return
}
var token = result.data
console.log("获得Token",token)
var e = this
// onClose的定义
if (e.connectionstatus == "init") {
signal.onclose(() => {
e.connectionstatus = "close"
signal.stop()
console.log("连接已关闭")
e.retryConnection()
});
}
// 放入token
signal.connection.options.accessTokenFactory = () => token
// 然后我们请求连接signalr
signal.start().then(() => {
if (e.connectionstatus == "close") {
clearInterval(e.timer)
}
e.connectionstatus = "start"
console.log('连接成功')
})
}catch(e){
if (e.code == "ERR_NETWORK") {
console.log("Token 请求失败")
}
}
},
retryConnection() {
var e = this
if (this.connectionstatus == "init" || this.connectionstatus == "start") {
return
}else if(this.connectionstatus == "close"){
console.log("正在重试连接...")
this.timer = setInterval(()=>{
e.onConnectionClickButton()
},10000)
return
}
}
}
}
</script>
这样就不用担心服务器挂了还是没挂了。
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739

