Asp.Net Core SignalR
## 1.Asp.Net Core SignalR
- WebSocket基于TCP协议,支持二进制通信,双工通信。
- 性能和并发能力更强。
- WebSocket独立于HTTP协议,不过我们一般仍然把WebSocket服务器端部署到Web服务器上,因为可以借助HTTP协议完成初始的握手(可选),并且共享HTTP服务器的端口(主要)
1.1 SignalR基本能使用
(1)ChatRoomHub
public class ChatRoomHub : Hub
{
public Task SendPublicMessage(string message)
{
string connId = this.Context.ConnectionId;
string msg = $"{connId} {DateTime.Now}:{message}";
return Clients.All.SendAsync("ReceivePublicMessage", msg);
}
}
(2)注入SignalR,顺便加上Cores,注意位置
builder.Services.AddSignalR();
builder.Services.AddCors(c => {
//允许任意跨域请求
c.AddPolicy("*",
policy =>
{
policy
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
app.UseCors("*");
注意app.MapHub
(3)使用vue创建前端
这里就不讲如何安装node和vue了
vue init webpack SignalRClient
安装npm install @microsoft/signalr
官网教程是安装@microsoft/signalr,我这里运行时一直失败
可以尝试npm install @aspnet/signalr
(4)前端代码
<template>
<div class="hello">
<h1>SignalR</h1>
<input type="text" v-model="userMessage" v-on:keypress="txtMsgOnkeypress" />
<div>
<ul>
<li v-for="(msg, index) in messages" :key="index">{{ msg }}</li>
</ul>
</div>
</div>
</template>
<script>
//import * as signalR from '@microsoft/signalr';
import * as signalR from "@aspnet/signalr";
export default {
name: "HelloWorld",
data() {
return {
connection: "",
userMessage: "",
messages: [],
};
},
methods: {
txtMsgOnkeypress: async function (e) {
if (e.keyCode != 13) return;
await this.connection.invoke("SendPublicMessage", this.userMessage);
this.userMessage = "";
},
},
created: function () {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("https://localhost:7066/Hubs/ChatRoomHub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
//.withAutomaticReconnect()
.build();
this.connection.start();
this.connection.on("ReceivePublicMessage", (msg) => {
this.messages.push(msg);
});
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
/* li {
display: inline-block;
margin: 0 10px;
} */
a {
color: #42b983;
}
</style>
1.2 SignalR的协议协商
(1)协议协商
-
SignalR支持多种服务器推送方式:Websocket、Server-Sent Events、长轮询。默认按顺序尝试。
-
SignalR协商过程
- websocket和http共用端口。websocket是http的升级版(upgrade),我们只需要定制http服务器的upgrade函数即可。
(2)协议协商的问题
“协商”请求被服务器A处理,而接下来的WebSocket请求却被服务器B处理
- 粘性会话(Sticky Session):把来自同一个客户端的请求都转发给同一台服务器上。缺点:因为共享公网IP等造成请求无法被平均的分配到服务器集群;扩容的自适应性不强。
- “禁用协商”:直接向服务器发出WebSocket请求。WebSocket连接一旦建立后,在客户端和服务器端直接就建立了持续的网络连接通道,在这个WebSocket连接中的后续往返WebSocket通信都是由同一台服务器来处理。缺点:无法降级到“服务器发送事件”或“长轮询”,不过不是大问题。
(3)禁用协议协商的方式
1.3 SignalR分布式部署
所有服务器连接到同一个消息中间件。
(1)安装Install-Package Microsoft.AspNetCore.SignalR.StackExchangeRedis
(2)配置SignalR中间件
builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0.1", options => {
options.Configuration.ChannelPrefix = "Test1_";
});
1.4 SignalR身份认证
(1)服务端认证
(2)客户端认证
1.5 SignalR向部分客户端发送消息
public async Task<string> SendPrivateMessage(string destUserName, string message)
{
User? destUser =await userManager.FindByNameAsync(destUserName);
if (destUser == null)
{
return "DestUserNotFound";
}
string destUserId = destUser.Id.ToString();
string srcUserName = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
string time = DateTime.Now.ToShortTimeString();
await this.Clients.User(destUserId).SendAsync("ReceivePrivateMessage",
srcUserName, time, message);
return "ok";
}
1.6 外部向SignalR的Hub发送消息
注入IHubContext
注意:hubContext无法使用Caller、other方法,因为websocket连接和controller的连接不在一起
1.7 SignalR实现进度条
(1)后端
ImportDictHub
public class ImportDictHub : Hub
{
private readonly ImportExecutor executor;
public ImportDictHub(ImportExecutor executor)
{
this.executor = executor;
}
public Task Import()
{
_ = executor.ExecuteAsync(this.Context.ConnectionId);
return Task.CompletedTask;
}
}
ImportExecutor
public class ImportExecutor
{
private readonly IOptions<ConnStrOptions> optionsConnStr;
private readonly IHubContext<ImportDictHub> hubContext;
private readonly ILogger<ImportExecutor> logger;
private readonly IWebHostEnvironment webHostEnvironment;
public ImportExecutor(IOptions<ConnStrOptions> optionsConnStr,
IHubContext<ImportDictHub> hubContext, ILogger<ImportExecutor> logger, IWebHostEnvironment webHostEnvironment)
{
this.optionsConnStr = optionsConnStr;
this.hubContext = hubContext;
this.logger = logger;
this.webHostEnvironment = webHostEnvironment;
}
public async Task ExecuteAsync(string connectionId)
{
try
{
await DoExecuteAsync(connectionId);
}
catch (Exception ex)
{
await hubContext.Clients.Client(connectionId).SendAsync("Failed");
logger.LogError(ex, "ImportExecutor出现异常");
}
}
public async Task DoExecuteAsync(string connectionId)
{
//Hub方法超时时间很短,所以要放到线程中执行导入
//用UserId进行客户端过滤的好处:退出后重新进来仍然能看到进度,坏处:不能并发多个任务
//用ConnectionId过滤客户端的好处:可以多个并发,坏处:推出后再进来就看不到了。
//E:\Data\MyCode\YZK.Study\YZK.Study\ImportDict\\file\\stardict.csv
string path = webHostEnvironment.ContentRootPath + "file\\stardict.csv";
string[] lines = await File.ReadAllLinesAsync(path);//("d:/stardict.csv");//读取文件
var client = hubContext.Clients.Client(connectionId);
await client.SendAsync("Started");
string connStr = optionsConnStr.Value.Default;//读取连接字符串
using SqlConnection conn = new SqlConnection(connStr);
await conn.OpenAsync();
using SqlBulkCopy bulkCopy = new SqlBulkCopy(conn);
bulkCopy.DestinationTableName = "T_WordItems";
bulkCopy.ColumnMappings.Add("Word", "Word");
bulkCopy.ColumnMappings.Add("Phonetic", "Phonetic");
bulkCopy.ColumnMappings.Add("Definition", "Definition");
bulkCopy.ColumnMappings.Add("Translation", "Translation");
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Word");
dataTable.Columns.Add("Phonetic");
dataTable.Columns.Add("Definition");
dataTable.Columns.Add("Translation");
int count = lines.Length;
for (int i = 1; i < count; i++)//跳过表头
{
string line = lines[i];
string[] segments = line.Split(',');
string word = segments[0];
string? phonetic = segments[1];
string? definition = segments[2];
string? translation = segments[3];
var dataRow = dataTable.NewRow();
dataRow["Word"] = word;
dataRow["Phonetic"] = phonetic;
dataRow["Definition"] = definition;
dataRow["Translation"] = translation;
dataTable.Rows.Add(dataRow);
if (dataTable.Rows.Count == 1000)
{
await bulkCopy.WriteToServerAsync(dataTable);
dataTable.Clear();
await client.SendAsync("ImportProgress", i, count);
}
}
await client.SendAsync("ImportProgress", count, count);
await bulkCopy.WriteToServerAsync(dataTable);//处理剩余的一组
await client.SendAsync("Completed");
}
}
(2)前端
<template>
<div class="hello">
<h1>SignalR</h1>
<div>
<input type="button" value="导入" v-on:click="importData" />
<div>{{ this.message }}</div>
<progress
:value="this.progress.value"
:max="this.progress.total"
></progress>
</div>
</div>
</template>
<script>
//import * as signalR from '@microsoft/signalr';
import * as signalR from "@aspnet/signalr";
export default {
name: "HelloWorld",
data() {
return {
connection: "",
userMessage: "",
messages: [],
message:"",
progress: {
value: 0,
total: 0,
},
};
},
methods: {
importData: async function () {
try {
await this.connection.invoke("Import");
this.message = "开始导入";
} catch (err) {
alert(err);
}
},
},
created: function () {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("https://localhost:7249/Hubs/ImportDictHub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
//.withAutomaticReconnect()
.build();
try {
this.connection.start();
} catch (err) {
alert(err);
return;
}
this.connection.on("ImportProgress", (value, total) => {
this.progress.value = value;
this.progress.total = total;
this.message = "导入中(" + value + "/" + total + ")";
});
this.connection.on("Started", () => {
this.message = "文件解析完毕,开始导入";
});
this.connection.on("Failed", () => {
this.message = "导入失败";
});
this.connection.on("Completed", () => {
this.message = "导入完成";
});
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
/* li {
display: inline-block;
margin: 0 10px;
} */
a {
color: #42b983;
}
</style>