2

SignalR WebSocket通讯机制 - wskxy

 11 months ago
source link: https://www.cnblogs.com/wskxy/p/17391001.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SignalR WebSocket通讯机制

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

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;

namespace ServerSignalR.Models
{
    public class ChatRoomHub:Hub
    {
        public override Task OnConnectedAsync()//连接成功触发
        {
            return base.OnConnectedAsync();
        }

        public Task SendPublicMsg(string fromUserName,string msg)//给所有client发送消息
        {
            string connId = this.Context.ConnectionId;
            string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}";
            return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
        }
    }
}

  Startup添加

        static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins";
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" });
            });
            services.AddSignalR();
            services.AddCors(options =>
            {
                options.AddPolicy(_myAllowSpecificOrigins, policy =>
                {
                    policy.WithOrigins("http://localhost:4200")
                    .AllowAnyHeader().AllowAnyMethod().AllowCredentials();
                });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1"));
            }
            app.UseCors(_myAllowSpecificOrigins);
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
            });
        }

   2.2 前端Angular

npm i --save @microsoft/signalr
import { Component, OnInit } from '@angular/core';
import * as signalR from '@microsoft/signalr';
import { CookieService } from 'ngx-cookie-service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  msg = '';
  userName='kxy'
  public messages: string[] = [];
  public hubConnection: signalR.HubConnection;

  constructor(
    private cookie: CookieService
  ) {this.hubConnection=new signalR.HubConnectionBuilder()
    .withUrl('https://localhost:44313/Hubs/ChatRoomHub',
      {
        skipNegotiation:true,//跳过三个协议协商
        transport:signalR.HttpTransportType.WebSockets,//定义使用WebSocket协议通讯
      }
    )
    .withAutomaticReconnect()
    .build();
    this.hubConnection.on('ReceivePublicMsg',msg=>{
      this.messages.push(msg);
      console.log(msg);
    });
  }
  ngOnInit(): void {
  }
  JoinChatRoom(){
    this.hubConnection.start()
    .catch(res=>{
      this.messages.push('连接失败');
      throw res;
    }).then(x=>{
      this.messages.push('连接成功');
    });
  }
  SendMsg(){
    if(!this.msg){
      return;
    }
    this.hubConnection.invoke('SendPublicMsg', this.userName,this.msg);
  }
}

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

  有一点值得记录一下

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

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

3、引入Jwt进行权限验证

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

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

  Startup定义如下

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using ServerSignalR.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using JwtHelperCore;

namespace ServerSignalR
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins";
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" });
            });
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;//是否需要https
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = false,//是否验证Issuer
                        ValidateAudience = false,//是否验证Audience
                        ValidateLifetime = true,//是否验证失效时间
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("VertivSecurityKey001")),//拿到SecurityKey
                    };
                    options.Events = new JwtBearerEvents()//从url获取token
                    {
                        OnMessageReceived = context =>
                        {
                            if (context.HttpContext.Request.Path.StartsWithSegments("/Hubs/ChatRoomHub"))//判断访问路径
                            {
                                var accessToken = context.Request.Query["access_token"];//从请求路径获取token
                                if (!string.IsNullOrEmpty(accessToken))
                                    context.Token = accessToken;//将token写入上下文给Jwt中间件验证
                            }
                            return Task.CompletedTask;
                        }
                    };
                }
            );

            services.AddSignalR();

            services.AddCors(options =>
            {
                options.AddPolicy(_myAllowSpecificOrigins, policy =>
                {
                    policy.WithOrigins("http://localhost:4200")
                    .AllowAnyHeader().AllowAnyMethod().AllowCredentials();
                });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1"));
            }

            app.UseCors(_myAllowSpecificOrigins);
            app.UseHttpsRedirection();

            app.UseRouting();

            //Token  授权、认证
            app.UseErrorHandling();//自定义的处理错误信息中间件
            app.UseAuthentication();//判断是否登录成功
            app.UseAuthorization();//判断是否有访问目标资源的权限

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
                endpoints.MapControllers();
            });
        }
    }
}

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

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

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

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ServerSignalR.Models
{
    [Authorize]//jwt认证
    public class ChatRoomHub:Hub
    {
        
        public override Task OnConnectedAsync()//连接成功触发
        {
            return base.OnConnectedAsync();
        }

        public Task SendPublicMsg(string msg)//给所有client发送消息
        {
            var roles = this.Context.User.Claims.Where(x => x.Type.Contains("identity/claims/role")).Select(x => x.Value).ToList();//获取角色
            var fromUserName = this.Context.User.Identity.Name;//从token获取登录人,而不是传入(前端ts方法的传入参数也需要去掉)
            string connId = this.Context.ConnectionId;
            string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}";
            return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
        }
    }
}

  然后ts添加

  constructor(
    private cookie: CookieService
  ) {
    var token  = this.cookie.get('spm_token');
    this.hubConnection=new signalR.HubConnectionBuilder()
    .withUrl('https://localhost:44313/Hubs/ChatRoomHub',
      {
        skipNegotiation:true,//跳过三个协议协商
        transport:signalR.HttpTransportType.WebSockets,//定义使用WebSocket协议通讯
        accessTokenFactory:()=> token.slice(7,token.length)//会自动添加Bearer头部,我这里已经有Bearer了,所以需要截掉
      }
    )
    .withAutomaticReconnect()
    .build();
    this.hubConnection.on('ReceivePublicMsg',msg=>{
      this.messages.push(msg);
      console.log(msg);
    });
  }
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ServerSignalR.Models
{
    [Authorize]//jwt认证
    public class ChatRoomHub:Hub
    {
        private static List<UserModel> _users = new List<UserModel>();
        public override Task OnConnectedAsync()//连接成功触发
        {
            var userName = this.Context.User.Identity.Name;//从token获取登录人
            _users.Add(new UserModel(userName, this.Context.ConnectionId));
            return base.OnConnectedAsync();
        }
        public override Task OnDisconnectedAsync(Exception exception)
        {
            var userName = this.Context.User.Identity.Name;//从token获取登录人
            _users.RemoveRange(_users.FindIndex(x => x.UserName == userName), 1);
            return base.OnDisconnectedAsync(exception);
        }

        public Task SendPublicMsg(string msg)//给所有client发送消息
        {
            var fromUserName = this.Context.User.Identity.Name;
            //var ss = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
            string str = $"[{DateTime.Now}]\r\n{fromUserName}:{msg}";
            return this.Clients.All.SendAsync("ReceivePublicMsg",str);//发送给ReceivePublicMsg方法,这个方法由SignalR机制自动创建
        }

        public Task SendPrivateMsg(string destUserName, string msg)
        {
            var fromUser = _users.Find(x=>x.UserName== this.Context.User.Identity.Name);
            var toUser = _users.Find(x=>x.UserName==destUserName);
            string str = $"";
            if (toUser == null)
            {
                msg = $"用户{destUserName}不在线";
                str = $"[{DateTime.Now}]\r\n系统提示:{msg}";
                return this.Clients.Clients(fromUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str);
            }
            str = $"[{DateTime.Now}]\r\n{fromUser.UserName}-{destUserName}:{msg}";
            return this.Clients.Clients(fromUser.WebScoketConnId,toUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str);
        }
    }
}
//加一个监听
    this.hubConnection.on('ReceivePublicMsg', msg => {
      this.messages.push('公屏'+msg);
      console.log(msg);
    });
    this.hubConnection.on('ReceivePrivateMsg',msg=>{
      this.messages.push('私聊'+msg);
      console.log(msg);
    });

//加一个发送
    if (this.talkType == 1)
      this.hubConnection.invoke('SendPublicMsg', this.msg);
    if (this.talkType == 3){
      console.log('11111111111111');
      this.hubConnection.invoke('SendPrivateMsg',this.toUserName, this.msg);
    }

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

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

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

namespace ServerSignalR.Controllers
{
    //[Authorize]
    public class HomeController : Controller
    {
        private IHubContext<ChatRoomHub> _hubContext;
        public HomeController(IHubContext<ChatRoomHub> hubContext)
        {
            _hubContext = hubContext;
        }
        [HttpGet("Welcome")]
        public async Task<ResultDataModel<bool>> Welcome()
        {
            await _hubContext.Clients.All.SendAsync("ReceivePublicMsg", "欢迎");
            return new ResultDataModel<bool>(true);
        }
    }
}

  至此,感谢关注!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK