说在前面的话

距离上次更新已然快过去一年了,这中间日子里进入了非常繁忙的项目迭代开发中,时至今日终于有空停下来写一写之前的博客计划,续更后的第一篇,温故知新,用一个Demo介绍技术点的落地实操,如有不同意见评论区留下你的想法,Of course ,如果你杠精就是你对。

依照惯例,源代码在文末,需要自取~

不同解决方案?

直接看看执行效果,看完之后,你是否会与我选择同样的技术方案呢?
温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

实现效果

一个用户列表,可以展示所有用户的信息,需要对其中的密码进行加密,加密使用非对称加密,点击加密按钮以及解密按钮,实时地可以看到加密和解密的数据。

乍一看看可太简单,RSA的非对称加密,网上直接ctrl C V一套已有的就完事,就是要解决实时性的问题。

这里我选择了SignlR做实时推送,然后为了可以看到效果与性能考虑,使用了ConcurrentQueue线程安全队列,控制加解密的速度。

好了砖头抛出来了,看各位大佬骚操作

拉代码看代码

如果你不晓得这几个技术点该如何加入你的框架中,或者知道一些概念,但是没用过,下文适合你食用!

RSA加解密

非对称加密的使用现在已然太多示例,提供的项目源代码中,专门提供了一个可以直接跑的Demo,拉下来F5,调试一下完事,贴心的为你提供了Web API接口与测试页面。

温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

QueueDemo 作为WebApi启动, RSAProcessing.MVC 作为前端页面启动

温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

一下就是核心 RSA加密处理程序的核心代码,Ctrl C V之后, 使用 RSAProcessing.GenerateKeys(out string publicKey, out string pricateKey); 即可生成公钥和秘钥,加密解密使用方式同上。

    /// <summary>
    /// RSA加密处理程序
    /// </summary>
    public static class RSAProcessing
    {
        /// <summary>
        ///  生成RSA密钥对
        /// </summary>
        /// <param name="publicKey"></param>
        /// <param name="privateKey"></param>
        public static void GenerateKeys(out string publicKey, out string privateKey)
        {
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                publicKey = rsa.ToXmlString(false);
                privateKey = rsa.ToXmlString(true);
            }
        }

        /// <summary>
        ///  使用公钥加密文本
        /// </summary>
        /// <param name="plainText"></param>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        public static string Encrypt(string plainText, string publicKey)
        {
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);

            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(publicKey);
                byte[] encryptedBytes = rsa.Encrypt(plainBytes, false);
                return Convert.ToBase64String(encryptedBytes);
            }
        }

        /// <summary>
        ///  使用私钥解密文本
        /// </summary>
        /// <param name="encryptedText"></param>
        /// <param name="privateKey"></param>
        /// <returns></returns>
        public static string Decrypt(string encryptedText, string privateKey)
        {
            byte[] encryptedBytes = Convert.FromBase64String(encryptedText);

            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(privateKey);
                byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false);
                return Encoding.UTF8.GetString(decryptedBytes);
            }
        }

......

SignlR的实时推送

SignlR是什么?这个不用我去百度,ChatGPT可以给你一个简洁有效的答案。

温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

看到这里,可能你会有和我一样感受,这个作为实时通讯,是不是我直接做一个仿QQ和微信的聊天工具来,做大做强? 看看 GPT的回答:

温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

可以看到ChatGPT可以给予我们绝大部分答案,但是这里给大家补充一下:

  • 通讯方式的选择取决于浏览器版本以及服务端和客户端能力范围内的最佳通讯方式,通常是WebSocket > Server-Sent Events > Long Poling
  • SignalR不仅仅可在线聊天的通讯软件,做事件推送也非常好用,例如直播或者视频的观看人数统计等等

SignlR的.net 6代码实现

使用之前,一定先去看看微软的官方Demo!
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-6.0

温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP

以上是简易的两个Web应用的架构图,5102的WebApi作为服务端提供数据,MVC 应用作为客户端接受数据,以及发送SignalR连接,因为他们是两个Web应用,所属的web域不同,客户端请求服务端,需要服务端配置允许跨域请求。

注册服务

在5102的服务端中注册SignalR ,并且配置允许跨域

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyMethod()
               .AllowAnyHeader()
               .WithOrigins("http://localhost:5067")
               .AllowCredentials(); // 允许包含凭据;
    });
});
builder.Services.AddSignalR();

...注册其他的服务...  

var app = builder.Build();

app.UseCors();
app.MapHub<QueueHub>("/queueHub");  // 配置路由

配置消息处理中心

在持久性连接的基础上,SignalR提供了一个更高层次的抽象层:Hub,基于javascript的灵活性和C#的动态特性,Hub是一个至关重要的开发模式,它消弭了客户端和服务端这两个独立的物理环境之间的界限。 在Web环境中最通用的使用模式允许我们透明地在客户端和服务端之间进行方法调用。

简单的说,就是双向RPC,即可以直接从客户端调用服务器端的方法,同时服务端也可以调用客户端的方法。

using Microsoft.AspNetCore.SignalR;

namespace QueueDemo.Core
{
    /// <summary>
    /// 队列的signalR总线
    /// </summary>
    public class QueueHub : Hub
    {
        /// <summary>
        /// 加入连接的事件
        /// </summary>
        /// <returns></returns>
        public override async Task OnConnectedAsync()
        {
            GlobalUserInfo.Clients = Clients;
            await base.OnConnectedAsync();
        }

        /// <summary>
        /// signalR推送加密信息
        /// </summary>
        /// <param name="userId">用户id</param>
        /// <param name="message">加密数据</param>
        /// <returns></returns>
        public async Task SendEncryptDequeue(int userId, string message)
        {
            await GlobalUserInfo.Clients.All.SendAsync("ReceiveEncrypt", userId, message);
        }

        /// <summary>
        ///  signalR推送解密信息
        /// </summary>
        /// <param name="userId">用户id</param>
        /// <param name="message">解密数据</param>
        /// <returns></returns>
        public async Task SendDecryptDequeue(int userId, string message)
        {
            await GlobalUserInfo.Clients.All.SendAsync("ReceiveDecrypt", userId, message);
        }
    }
}

客户端JS配置

客户端主要需要做的就是,与SignalR服务端建立连接,接受服务端推送过来的数据。

<script>
    let queueHost = 'http://localhost:5102';

    // 创建signalR连接
    var connection = new signalR.HubConnectionBuilder().withUrl(queueHost + "/queueHub").build();

    // 接收到  ReceiveEncrypt 的消息
    connection.on("ReceiveEncrypt", function (userId, message) {
        console.log(userId);
        console.log(message);
        // 使用特定 id 来定位并修改文本内容
        $('#en_' + userId).text(message);
    });

    //  接收到  ReceiveDecrypt 的消息
    connection.on("ReceiveDecrypt", function (userId, message) {
        console.log(userId);
        console.log(message);
        // 使用特定 id 来定位并修改文本内容
        $('#de_' + userId).text(message);
    });

    // 连接成功
    connection.start().then(function () {
        console.log("Connection Success")
    }).catch(function (err) {
        return console.error(err.toString());
    });

</script>

ConcurrentQueue队列连接客户端与服务端

SignalR与QueueHub的连接已然搞定,就是如何触发推送加解密信息。

这里使用的方案是ConcurrentQueue队列,将所有的用户信息推送到加密队列(&解密队列)中,出队一个UserInfo,就加密(&解密)一个用户信息,随后利用SignalR推送一个加密解密信息。

再说到队列,大家熟知都是RabbitMQ ,Kafka , RocketMQ,然而在实战中,急着要用一个队列,如果此时用上RabbitMQ,那么还需要额外部署一个应用,开防火墙等等,这一套搞下来,加上走流程快的话一周过去了,此时用一个内存队列就是最合适的,用线程加内存队列可以做一个低配版的rabbitmq,先实现业务需求,再后期去升级。

初始化队列

依上述所言,队列为了简单易用,在StartUp中创建两个线程去跑。

// 创建并启动后台任务            
UserQueueHandler ledgerQueue = new(new QueueHub());
Task task = Task.Run(() => ledgerQueue.DeProcessQueue(builder.Services, GlobalUserQueue.DecryptCancelToken.Token));
Task task2 = Task.Run(() => ledgerQueue.EnProcessQueue(builder.Services, GlobalUserQueue.EncryptCancelToken.Token));

两个全局的静态变量存储队列的配置,并且创建两个中断循环的开关。

  /// <summary>
  /// 全局用户队列初始化
  /// </summary>
  public static class GlobalUserQueue
  {
      /// <summary>
      /// 解密队列 退出循环开关
      /// </summary>
      public static CancellationTokenSource DecryptCancelToken = new();
      /// <summary>
      /// 加密队列 退出循环开关
      /// </summary>
      public static CancellationTokenSource EncryptCancelToken = new();

      /// <summary>
      /// 解密队列
      /// </summary>
      public static ConcurrentQueue<DecryptRequest> DecryptQueue = new();

      /// <summary>
      /// 加密队列
      /// </summary>
      public static ConcurrentQueue<EncryptRequest> EncryptQueue = new();
  }

初始化用户队列处理程序

    /// <summary>
    /// 用户队列处理程序
    /// </summary>
    public class UserQueueHandler
    {
        private QueueHub _queueHub;

        /// <summary>
        /// 注入队列总线
        /// </summary>
        /// <param name="queueHub"></param>
        public UserQueueHandler(QueueHub queueHub)
        {
            _queueHub = queueHub;
        }

        /// <summary>
        /// 启动解密队列
        /// </summary>
        /// <param name="services">注册服务</param>
        /// <param name="cancellationToken">退出Token控制器</param>
        /// <returns></returns>
        public async Task DeProcessQueue(IServiceCollection services, CancellationToken cancellationToken)
        {
            try
            {
                var serviceProvider = services.BuildServiceProvider();
                // Rsa加解密服务
                var rsaService = serviceProvider.GetRequiredService<IRSAService>();
                await Console.Out.WriteLineAsync($"Decrypt ProcessQueue Start! ");

                while (true)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }

                    // 解密队列出队
                    if (GlobalUserQueue.DecryptQueue.TryDequeue(out DecryptRequest deRequest))
                    {
                        await Console.Out.WriteLineAsync($"DeProcessQueue UserIndexId -- {deRequest.UserIndex} -- {JsonConvert.SerializeObject(deRequest)} ");
                        try
                        {
                            // 解密
                            var deScryptRsp = rsaService.Decrypt(deRequest);
                            if (deScryptRsp != null)
                            {
                                var userInfo = GlobalUserInfo.UserInfos.First(x => x.Index == deScryptRsp.UserIndex);
                                userInfo.DecryptedPwd = deScryptRsp.DecryptedPwd;

                                // 推送解密信息到前端
                                await _queueHub.SendDecryptDequeue(userInfo.UserId, userInfo.DecryptedPwd);
                                await Console.Out.WriteLineAsync($"DeProcessQueue Success! UserId--{userInfo.UserId} UserIndex--{deScryptRsp.UserIndex} ");
                            }
                            await Task.Delay(1000);
                        }
                        catch (Exception ex)
                        {
                            await Console.Out.WriteLineAsync($"DeProcessQueue Error --{JsonConvert.SerializeObject(ex)} ");
                        }
                    }
                    else
                    {
                        // 队列中无数量 则休眠10秒
                        await Task.Delay(10000);
                    }
                }
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(ex.Message);
                throw;
            }
        }
}

这个方法就是触发推送加解密的信息,连接客户端和服务端的核心。

  • ConcurrentQueue出队使用的是 GlobalUserQueue.DecryptQueue.TryDequeue(out DecryptRequest deRequest)
  • 利用serviceProvider.GetRequiredService<IRSAService>(); 获取RSA解密服务,然后再调用解密方法。
  • 拿到解密之后的信息之后,使用queueHub的方法 await _queueHub.SendDecryptDequeue(userInfo.UserId, userInfo.DecryptedPwd); 推送解密数据
  • 推送数据主要是SignalR的方法 await GlobalUserInfo.Clients.All.SendAsync("ReceiveDecrypt", userId, message);

总结

稍微总结一下,

  • Demo集成了RSA加密、SignalR推送、内存版的队列。
  • 讲解了一下SignalR的用法以及注意事项
  • 内存版的队列在Web应用中的优势
  • 有更好的更快速的解决方案评论区留下信息
    项目拉取下来,在解决方案设置中,同时启动两个项目即可。
    温故知新,signalR、RSA加密、ConcurrentQueue队列-LMLPHP
    源代码仓库 https://github.com/OrzCoCo-Y/QueueDemo

参考资料

【微软文档】 https://learn.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-6.0
【ChatGPT】

06-27 21:39