SignalR WebSocket通讯机制

悠悠 2024-03-16 17:50 138阅读 0赞

1、什么是SignalR

  ASP.NET SignalR 是一个面向 ASP.NET 开发人员的库,可简化向应用程序添加实时 Web 功能的过程。 实时 Web 功能是让服务器代码在可用时立即将内容推送到连接的客户端,而不是让服务器等待客户端请求新数据。

  SignalR使用的三种底层传输技术分别是Web Socket, Server Sent Events 和 Long Polling, 它让你更好的关注业务问题而不是底层传输技术问题。

  WebSocket是最好的最有效的传输方式, 如果浏览器或Web服务器不支持它的话(IE10之前不支持Web Socket), 就会降级使用SSE, 实在不行就用Long Polling。

  (现在也很难找到不支持WebSocket的浏览器了,所以我们一般定义必须使用WebSocket)

2、我们做一个聊天室,实现一下SignalR前后端通讯

  由简入深,先简单实现一下 

  2.1 服务端Net5

4f3c28755961e72a8001141468b0a481.gif

  1. using Microsoft.AspNetCore.Authorization;
  2. using Microsoft.AspNetCore.SignalR;
  3. using System;
  4. using System.Threading.Tasks;
  5. namespace ServerSignalR.Models
  6. {
  7. public class ChatRoomHub:Hub
  8. {
  9. public override Task OnConnectedAsync()//连接成功触发
  10. {
  11. return base.OnConnectedAsync();
  12. }
  13. public Task SendPublicMsg(string fromUserName,string msg)//给所有client发送消息
  14. {
  15. string connId = this.Context.ConnectionId;
  16. string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}";
  17. return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
  18. }
  19. }
  20. }

08e2c899f04efdef99e306c812048067.gif

  Startup添加

9e75a76c6b0cc34ab4e5802af15fc6cd.gif

  1. static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins";
  2. public void ConfigureServices(IServiceCollection services)
  3. {
  4. services.AddControllers();
  5. services.AddSwaggerGen(c =>
  6. {
  7. c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" });
  8. });
  9. services.AddSignalR();
  10. services.AddCors(options =>
  11. {
  12. options.AddPolicy(_myAllowSpecificOrigins, policy =>
  13. {
  14. policy.WithOrigins("http://localhost:4200")
  15. .AllowAnyHeader().AllowAnyMethod().AllowCredentials();
  16. });
  17. });
  18. }
  19. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  20. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  21. {
  22. if (env.IsDevelopment())
  23. {
  24. app.UseDeveloperExceptionPage();
  25. app.UseSwagger();
  26. app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1"));
  27. }
  28. app.UseCors(_myAllowSpecificOrigins);
  29. app.UseHttpsRedirection();
  30. app.UseRouting();
  31. app.UseAuthorization();
  32. app.UseEndpoints(endpoints =>
  33. {
  34. endpoints.MapControllers();
  35. endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
  36. });
  37. }

ff9e90de49e2753cab53402013a72dc4.gif

  2.2 前端Angular

    引入包

  1. npm i --save @microsoft/signalr

    ts:

79fa9ba2e71a35a281a951a97ab7b94e.gif

  1. import { Component, OnInit } from '@angular/core';
  2. import * as signalR from '@microsoft/signalr';
  3. import { CookieService } from 'ngx-cookie-service';
  4. @Component({
  5. selector: 'app-home',
  6. templateUrl: './home.component.html',
  7. styleUrls: ['./home.component.scss']
  8. })
  9. export class HomeComponent implements OnInit {
  10. msg = '';
  11. userName='kxy'
  12. public messages: string[] = [];
  13. public hubConnection: signalR.HubConnection;
  14. constructor(
  15. private cookie: CookieService
  16. ) {this.hubConnection=new signalR.HubConnectionBuilder()
  17. .withUrl('https://localhost:44313/Hubs/ChatRoomHub',
  18. {
  19. skipNegotiation:true,//跳过三个协议协商
  20. transport:signalR.HttpTransportType.WebSockets,//定义使用WebSocket协议通讯
  21. }
  22. )
  23. .withAutomaticReconnect()
  24. .build();
  25. this.hubConnection.on('ReceivePublicMsg',msg=>{
  26. this.messages.push(msg);
  27. console.log(msg);
  28. });
  29. }
  30. ngOnInit(): void {
  31. }
  32. JoinChatRoom(){
  33. this.hubConnection.start()
  34. .catch(res=>{
  35. this.messages.push('连接失败');
  36. throw res;
  37. }).then(x=>{
  38. this.messages.push('连接成功');
  39. });
  40. }
  41. SendMsg(){
  42. if(!this.msg){
  43. return;
  44. }
  45. this.hubConnection.invoke('SendPublicMsg', this.userName,this.msg);
  46. }
  47. }

2e13b7287bb34c462dcc8a75f7f35217.gif

  这样就简单实现了SignalR通讯!!!

  有一点值得记录一下

    问题:强制启用WebSocket协议,有时候发生错误会被屏蔽,只是提示找不到/连接不成功

    解决:可以先不跳过协商,调试完成后再跳过

3、引入Jwt进行权限验证

  1. 安装Nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

  Net5的,注意包版本选择5.x,有对应关系

  Startup定义如下

acaa8c0385f95241ea4bec2d6f487df6.gif

  1. using Microsoft.AspNetCore.Builder;
  2. using Microsoft.AspNetCore.Hosting;
  3. using Microsoft.Extensions.Configuration;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Hosting;
  6. using Microsoft.OpenApi.Models;
  7. using ServerSignalR.Models;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using System.Threading.Tasks;
  12. using System.Text;
  13. using Microsoft.AspNetCore.Authentication.JwtBearer;
  14. using Microsoft.IdentityModel.Tokens;
  15. using JwtHelperCore;
  16. namespace ServerSignalR
  17. {
  18. public class Startup
  19. {
  20. public Startup(IConfiguration configuration)
  21. {
  22. Configuration = configuration;
  23. }
  24. public IConfiguration Configuration { get; }
  25. // This method gets called by the runtime. Use this method to add services to the container.
  26. static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins";
  27. public void ConfigureServices(IServiceCollection services)
  28. {
  29. services.AddControllers();
  30. services.AddSwaggerGen(c =>
  31. {
  32. c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" });
  33. });
  34. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  35. .AddJwtBearer(options =>
  36. {
  37. options.RequireHttpsMetadata = false;//是否需要https
  38. options.TokenValidationParameters = new TokenValidationParameters
  39. {
  40. ValidateIssuer = false,//是否验证Issuer
  41. ValidateAudience = false,//是否验证Audience
  42. ValidateLifetime = true,//是否验证失效时间
  43. ValidateIssuerSigningKey = true,//是否验证SecurityKey
  44. IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("VertivSecurityKey001")),//拿到SecurityKey
  45. };
  46. options.Events = new JwtBearerEvents()//从url获取token
  47. {
  48. OnMessageReceived = context =>
  49. {
  50. if (context.HttpContext.Request.Path.StartsWithSegments("/Hubs/ChatRoomHub"))//判断访问路径
  51. {
  52. var accessToken = context.Request.Query["access_token"];//从请求路径获取token
  53. if (!string.IsNullOrEmpty(accessToken))
  54. context.Token = accessToken;//将token写入上下文给Jwt中间件验证
  55. }
  56. return Task.CompletedTask;
  57. }
  58. };
  59. }
  60. );
  61. services.AddSignalR();
  62. services.AddCors(options =>
  63. {
  64. options.AddPolicy(_myAllowSpecificOrigins, policy =>
  65. {
  66. policy.WithOrigins("http://localhost:4200")
  67. .AllowAnyHeader().AllowAnyMethod().AllowCredentials();
  68. });
  69. });
  70. }
  71. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  72. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  73. {
  74. if (env.IsDevelopment())
  75. {
  76. app.UseDeveloperExceptionPage();
  77. app.UseSwagger();
  78. app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1"));
  79. }
  80. app.UseCors(_myAllowSpecificOrigins);
  81. app.UseHttpsRedirection();
  82. app.UseRouting();
  83. //Token 授权、认证
  84. app.UseErrorHandling();//自定义的处理错误信息中间件
  85. app.UseAuthentication();//判断是否登录成功
  86. app.UseAuthorization();//判断是否有访问目标资源的权限
  87. app.UseEndpoints(endpoints =>
  88. {
  89. endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
  90. endpoints.MapControllers();
  91. });
  92. }
  93. }
  94. }

d612f8999c425f34718bb2a5aa5ffdaa.gif

  红色部分为主要关注代码!!!

  因为WebSocket无法自定义header,token信息只能通过url传输,由后端获取并写入到上下文

  认证特性使用方式和http请求一致:

4e502d759e796f80c0659666f3e53127.gif

  1. using Microsoft.AspNetCore.Authorization;
  2. using Microsoft.AspNetCore.SignalR;
  3. using System;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. namespace ServerSignalR.Models
  7. {
  8. [Authorize]//jwt认证
  9. public class ChatRoomHub:Hub
  10. {
  11. public override Task OnConnectedAsync()//连接成功触发
  12. {
  13. return base.OnConnectedAsync();
  14. }
  15. public Task SendPublicMsg(string msg)//给所有client发送消息
  16. {
  17. var roles = this.Context.User.Claims.Where(x => x.Type.Contains("identity/claims/role")).Select(x => x.Value).ToList();//获取角色
  18. var fromUserName = this.Context.User.Identity.Name;//从token获取登录人,而不是传入(前端ts方法的传入参数也需要去掉)
  19. string connId = this.Context.ConnectionId;
  20. string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}";
  21. return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
  22. }
  23. }
  24. }

4e502d759e796f80c0659666f3e53127.gif

  然后ts添加

4e83941e29c6cae55299e64e67906fa2.gif

  1. constructor(
  2. private cookie: CookieService
  3. ) {
  4. var token = this.cookie.get('spm_token');
  5. this.hubConnection=new signalR.HubConnectionBuilder()
  6. .withUrl('https://localhost:44313/Hubs/ChatRoomHub',
  7. {
  8. skipNegotiation:true,//跳过三个协议协商
  9. transport:signalR.HttpTransportType.WebSockets,//定义使用WebSocket协议通讯
  10. accessTokenFactory:()=> token.slice(7,token.length)//会自动添加Bearer头部,我这里已经有Bearer了,所以需要截掉
  11. }
  12. )
  13. .withAutomaticReconnect()
  14. .build();
  15. this.hubConnection.on('ReceivePublicMsg',msg=>{
  16. this.messages.push(msg);
  17. console.log(msg);
  18. });
  19. }

d520218b416c1e97f5655130ad3c1560.gif

4、私聊

  Hub

dafc9d09df09ba0bfd5ae0269a4b47f8.gif

  1. using Microsoft.AspNetCore.Authorization;
  2. using Microsoft.AspNetCore.SignalR;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Threading.Tasks;
  6. namespace ServerSignalR.Models
  7. {
  8. [Authorize]//jwt认证
  9. public class ChatRoomHub:Hub
  10. {
  11. private static List<UserModel> _users = new List<UserModel>();
  12. public override Task OnConnectedAsync()//连接成功触发
  13. {
  14. var userName = this.Context.User.Identity.Name;//从token获取登录人
  15. _users.Add(new UserModel(userName, this.Context.ConnectionId));
  16. return base.OnConnectedAsync();
  17. }
  18. public override Task OnDisconnectedAsync(Exception exception)
  19. {
  20. var userName = this.Context.User.Identity.Name;//从token获取登录人
  21. _users.RemoveRange(_users.FindIndex(x => x.UserName == userName), 1);
  22. return base.OnDisconnectedAsync(exception);
  23. }
  24. public Task SendPublicMsg(string msg)//给所有client发送消息
  25. {
  26. var fromUserName = this.Context.User.Identity.Name;
  27. //var ss = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
  28. string str = $"[{DateTime.Now}]\r\n{fromUserName}:{msg}";
  29. return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
  30. }
  31. public Task SendPrivateMsg(string destUserName, string msg)
  32. {
  33. var fromUser = _users.Find(x=>x.UserName== this.Context.User.Identity.Name);
  34. var toUser = _users.Find(x=>x.UserName==destUserName);
  35. string str = $"";
  36. if (toUser == null)
  37. {
  38. msg = $"用户{destUserName}不在线";
  39. str = $"[{DateTime.Now}]\r\n系统提示:{msg}";
  40. return this.Clients.Clients(fromUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str);
  41. }
  42. str = $"[{DateTime.Now}]\r\n{fromUser.UserName}-{destUserName}:{msg}";
  43. return this.Clients.Clients(fromUser.WebScoketConnId,toUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str);
  44. }
  45. }
  46. }

c99acd3b86048571a2b8c895c8ebc712.gif

  TS:

df811534ee64faec87ade39dd46f8ecc.gif

  1. //加一个监听
  2. this.hubConnection.on('ReceivePublicMsg', msg => {
  3. this.messages.push('公屏'+msg);
  4. console.log(msg);
  5. });
  6. this.hubConnection.on('ReceivePrivateMsg',msg=>{
  7. this.messages.push('私聊'+msg);
  8. console.log(msg);
  9. });
  10. //加一个发送
  11. if (this.talkType == 1)
  12. this.hubConnection.invoke('SendPublicMsg', this.msg);
  13. if (this.talkType == 3){
  14. console.log('11111111111111');
  15. this.hubConnection.invoke('SendPrivateMsg',this.toUserName, this.msg);
  16. }

330cf08d5c85c06a5cc899371e0d1eff.gif

5、在控制器中使用Hub上下文

  Hub链接默认30s超时,正常情况下Hub只会进行通讯,而不再Hub里进行复杂业务运算

  如果涉及复杂业务计算后发送通讯,可以将Hub上下文注入外部控制器,如

19556310e949fd2faa9b0e10b4c23bee.gif

  1. namespace ServerSignalR.Controllers
  2. {
  3. //[Authorize]
  4. public class HomeController : Controller
  5. {
  6. private IHubContext<ChatRoomHub> _hubContext;
  7. public HomeController(IHubContext<ChatRoomHub> hubContext)
  8. {
  9. _hubContext = hubContext;
  10. }
  11. [HttpGet("Welcome")]
  12. public async Task<ResultDataModel<bool>> Welcome()
  13. {
  14. await _hubContext.Clients.All.SendAsync("ReceivePublicMsg", "欢迎");
  15. return new ResultDataModel<bool>(true);
  16. }
  17. }
  18. }

88852805a62465d2439578532a3dce12.gif

6、SignalR传输图片和文件

  原理十分简单,是把图片转成blob进行传输

  html核心代码

0353dd704b42ff8e323129b4d391e002.gif

  1. <form>
  2. <input class="col-md-9" id="file" type="file" name='file' (change)="SelFile($event)">
  3. <div class="btn btn-outline-success col-md-2" (click)="SendFile()">SendFile</div>
  4. </form>
  5. <ul>
  6. <li class="list-group-item" *ngFor="let item of messages">
  7. <ng-container *ngIf="item.Type=='String'"> <!-文字消息->
  8. {
  9. {item.FromUser}}-{
  10. {item.SendDateTime}}<br />
  11. {
  12. {item.Value}}
  13. </ng-container>
  14. <ng-container *ngIf="item.Type=='Images'">
  15. {
  16. {item.FromUser}}-{
  17. {item.SendDateTime}}<br />
  18. <img [src]="item.Value" name="图片" style="width: 100px;">
  19. </ng-container>
  20. <ng-container *ngIf="item.Type=='File'">
  21. {
  22. {item.FromUser}}-{
  23. {item.SendDateTime}}<br />
  24. <a [href]="item.Value" [download]="item.FileName">{
  25. {item.FileName}}</a>
  26. </ng-container>
  27. </li>
  28. </ul>

45c70c51ab19460363c531159890d5bf.gif

  ts:选择和发送

c6dc1aa9b936210ceac711cab3dcf32e.gif

  1. blob: string = ''
  2. selFileName: string = ''
  3. SelFile(e: any) {
  4. console.log(e);
  5. if (e.target.files.length != 0) { let file = e.target.files[0];
  6. const reader = new FileReader();
  7. reader.readAsDataURL(file);
  8. reader.onload = () => {
  9. this.blob = reader.result as string;
  10. this.selFileName = file.name
  11. } }
  12. if (e.target.files.length == 0) {
  13. this.blob = ''
  14. this.selFileName = ''
  15. }
  16. }
  17. SendFile() {
  18. if (!this.blob)
  19. return;
  20. if (this.blob.split(';')[0].indexOf('image') != -1){
  21. this.hubConnection.invoke('SendFile', this.blob, 'Images',this.selFileName);
  22. return;
  23. }
  24. this.hubConnection.invoke('SendFile', this.blob, 'File', this.selFileName);
  25. }

fc164fd933f61b0a513928030b92a26a.gif

  ts:监听后端数据

  1. this.hubConnection.on('FileMsg', (datetime, fromUserAccount, msg, type,fileName) => {
  2. this.messages.push(new MsgInfo(datetime, fromUserAccount, msg).setType(type).setFileName(fileName));
  3. });

  Net:后端hub

e54a497ebf4976d99295fcb57640f724.gif

  1. public Task SendFile(string msg, string type, string fileName)
  2. {
  3. var fromUserAccount = this.Context.User.Identity.Name;
  4. return this.Clients.All.SendAsync("FileMsg", DateTime.Now.ToString(), fromUserAccount, msg, type, fileName);
  5. }

88852805a62465d2439578532a3dce12.gif

  Net:SignalR默认接收字符串只有1000字节,正常会超过此范围,需要修改接受字符串长度

  1. services.AddSignalR(hubOptions =>
  2. {
  3. hubOptions.MaximumReceiveMessageSize = 10 * 1024 * 1024;//10M
  4. });

发表评论

表情:
评论列表 (有 0 条评论,138人围观)

还没有评论,来说两句吧...

相关阅读