26

使用SignalR从服务端主动推送警报日志到各种终端(桌面、移动、网页)

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA%3D%3D&%3Bmid=2654077322&%3Bidx=3&%3Bsn=be2b70f26bad9451adee65687019e144
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.
neoserver,ios ssh client

工作上有个业务,.Net Core WebAPI作为服务端,需要将运行过程中产生的日志分类,并实时推送到各种终端进行报警,终端有桌面(WPF)、移动(Xamarin.Forms)、网站(Angular.JS)等,使用SignalR进行警报日志推送。

微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏。

https://dotnet9.com

阅读导航

  1. 本文背景

  2. 代码实现

  3. 本文参考

1.本文背景

工作上有个业务,.Net Core WebAPI作为服务端,需要将运行过程中产生的日志分类,并实时推送到各种终端进行报警,终端有桌面(WPF)、移动(Xamarin.Forms)、网站(Angular.JS)等,使用SignalR进行警报日志推送。

下面是桌面端的测试效果:

7NbAfej.gif

2.代码实现

整个系统由服务端、桌面端、网站、移动端组成,结构如下:

qqi6BrF.png!web

2.1 服务端与客户端都使用的日志实体类

简单的日志定义,服务端会主动将最新日志通过AlarmLogItem实例推送到各个终端:

/// <summary>
/// 报警日志
/// </summary>
public class AlarmLogItem
{
    public string Id { get; set; }
    /// <summary>
    /// 日志类型
    /// </summary>
    public AlarmLogType Type { get; set; }
    /// <summary>
    /// 日志名称
    /// </summary>
    public string Text { get; set; }
    /// <summary>
    /// 日志详细信息
    /// </summary>
    public string Description { get; set; }
    /// <summary>
    /// 日志更新时间
    /// </summary>
    public string UpdateTime { get; set; }
}

public enum AlarmLogType
{
    Info,
    Warn,
    Error
}

2.2 服务端

使用 .Net Core 2.2 搭建的Web API项目

2.2.1 集线器类AlarmLogHub.cs

定义集线器Hub类AlarmLogHub,继承自Hub,用于SignalR通信,看下面的代码,没加任何方法,您没看错:

public class AlarmLogHub : Hub
{}

2.2.2 Startup.cs

需要在此类中注册SignalR管道及服务,在下面两个关键方法中用到,B/S后端的朋友非常熟悉了。

  1. ConfigureServices方法

添加SignalR管道(是这个说法吧?):

services.AddSignalR(options => { options.EnableDetailedErrors = true; });
  1. Configure方法注册SignalR服务地址

端口用的8022,客户端访问地址是:http://localhost:8022/alarmlog

app.UseSignalR(routes =>
{
    routes.MapHub<AlarmLogHub>("/alarmlog");
});

2.2.3 SignalRTimedHostedService.cs

这是个 关键类 ,用于服务端主动推送日志使用,Baidu、Google好久才找到,站长技术栈以C/S为主,B/S做的不多,没人指点,心酸,参考网址:How do I push data from hub to client every second using SignalR 。

该类继承自IHostedService,作为服务自启动(乱说的),通过SignalRTimedHostedService 的构造函数依赖注入得到IHubContext<AlarmLogHub>的实例,用于服务端向各客户端推送日志使用(在StartAsync方法中开启定时器,模拟服务端主动推送警报日志,见 DoWork 方法):

internal class SignalRTimedHostedService : IHostedService, IDisposable
{
    private readonly IHubContext<AlarmLogHub> _hub;
    private Timer _timer;

    //模拟发送报警日志
    List<AlarmLogItem> lstLogs = new List<AlarmLogItem> {
            new AlarmLogItem{ Type=AlarmLogType.Error,Text="OK WebSocket断连",Description="尝试连接50次,未成功重连!"},
            new AlarmLogItem{ Type=AlarmLogType.Warn,Text="OK WebSocket断开重连",Description="尝试连接5次,成功重连!"},
            new AlarmLogItem{ Type=AlarmLogType.Warn,Text="OK Restfull断连",Description="尝试连接30次,成功重连!"},
            new AlarmLogItem{ Type=AlarmLogType.Error,Text="OK WebSocket断连",Description="第一次断开链接!"},
            new AlarmLogItem{ Type=AlarmLogType.Info,Text="OK WebSocket连接成功",Description="首次成功连接!"},
            new AlarmLogItem{ Type=AlarmLogType.Error,Text="OK WebSocket断连",Description="尝试连接第7次,未成功重连!"}
        };

    Random rd = new Random(DateTime.Now.Millisecond);

    public SignalRTimedHostedService(IHubContext<AlarmLogHub> hub)
    {
        _hub = hub;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(1));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        if (DateTime.Now.Second % rd.Next(1, 3) == 0)
        {
            AlarmLogItem log = lstLogs[rd.Next(lstLogs.Count)];
            log.UpdateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
            _hub.Clients.All.SendAsync("ReceiveAlarmLog", log);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

SignalRTimedHostedService 类作为Host服务(继承自 IHostedService),需要在Startup.cs的ConfigureServices方法中注册管道(是吧?各位有没有B/S比较好的书籍推荐,站长打算有空好好学学):

services.AddHostedService<SignalRTimedHostedService>();

服务端关键代码已经全部奉上,下面主要说说桌面端和移动端代码,其实两者代码类似。

2.3 网站

参考 index.html

2.4 桌面端(WPF)

使用 .Net Core 3.0创建的WFP工程,需要引入Nuget包:Microsoft.AspNetCore.SignalR.Client

界面用一个ListView展示收到的日志:

<Grid>
    <ListBox x:Name="messagesList"  RenderTransformOrigin="-0.304,0.109" BorderThickness="1" BorderBrush="Gainsboro"/>
</Grid>

后台写的简陋,直接在窗体构造函数中连接服务端SignalR地址:http://localhost:8022/alarmlog, 监听服务端警报日志推送:ReceiveAlarmLog。

using AppClient.Models;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Threading.Tasks;
using System.Windows;

namespace SignalRChatClientCore
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        HubConnection connection;
        public MainWindow()
        {
            InitializeComponent();

            connection = new HubConnectionBuilder()
                .WithUrl("http://localhost:8022/alarmlog")
                .Build();

            connection.Closed += async (error) =>
            {
                await Task.Delay(new Random().Next(0, 5) * 1000);
                await connection.StartAsync();
            };
            connection.On<AlarmLogItem>("ReceiveAlarmLog", (message) =>
            {
                this.Dispatcher.Invoke(() =>
                {
                    messagesList.Items.Add(message.Description);
                });
            });

            try
            {
                connection.StartAsync();
                messagesList.Items.Add("Connection started");
            }
            catch (Exception ex)
            {
                messagesList.Items.Add(ex.Message);
            }
        }
    }
}

2.4 移动端

移动端其实和桌面端类似,因为桌面端使用的 .Net Core 3.0,移动端使用的 .NET Standard 2.0,都需要引入Nuget包:Microsoft.AspNetCore.SignalR.Client。

界面使用ListView展示日志,这就不贴代码了,使用的MVVM方式,直接贴ViewModel代码吧,大家只看个大概,不要纠结具体代码,参照桌面.cs代码,是不是一样的?

using AppClient.Models;
using AppClient.Views;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
using System.Linq;

namespace AppClient.ViewModels
{
    /// <summary>
    /// 报警日志VM
    /// </summary>
    public class AlarmItemsViewModel : BaseViewModel
    {
        private ViewState _state = ViewState.Disconnected;

        /// <summary>
        /// 报警日志列表
        /// </summary>
        public ObservableCollection<AlarmLogItem> AlarmItems { get; set; }
        public Command LoadItemsCommand { get; set; }

        //连接报警服务端
        private HubConnection _connection;

        public AlarmItemsViewModel()
        {
            Title = "报警日志";
            AlarmItems = new ObservableCollection<AlarmLogItem>();
            LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());

            //收到登录成功通知
            MessagingCenter.Subscribe<LoginViewModel, LoginUser>(this, "LoginSuccess", async (sender, userInfo) =>
             {
                 //DisplayAlert("登录成功", userInfo.UserName, "确定");
             });
            MessagingCenter.Subscribe<NewItemPage, AlarmLogItem>(this, "添加项", async (obj, item) =>
            {
                var newItem = item as AlarmLogItem;
                AlarmItems.Add(newItem);
                await DataStore.AddItemAsync(newItem);
            });

            ConnectAlarmServer();
        }

        async Task ExecuteLoadItemsCommand()
        {
            if (IsBusy)
                return;

            IsBusy = true;

            try
            {
                AlarmItems.Clear();
                var items = await DataStore.GetItemsAsync(true);
                foreach (var item in items)
                {
                    AlarmItems.Add(item);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        private async Task ConnectAlarmServer()
        {
            if (_state == ViewState.Connected)
            {
                try
                {
                    await _connection.StopAsync();
                }
                catch (Exception ex)
                {
                    return;
                }
                _state = ViewState.Disconnected;
            }
            else
            {
                try
                {
                    _connection = new HubConnectionBuilder()
                      .WithUrl(App.Setting.AlarmHost)
                      .Build();
                    _connection.On<AlarmLogItem>("ReceiveAlarmLog", async (newItem) =>
                    {
                        AlarmItems.Add(newItem);
                        await DataStore.AddItemAsync(newItem);
                    });
                    _connection.Closed += async (error) =>
                    {
                        await Task.Delay(new Random().Next(0, 5) * 1000);
                        await _connection.StartAsync();
                    };
                    await _connection.StartAsync();
                }
                catch (Exception ex)
                {
                    return;
                }
                _state = ViewState.Connected;
            }
        }

        private enum ViewState
        {
            Disconnected,
            Connecting,
            Connected,
            Disconnecting
        }
    }
}

关键代码已经贴完了,希望对大家能有所帮助。

3.参考

  1. .NET 客户端 SignalR ASP.NET Core

  2. SignalR-samples

  3. How do I push data from hub to client every second using SignalR

除非注明,文章均由 Dotnet9 整理发布,欢迎转载。

转载请注明本文地址: https://dotnet9.com/6913.html

欢迎扫描下方二维码关注 Dotnet9 的微信公众号,本站会及时推送最新技术文章(微信公众号“ dotnet9_com ”):

Dotnet9.com

YbqmYrv.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK