在C#中实现发送服务器推送事件(不使用ASP.NET / MVC / ...)
在C#中实现发送服务器推送事件(不使用ASP.NET / MVC / ...)
我需要在一个C#应用程序中实现SSE(Server Sent Events)来完成一个项目。虽然这听起来很简单,但我不知道该如何解决这个问题。
由于我对C#还很陌生(虽然对编程并不陌生),所以我去Google上寻找一些示例代码。到目前为止,我发现可以学习如何用C#构建HTTP服务器或者消费服务器发送的事件,但没有找到关于如何发送SSE的信息。
我试图理解的问题是:我如何在接收到请求后继续发送更新的数据?通常,你会接收到一个请求,执行操作然后回复,然后关闭连接。但在这种情况下,我希望能够“粘”在响应流中,并且每当应用程序中的某个事件触发时,发送新的数据。
对我来说,问题在于这种基于事件的方法:它不是基于循环或定时器的轮询和更新。而是应用程序会像这样说:“嘿,发生了什么事情。我真的应该告诉你!”
简而言之:我如何保持对响应流的控制并发送更新数据,而不是基于循环或计时器,而是在特定事件发生时发送?
另外,在我忘记之前:我知道有一些库可以做到这一点。但从我目前看到的(以及我理解的内容;如果我理解有误,请纠正我),这些解决方案都依赖于ASP.NET/MVC/你来命名它。而我只是在编写一个“纯粹”的C#应用程序,我不认为我满足这些要求。
问题的出现原因是作者想要在C#中实现发送服务器发送事件,但是不希望使用ASP.NET/MVC等框架。他们已经注意到了SignalR这个库,但是因为SignalR是ASP.NET的一部分,这让他们有些犹豫。虽然SignalR不依赖于System.Web或IIS,但是作者还是希望找到一种在非ASP.NET环境下使用SignalR的方法。
解决方法是使用自托管的ASP.NET应用程序来实现。如果使用的是.NET Core,那么默认情况下就是自托管的,可以作为普通的控制台应用程序运行。还有可能将SignalR包含在VSTO插件中的想法,但是担心这样做会导致线程问题(即服务器阻塞后续代码路径)。
在对SignalR的讨论中,某些情况下SignalR虽然是ASP.NET的一部分,但是并不意味着它依赖于System.Web。作者表示他并没有说SignalR依赖于System.Web,而且ASP.NET Core根本没有任何对System.Web的依赖。他还提到了Web API和SignalR在依赖ASP.NET方面是一样的。最后,讨论的双方对文章进行了一些修改,以便更准确地表达信息。
作者想要在C#中实现发送服务器发送事件,但不使用ASP.NET/MVC等框架。解决方法是使用自托管的ASP.NET应用程序来实现,可以将SignalR包含在VSTO插件中。同时,作者强调SignalR不依赖于System.Web或IIS,特别是ASP.NET Core根本不依赖于System.Web。
问题的出现原因是希望在没有使用ASP.NET、MVC等框架的情况下,在C#中实现发送服务器发送事件(Server Sent Events)。解决方法是使用OWIN自托管的WebAPI来实现一个简单的服务器,然后使用PushStreamContent来发送HTTP响应头,并保持连接以在数据可用时写入数据。
以下是示例代码:
public class EventController : ApiController { public HttpResponseMessage GetEvents(CancellationToken clientDisconnectToken) { var response = Request.CreateResponse(); response.Content = new PushStreamContent(async (stream, httpContent, transportContext) => { using (var writer = new StreamWriter(stream)) { using (var consumer = new BlockingCollection()) { var eventGeneratorTask = EventGeneratorAsync(consumer, clientDisconnectToken); foreach (var event in consumer.GetConsumingEnumerable(clientDisconnectToken)) { await writer.WriteLineAsync("data: " + event); await writer.WriteLineAsync(); await writer.FlushAsync(); } await eventGeneratorTask; } } }, "text/event-stream"); return response; } private async Task EventGeneratorAsync(BlockingCollection producer, CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { producer.Add(DateTime.Now.ToString(), cancellationToken); await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } } finally { producer.CompleteAdding(); } } }
上述代码中的重要部分是PushStreamContent,它发送HTTP头部并保持连接以在数据可用时写入数据。在示例中,事件在一个额外的任务中生成,并被添加到生产者-消费者集合中。每当有新事件到达时,GetConsumingEnumerable将自动被通知。然后,将新事件以正确的服务器发送事件格式写入流并刷新。在实际应用中,您需要每隔一分钟左右发送一些伪Ping事件,因为长时间未发送数据的打开流可能会被操作系统/框架关闭。
以下是用于测试的示例客户端代码:
using (var client = new HttpClient()) { using (var stream = await client.GetStreamAsync("http://localhost:9000/api/event")) { using (var reader = new StreamReader(stream)) { while (true) { Console.WriteLine(reader.ReadLine()); } } } }
这段代码会在一个异步方法中使用。它使用HttpClient来获取流,并使用StreamReader读取数据。然后,在一个无限循环中将数据打印到控制台。
为什么要使用这种方法而不是SignalR呢?因为对于一些特定需求,如在桌面C# WPF应用程序中消费服务器发送事件,可能不能控制客户端的实现。在这种情况下,使用上述方法可以实现流式传输。当然,对于流式传输的其他场景,SignalR是一个很好的工具。