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("*");

image-20220310235354616

注意app.MapHub("/Hubs/ChatRoomHub");位置

image-20220310235600458

(3)使用vue创建前端

这里就不讲如何安装node和vue了

vue init webpack SignalRClient

image-20220310235208913

安装npm install @microsoft/signalr

官网教程是安装@microsoft/signalr,我这里运行时一直失败

可以尝试npm install @aspnet/signalr

image-20220311000015081

(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协商过程

image-20220311010954271

image-20220311011551350

  • websocket和http共用端口。websocket是http的升级版(upgrade),我们只需要定制http服务器的upgrade函数即可。

(2)协议协商的问题

“协商”请求被服务器A处理,而接下来的WebSocket请求却被服务器B处理

  • 粘性会话(Sticky Session):把来自同一个客户端的请求都转发给同一台服务器上。缺点:因为共享公网IP等造成请求无法被平均的分配到服务器集群;扩容的自适应性不强。
  • “禁用协商”:直接向服务器发出WebSocket请求。WebSocket连接一旦建立后,在客户端和服务器端直接就建立了持续的网络连接通道,在这个WebSocket连接中的后续往返WebSocket通信都是由同一台服务器来处理。缺点:无法降级到“服务器发送事件”或“长轮询”,不过不是大问题。

(3)禁用协议协商的方式

image-20220311012051710

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)服务端认证

image-20220317213554597

(2)客户端认证

image-20220317213925420

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";
}

image-20220317221522864

image-20220317221548986

1.6 外部向SignalR的Hub发送消息

注入IHubContext hubContext

注意:hubContext无法使用Caller、other方法,因为websocket连接和controller的连接不在一起

image-20220317223311203

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>
posted @ 2022-04-08 16:09  peng_boke  阅读(392)  评论(1编辑  收藏  举报