【问题标题】:ASPNET Core Server Sent Events / Response flushASPNET Core 服务器发送事件/响应刷新
【发布时间】:2016-07-13 15:49:49
【问题描述】:

虽然没有官方文档,但有谁知道如何使用 ASP.NET Core 实现 SSE?

我怀疑一种实现可能会使用自定义中间件,但也许可以在控制器操作中做到这一点?

【问题讨论】:

  • 你说的是 SignalR 吗?
  • 不,Core 还没有 SignalR。
  • 事实上,在 rc1-final 中,SignalR 3 可用于官方 nuget 提要以及 myget 马厩myget.org/gallery/aspnetmaster 和 nightlies。这就是我问的原因。但是,当 ASP.NET Core 1.0 按照路线图github.com/aspnet/Home/wiki/Roadmap 中所述发布时,它还没有准备好发布
  • 谢谢,我不知道。我会看看源代码,也许我会发现一些有用的东西。在这种情况下,完整的框架有点矫枉过正。我有一个单向通道,使用 websockets 实现,我正在寻找如何使它更简单的方法。

标签: c# asp.net-core asp.net-core-mvc asp.net-core-1.0


【解决方案1】:

客户端 - wwwroot/index.html

在页面加载时,为 http://www.somehost.ca/sse 网址创建一个 EventSource。然后将其事件写入控制台。

<body>
    <script type="text/javascript">

        var source = new EventSource('sse');

        source.onmessage = function (event) {
            console.log('onmessage: ' + event.data);
        };

        source.onopen = function(event) {
            console.log('onopen');
        };

        source.onerror = function(event) {
            console.log('onerror');
        }

    </script>
</body>

服务器端替代方案 #1 - 使用中间件

中间件处理sse 路径。它将Content-Type 标头设置为text/event-stream,这是服务器套接字事件所需的。它写入响应流,而不关闭连接。它通过在写入之间延迟五秒钟来模仿工作。

app.Use(async (context, next) =>
{
    if (context.Request.Path.ToString().Equals("/sse"))
    {
        var response = context.Response;
        response.Headers.Add("Content-Type", "text/event-stream");

        for(var i = 0; true; ++i)
        {
            // WriteAsync requires `using Microsoft.AspNetCore.Http`
            await response
                .WriteAsync($"data: Middleware {i} at {DateTime.Now}\r\r");

            await response.Body.FlushAsync();
            await Task.Delay(5 * 1000);
        }
    }

    await next.Invoke();
});

服务器端替代方案 #2 - 使用控制器

控制器的作用与中间件完全相同。

[Route("/api/sse")]
public class ServerSentEventController : Controller
{
    [HttpGet]
    public async Task Get()
    {
        var response = Response;
        response.Headers.Add("Content-Type", "text/event-stream");

        for(var i = 0; true; ++i)
        {
            await response
                .WriteAsync($"data: Controller {i} at {DateTime.Now}\r\r");

            response.Body.Flush();
            await Task.Delay(5 * 1000);
        }
    }
}

Firefox 中的客户端控制台输出

这是 Firefox 控制台窗口中的结果。每五秒钟就有一条新消息到达。

onopen
onmessage: Message 0 at 4/15/2016 3:39:04 PM
onmessage: Message 1 at 4/15/2016 3:39:09 PM
onmessage: Message 2 at 4/15/2016 3:39:14 PM
onmessage: Message 3 at 4/15/2016 3:39:19 PM
onmessage: Message 4 at 4/15/2016 3:39:24 PM

参考文献:

【讨论】:

  • 谢谢肖恩,你的回答看起来很全面。我将很快转移声誉点,我仍然无法在本地计算机和 Azure 上的 RC1 上复制此点。看起来每次迭代都没有刷新响应。也许现在是进入 pre-RC2 的好时机。
  • 感谢您推动前沿 :) 现在我可以确认它可以按预期在 pre-RC2 上运行,但在 RC1 上不行。
  • @Zygimantas 好东西。 RC1 的东西很好,是一个“上线”的产品。 RC2 虽然是未来,但开始学习它是个好主意。我很高兴听到这对你有用。 :)
  • 我还注意到您使用的是 IHttpContextAccessor 而不是继承 ControllerBase 并调用 this.HttpContext。我以前在 RC1 上没见过这个。这样做有什么好处吗?
  • @Zygimantas 我不确定一个比另一个的优势。我知道新的 MVC 支持plain-old-crl-object (POCO) 控制器。换句话说,这些是不从Controller 类继承的控制器。我倾向于远离简单项目的继承以避免开销。继承Controller 类有很多好处。可以这么说,它带有很多辅助方法。这是课程。查看它的公共/保护成员,看看它给你什么:github.com/aspnet/Mvc/blob/dev/src/…
【解决方案2】:

服务器发送的事件可以完全在控制器动作中实现。

这是基于 Shaun Luttin 的回答,但它更像是一个真实世界的示例,它将无限期地保持打开连接,并发送消息到 EventSource 以响应正在创建的消息。

using Example.Models;
using Example.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Example.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SseMessagesController : ControllerBase
    {
        private readonly IMessageRepository messageRepository;
        private readonly JsonSerializerSettings jsonSettings;

        public SseMessagesController(IMessageRepository messageRepository)
        {
            this.messageRepository = messageRepository;
            this.jsonSettings = new JsonSerializerSettings();
            jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }

        [HttpGet]
        public async Task GetMessages(CancellationToken cancellationToken)
        {
            Response.StatusCode = 200;
            Response.Headers.Add("Content-Type", "text/event-stream");

            EventHandler<MessageCreatedArgs> onMessageCreated = async (sender, eventArgs) =>
            {
                try
                {
                    var message = eventArgs.Message;
                    var messageJson = JsonConvert.SerializeObject(message, jsonSettings);
                    await Response.WriteAsync($"data:{messageJson}\n\n");
                    await Response.Body.FlushAsync();
                }
                catch (Exception)
                {
                    // TODO: log error
                }
            };
            messageRepository.MessageCreated += onMessageCreated;

            while (!cancellationToken.IsCancellationRequested) {
                await Task.Delay(1000);
            }

            messageRepository.MessageCreated -= onMessageCreated;
        }
    }
}

每当EventSource 连接到/api/ssemessages 时,我们都会向消息存储库上的MessageCreated 事件添加一个事件委托。然后我们每隔1秒检查一下EventSource是否已经关闭,这会导致请求被取消。一旦请求被取消,我们就会移除事件委托。

事件委托从事件参数中获取Message对象,将其序列化为 JSON(使用驼峰式大小写与 ASP.NET Core 在返回对象结果时的默认行为一致),将 JSON 写入主体,然后刷新正文的流以将数据推送到EventSource。

有关创建事件委托的更多信息,请参阅 this article 和 this update for .NET Core。

另外,如果你在 Nginx 后面托管这个,你会想要阅读 this SO answer 和 this ServerFault answer。

【讨论】:

  • 您好,谢谢您的回答!不确定您是否知道,但这个问题已经存在 3 年多了 - 如果您想将精力集中在新问题上,可以使用“最新”选项卡来实现。例如,this 是最新 C# 问题的链接。
  • 您是否有 GitHub 存储库或 gist 以及包含 IMessageRepository 实现的完整示例?
  • @MarkG 我知道它是 1 年后,但这可能会有所帮助:github.com/Nordes/Sample.SSEvent(我不认为我会删除它,但它可以帮助查看事件的来源和如何广播)。
  • 感谢您的回答!使用“cancellationToken”和“response.body.FlushAsync”对我的项目至关重要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-11
  • 2020-08-12
  • 1970-01-01
  • 1970-01-01
  • 2015-09-24
  • 2015-06-16
  • 2013-08-20
相关资源
最近更新 更多