tnblog
首页
视频
资源
登录

.NetCore3.1及以上Server-Sent Events(SSE)轻量级主动推送和Redis发布订阅

909人阅读 2024/12/27 16:41 总访问:3548 评论:1 收藏:0 手机
分类: .NET6

1.前言

服务端推送,也称为消息推送或通知推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。

服务端推送的背景与需求主要基于以下几个诉求:

实时通知:在很多情况下,用户期望实时接收到应用的通知,如记录、新订单、新支付

节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。

常见推送场景有:一些实时数据,订单状态、称重数据等


一、解决方案:

1、传统实时处理方案:

轮询:这是一种较为传统的方式,客户端会定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。

长轮询(Long Polling):轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。


2、HTML5 标准引入的实时处理方案:

WebSocket:一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。

SSE:SSE(Server-Sent Events)是一种基于 HTTP 协议的推送技术。服务端可以使用 SSE 来向客户端推送数据,但客户端不能通过SSE向服务端发送数据。相较于 WebSocket,SSE 更简单、更轻量级,但只能实现单向通信。

两者的主要区别:
1.通信区别
    Server-Sent Events: 单向通信由服务端指向客户端
    WebSocket:             双工通信,可以交换数据
2.协议区别
    Server-Sent Events: HTTP
    WebSocket:             WebSocket
3.自动重连
    Server-Sent Events: 支持
    WebSocket:             不支持需要轮询实现
4.响应文本信息:
    Server-Sent Events: 文本格式
    WebSocket:             文本格式需要二进制编译或者自定义json文本也是可以
5.浏览器支持:
 Server-Sent Events:大部分支持,但在Internet Explorer及早期的Edge浏览器中并不被支持
 WebSocket:            主流浏览器(包括移动端)的支持较好 

二、前期准备
    由于我们是需要SSE和Redis发布与订阅实现数据的主动推送和接受前端入参实现定向订阅操作。
    我们在编写程序的时候需要使用到Redis组件:我这里推荐FreeRedis

三、代码编写
    高质量的代码往往写的很朴素且很沉默(我们前端就用HTML5,后端就使用控制台和Api了)

下面进入示例了:
1.前端代码

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>SSE Client</title>
  7. </head>
  8. <body>
  9.     <h1>Receive: <span id="sse"></span></h1>
  10.     <script>
  11.         const numberElement = document.getElementById("sse");
  12.         //在这里Code是可以去自定义获取链接的和普通的Query一样,是为了定向订阅Redis的发布数据
  13.         const source = new EventSource('http://localhost:8080/GetSSEMessage?code=12345');
  14.         source.onmessage = (event) => {
  15.             numberElement.innerText = event.data;
  16.         };
  17.         source.onerror = (error) => {
  18.             console.error("SSE error:", error);
  19.         };
  20.     </script>
  21. </body>
  22. </html>


2.后端Redis发布消息(记得在Nuget上拉取FreeSql)

  1. class Program
  2. {
  3.     static void Main()
  4.     {
  5.         for (int i = 0; i < 10000; i++)
  6.         {
  7.             RedisClient cli = new RedisClient("127.0.0.1:6379,password=,defaultDatabase=2");
  8.             //发布消息
  9.             cli.Publish("12345""ok");
  10.             Console.WriteLine($"发送消息成功,等待接收{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
  11.             Thread.Sleep(2000); //10s一次
  12.         }
  13.     }
  14. }

上面就是Redis发布的代码,都说了朴实无华

3.后端服务代码:
1.WeatherForecastController.cs

  1. using FreeRedis;
  2. using Masuit.Tools;
  3. using Microsoft.AspNetCore.Authorization;
  4. using Microsoft.AspNetCore.Mvc;
  5. using SSEDemo.Dto;
  6. using System.Runtime.CompilerServices;
  7. namespace SSEDemo.Controllers
  8. {
  9.     [ApiController]
  10.     [Route("[controller]")]
  11.     public class WeatherForecastController : ControllerBase
  12.     {
  13.         private static readonly string[] Summaries = new[]
  14.         {
  15.             "Freezing""Bracing""Chilly""Cool""Mild""Warm""Balmy""Hot""Sweltering""Scorching"
  16.         };
  17.         private readonly ILogger<WeatherForecastController> _logger;
  18.         private readonly IRedisClient _freeRedis;
  19.         public WeatherForecastController(ILogger<WeatherForecastController> logger, IRedisClient freeRedis)
  20.         {
  21.             _logger = logger;
  22.             _freeRedis = freeRedis;
  23.         }
  24.         /// <summary>
  25.         /// 获取天气预报
  26.         /// </summary>
  27.         /// <returns></returns>
  28.         [HttpGet]
  29.         public IEnumerable<WeatherForecast> Get()
  30.         {
  31.             return Enumerable.Range(15).Select(index => new WeatherForecast
  32.             {
  33.                 Date = DateTime.Now.AddDays(index),
  34.                 TemperatureC = Random.Shared.Next(-2055),
  35.                 Summary = Summaries[Random.Shared.Next(Summaries.Length)]
  36.             })
  37.             .ToArray();
  38.         }
  39.         /// <summary>
  40.         /// Server-Sent Event Message
  41.         /// </summary>
  42.         /// <param name="code">消息标识</param>
  43.         /// <returns></returns>
  44.         [HttpGet("GetSSEMessage")]
  45.         [AllowAnonymous]
  46.         public async IAsyncEnumerable<stringGetSSEMessage([FromQuery] string code, [EnumeratorCancellation] CancellationToken cancellationToken)
  47.         {
  48.             // 设置响应头
  49.             Response.Headers["Content-Type"] = "text/event-stream";
  50.             Response.Headers["Cache-Control"] = "no-cache";
  51.             Response.Headers["Connection"] = "keep-alive";
  52.             if (string.IsNullOrEmpty(code))
  53.             {
  54.                 var data = CreateMessage(string.Empty, "请传入有效标识!"false);
  55.                 yield return $"data: {data.ToJsonString()}\n\n"// 返回错误信息
  56.                 yield break// 结束方法
  57.             }
  58.             IDisposable subscription = null;
  59.             try
  60.             {
  61.                 // 创建异步流用于心跳
  62.                 var heartbeatTask = SendHeartbeatAsync(code, cancellationToken);
  63.           
  64.                 // 创建频道订阅
  65.                 subscription = _freeRedis.Subscribe(code, async (channel, message) =>
  66.                 {
  67.                     var model = CreateMessage(code, string.Empty, true);
  68.                     await WriteResponse(model.ToJsonString(), cancellationToken);
  69.                 });
  70.                 // 等待直到取消请求
  71.                 await heartbeatTask; // 等待心跳任务完成
  72.             }
  73.             finally
  74.             {
  75.                 subscription?.Dispose();
  76.             }
  77.         }
  78.         /// <summary>
  79.         /// 发送心跳包
  80.         /// </summary>
  81.         private async Task SendHeartbeatAsync(string code, CancellationToken ct)
  82.         {
  83.             while (!ct.IsCancellationRequested)
  84.             {
  85.                 await Task.Delay(5000, ct); // 每5秒发送一次心跳
  86.                 var model = CreateMessage(code, string.Empty, true);
  87.                 model.IsHeartbeat = true;
  88.                 await WriteResponse(model.ToJsonString(), ct); // 心跳消息
  89.             }
  90.         }
  91.         /// <summary>
  92.         /// 输出数据
  93.         /// </summary>
  94.         /// <param name="jsonData"></param>
  95.         /// <param name="ct"></param>
  96.         /// <returns></returns>
  97.         private async Task WriteResponse(string jsonData, CancellationToken ct)
  98.         {
  99.             if (ct.IsCancellationRequested) return;
  100.             await Response.WriteAsync($"data: {jsonData}\n\n", ct);
  101.             await Response.Body.FlushAsync(ct);
  102.         }
  103.         /// <summary>
  104.         /// 消息输出
  105.         /// </summary>
  106.         /// <param name="vehicleCode"></param>
  107.         /// <param name="message"></param>
  108.         /// <param name="isSuccess"></param>
  109.         /// <returns></returns>
  110.         private MessageOutDto CreateMessage(string code, string message, bool isSuccess)
  111.         {
  112.             return new MessageOutDto
  113.             {
  114.                 Code = code,
  115.                 IsSuccess = isSuccess,
  116.                 Message = message,
  117.             };
  118.         } 
  119.     }
  120. }

2.MessageOutDto.cs

  1. namespace SSEDemo.Dto
  2. {
  3.     public class MessageOutDto
  4.     {
  5.         /// <summary>
  6.         /// 是否成功
  7.         /// </summary>
  8.         public bool IsSuccess { getset; }
  9.         /// <summary>
  10.         /// 消息
  11.         /// </summary>
  12.         public string Message { getset; }
  13.         /// <summary>
  14.         /// 标记
  15.         /// </summary>
  16.         public string Code { getset; }
  17.         /// <summary>
  18.         /// 是否心跳包
  19.         /// </summary>
  20.         public bool IsHeartbeat { getset; } = false;
  21.     }
  22. }

3.Program.cs

  1. using FreeRedis;
  2. var builder = WebApplication.CreateBuilder(args);
  3. builder.Services.AddControllers();
  4. // 注册 FreeRedis 客户端
  5. builder.Services.AddSingleton<IRedisClient>(sp =>
  6. {
  7.     var redisConnectionString = "127.0.0.1:6379,password=,defaultDatabase=2";
  8.     return new RedisClient(redisConnectionString);
  9. });
  10. var app = builder.Build();
  11. // Configure the HTTP request pipeline.
  12. app.UseAuthorization();
  13. app.MapControllers();
  14. app.Run();


轻量级SSE配合Redis的订阅发布,实现后端主动推送到前端的代码就完成了

评价

尘叶心繁

2025/1/10 15:50:49

[good]

.netcore3.1及以上异常和响应中间件服务一条龙服务

一、为什么要有这篇文章: 在.Net Core3.1及以上的开发操作中,我们通常对日志记录及响应记录,还有一些其他的事物性记...

vs2022及以上版本,net core net6 net8 添加dll引用

现在不是右键添加dll引用了。 右键添加com引用: 然后在下面点击浏览现在你需要引用的dll即可: 其实添加项目引用里边也...

Windows下Redis的主从复制

Redis拥有非常强大的主从复制功能,而且还支持一个master可以拥有多个slave,而一个slave又可以拥有多个slave,从而形成强...

Redis常用命令

启动服务命令 redis-server.exe redis.windows.conf 连接命令 redis-cli.exe -h ip地址 -p 6379 连接本地...

Redis基础安装操作-windows版

一、下载 redis官方没有提供windows版本,需要从微软的git下载releases版二、安装,启动1.解压出来 启动服务 可能会双击会...

Redis常用配置

配置主从节点slaveof127.0.0.16379 可能会遇到持久化错误:Error reply to PING from master: ‘-MISCONF Redis is configu...

Redis常见问题

配置文件配置密码后启动无效的问题 那是因为redis服务双击启动没法作用上配置文件,你可以在配置在启动命令的时候指定一下...

Redis中主从、哨兵和集群这三个有什么区别

主从模式:备份数据、负载均衡,一个Master可以有多个Slaves。sentinel(哨兵)发现master挂了后,就会从slave中重新选举一个...

StackExchange.Redis操作Redis(net core支持)

官方git开源地址https://github.com/StackExchange/StackExchange.Redis官方文档在docs里边都是官方的文档通过nuget命令下...

Service-stack.Redis配置连接池与读写分离(处理并发相关等)

配置连接池与读写分类 //写节点(主节点) List&lt;string&gt;writes=newList&lt;string&gt;(); writes.Add(&quot;123456a...

Service-stack.Redis操作Redis 并发相关问题

1:不要循环去创建对象,循环去访问redis的时候要特别注意,应该传一个集合进去处理,而不是循环去处理2:要注意释放对象,使...

ServiceStack.Redis操作Redis设置数据过期问题

添加一个key并设置过期时间(例如这个设置2分10秒后过期)TimeSpants=newTimeSpan(0,2,10); byte[]intbit=BitConverter.GetB...

ServiceStack.Redis操作Redis配置单例模式

我携漫天星辰以赠你,仍觉漫天星辰不如你。单利的应该是连接池而不应该是redis对象。如果每次操作都是一个redis对象是会有...

Redis常用查询命令

hash相关查询hash的所有key:hkey + hash名称查询hash的所有某个key:hget + hash名称+ key名称List相关根据key查询list :l...

ASP.net 使用Redis实现单点登录

Session介绍 session是用来记录客户端用户信息的,在客户端第一次向服务器发起请求的时候服务器会生成一个sessionid并返回...
感谢挫折
排名
158
文章
3
粉丝
0
评论
1
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2025TNBLOG.NET
技术交流:群号656732739
联系我们:contact@tnblog.net
公网安备:50010702506256
欢迎加群交流技术