一个多月前接手了一个产线机器人项目,上位机以读写寄存器的方式控制机器人,服务器就是用 ASP.NET Core 写的 Web API。由于前一位开发者写的代码质量问题,导致上位机需要16秒才能启动。经过我近一个月的改造,除了保留业务逻辑代码,其他的基本重写。如今上位机的启动时间在网络状态良好的条件下可以秒启动。原上位机启动慢的原因:

1、启动时使用同步方式访问 Web API,在网络较弱时需要等待很长时间。我改为导步请求,并且不等待请求结果,直接显示窗口;如果前面的请求失败,在窗口显示后再次发出异步请求,并且不等待。如果再失败才提示用户。

2、原项目在 Main 方式处就连接PLC,而产线的PLC压根就没插电源。我改为在连接机器人之后才连接,同样是异步不等待。如果连不上直接忽略。

3、原项目是一个窗口一个项目,然后把这些窗口生成 .dll,放到一个目录下,主程序启动时从目录下扫描 .dll,通过反射动态实例化窗口。这根本不需要的,一个上位机不可能有几百个窗口吧,何必呢。我改为使用服务容器的方式管理窗口,主界面通过依赖注入自动获取子窗口列表,再添加到主界面上。每个子窗口实现 IPage 接口用于识别,接口里面定义标题和页面索引即可。

4、干掉 Log4Net,使用官方的 Logging 库。

5、通信用的 JSON 数据全改用 System.Text.Json,而不是某 Newton,修改后速度快了一个次元。

由于 Web API 程序是运行在服务器的 IIS 中的,上一位开发者没有实现日志功能(仅仅用 ASP.NET Core 应用程序默认开启的控制台等日志功能),问题是日志没有保存。

我原来的计划是把日志写到系统中,这样就能保存下来,用“事件查看器”就能欣赏。后来想想这方案不行,工厂那伙人肯定找不到日志在哪。写数据库里面?想想似乎没这个必要。简单粗暴,直接自定义一个 ILogger,把日志输出到文件中,然后加一个 Web API 读取文件,上位机那里就可以调用,返回日志内容。

后经过现场调试发现,其实也不需要这样。时间长了,会存下很多日志文件,就算用日期标识文件名也是很乱。实际上他们并不要求保存日志,只是在运作过程中实时监控机器人(应该叫机械臂)的工作状态而已。如果不出问题,他们甚至连日志都不看。上面用文件实现的日志方式,主要缺点是不能实时推到上位机。就算他们不看,那我现场调试也方便我自己。

于是,我又想到了另一方案:用 SignalR 实时向上位机推送日志。

----------------------------------------------------------------------------------------------------------------------------------------

上面都是大话,现在开始主题。

原理是这样的:上位机作为 SignalR 客户端,发起连接后,不用主动调用服务器上的方法,而是等服务器调用回调方法。

第一步,咱们要自定义一个 ILogger。

public class KingkingLogger : ILogger
{
private readonly string cateName; public KingkingLogger(string cate)
{
cateName = cate;
} public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return default;
} public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
} public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if(IsEnabled(logLevel) == false)
{
return;
}
// 获取格式化后的文本
string fstr = formatter(state, exception);
// 显示消息类型
string head = logLevel switch
{
LogLevel.Information => "消息",
LogLevel.Warning => "警告",
LogLevel.Error => "错误",
_ => "未知"
};
// 加个日期
string currdate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
// 连接字符串
fstr = $"[{head}:{cateName}][{currdate}]{fstr}";
// 触发事件
TransferLog?.Invoke(fstr);
} // 静态属性
public static Action<string>? TransferLog { get; set; }
}

我暂时想不到叫啥名字,就暂且叫它 Kingking 日志记录器吧,我在项目中的类是叫 WTFLogger 的,什么内涵你懂的,反正现在这项目只有我一个人在写,取这个名字也无所谓。这个类不复杂,我解释一下你就明白了。

1、字符串 cateName 是类别名称。就是记录日志时它属于哪个名录下的,比如我们常见的 Microsoft.Hosting.Lifetime、Microsoft.Hosting.Lifetime 等这些就是。在 Logging 库中有两种方式指定:一是用字符串,二是用 ILogger<T> ,这个类型T将作为日志类别的名称。这里我采用的是字符串方式,所以不使用 ILogger<T>。

2、BeginScope 方法的用处是当你要把 logger 用在 using 语句块时才会实现。正因为用在 using 块中,所以它要求是实现 IDisposable 接口。这个实现 IDisposable 的类一般不用公开。这方法会接收一个泛型参数 TState state。这个看你的需要了,运行库内部调用经常会用字典类型传递一些额外数据。这个 TState 你可以自定义。此处我不需要把 logger 用在 using 语句块中,所以直接返回 default(或null)。

3、IsEnabled 方法的功能是分析一下 logLevel 参数指定的日志级别当前是否要输出日志。如果需要输出日志,返回 true;不想输出日志返回 false。后面实现的 Log 方法中也会用到它,如果返回 false,那就不必去处理怎么输出日志了。

4、Log 方法是核心。在此方法中你尽情发挥吧,你想怎样输出日志就在这里完成。比如你要用 Debug 类输出,那就调用 Debug 类的成员输出;你用控制台输出就调用 Console 类的成员。我这里是要把日志传给 SignalR Hub 对象,让其传回给客户端,故要调用静态的 TransferLog 属性。此属性是委托类型,可以与方法绑定,因为咱们不能在这里调用 Hub,Hub 是由 SignalR 组件自动激活的。所以要用委托来间接实现传递。这个和事件的作用一样,只是我不用事件成员罢了。

顺便说一下,我项目中的类是同时把日志写入数据库的(不写文件了,写数据库里好清理),这里老周为了让示例简单,没有加上写入数据的代码。其实也没啥难度的,就是在数据库中加个表,用 EF Core 往表里 INSERT 一条记录。

第二步,实现 Provider。ILogger 咱们定义好了,但这个 Kingking 日志记录器可不是直接扔进服务容器,而是通过叫 ILoggerProvider 的对象来创建实例。就相当于一个工厂类。

public class KingkingLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new KingkingLogger(categoryName);
} public void Dispose()
{
return;
}
}

代码很简单,没啥玄机。不过,为了调用方便,咱们可以封装一个扩展方法。

public static class CustLoggerExtensions
{
public static ILoggingBuilder AddKingkingLogger(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, KingkingLoggerProvider>();
return builder;
}
}

这样就做到了像官方 API 那样,用 AddXXX 的方法添加日志功能,用法如下:

var builder = WebApplication.CreateBuilder(args);
// 配置日志
builder.Services.AddLogging(o =>
{
// 清空所有日志提供者
o.ClearProviders();
// 添加控制台日志输出
o.AddConsole();
// 添加咱们自己写的日志记录器
o.AddKingkingLogger();
});

第三步,实现 Hub。Hub 是 SignalR 通信的“中心”类,当访问的 URL 匹配时就会激活咱们的 Hub。自定义 Hub 只要从 Hub 类派生即可。

public class MyHub : Hub
{
public MyHub() {
// 这里关联的就是日志记录类中的静态委托
KingkingLogger.TransferLog = KingkingLogger_TransferLog;
} private void KingkingLogger_TransferLog(string obj)
{
// 向所有客户端发日志
Clients.All.SendAsync("onLogged", obj);
} protected override void Dispose(bool disposing)
{
if(disposing)
{
// 实例释放时移除关联
KingkingLogger.TransferLog = null;
}
base.Dispose(disposing);
}
}

逻辑很简单,就是有日志了就推送给客户端。Clients.All 是把消息发给所有连接的客户端。

这里顺便提一下:Hub 是支持依赖注入的,即你可以在 MyHub 的构造函数里注入你要用的组件,如 DBContext 等。这里我用不到其他组件,所以没有注入。

在Web应用程序初始化时要启用 SignalR 相关服务。

var builder = WebApplication.CreateBuilder(args);
……
builder.Services.AddSignalR();
var app = builder.Build();

还要 Map 一下终结点,以绑定请求 Hub 的地址。

var builder = WebApplication.CreateBuilder(args);
……
var app = builder.Build(); …… // 记得这个
app.MapHub<MyHub>("/hub"); app.Run();

这里我设定的地址是 http://localhost/hub。

不要以为这样就完事了,当你运行后用客户端一测试,你会发现连毛都接收不到。这是因为 Hub 对象的默认生命周期太短了,仅在用的时候实例化,然后马上 Dispose 了。然后你会想,那我重写 OnConnectedAsync 方法,关联 TransferLog 委托;再重写 OnDisConnectedAsync 方法,把 TransferLog 委托设置为 null。这个也是不行的,原因还是那个—— Hub 对象生命周期太短。

有什么办法让 Hub 长寿一点呢?还真有,直接把 Hub 类型注册进服务器中,并使用单实例。

var builder = WebApplication.CreateBuilder(args);
……
// 把Hub注册为单实例
builder.Services.AddSingleton<MyHub>();
builder.Services.AddSignalR();
var app = builder.Build();

第四步,客户端程序。客户端并不是只能用 JS 来写,.NET 团队也做了相关的 Nuget 包。在项目中引用一下。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.0" />
</ItemGroup> </Project>

在主窗口中放一个文本框,两个按钮。文本框显示收到的日志,按钮用来请求连接和断开连接。

using Microsoft.AspNetCore.SignalR.Client;

namespace TestClient;

public partial class Form1 : Form
{
// 连接对象
HubConnection hubConn;
public Form1()
{
InitializeComponent();
// 初始化连接
var connBuilder = new HubConnectionBuilder()
.WithUrl("http://localhost:6225/hub")
.WithAutomaticReconnect();
hubConn = connBuilder.Build();
// 关联方法
hubConn.On<string>("onLogged", OnLogRecv);
} private void OnLogRecv(string msg)
{
// 服务器回调,显示收到的日志
textBox1.Invoke(() =>
{
textBox1.AppendText(msg + Environment.NewLine);
});
} private async void btnConn_Click(object sender, EventArgs e)
{
try
{
await hubConn.StartAsync();
lbMessage.Text = "已建立连接";
}
catch(Exception ex) {
lbMessage.Text = ex.Message;
}
} private async void btnDisconn_Click(object sender, EventArgs e)
{
if(hubConn.State == HubConnectionState.Connected)
{
await hubConn.StopAsync();
lbMessage.Text = "已断开连接";
}
}
}

注意,在调用 On 方法时,onLogged 要与服务器上指定的一致,否则服务器回调无效

/*---------------- 服务器端 ------------------*/
private void KingkingLogger_TransferLog(string obj)
{
// 向所有客户端发日志
Clients.All.SendAsync("onLogged", obj);
} /*--------------------- 客户端 -------------------*/
hubConn.On<string>("onLogged", OnLogRecv);

为了测试能否真的传递了日志,咱们在服务端写几个 Mini-API 来验证。

app.MapGet("/", (ILoggerFactory logFact) =>
{
ILogger logger = logFact.CreateLogger("MINI Main");
logger.LogInformation("欢迎来到圆环世界");
return "Hello Guy";
});
app.MapGet("/start", (ILoggerFactory logFact) =>
{
ILogger logger = logFact.CreateLogger("MINI Go Go Go");
logger.LogWarning("游戏开始了,你必须先和QB签订契约");
return "圆神启动";
});
app.MapGet("/shot", (ILoggerFactory loggerFact) =>
{
ILogger logger = loggerFact.CreateLogger("MINI Wind");
logger.LogInformation("干得好,三发入魂");
return "第一局完胜";
});

同时启动服务端和客户端试试吧。为了使测试更真实,我启动了三个客户端。触发日志记录,请调用任意一个 API。

依次点击三个窗口上的“连接”按钮,确认全部都连上。

然后依次调用那几个 mini API 试试。

可以看到,三个客户端都收到日志推送了。

为了演示,没有数据存储,所以如果客户端没有及时连接,会丢失前面的日志。老周的实际项目中是用数据库存起来,用的时候再取出来发给客户端。默认是发最近的 100 条。如果上位机要看全部,就调用一下 Hub 的方法,Hub 的代码会 select 整个日志表再发回。

【ASP.NET Core】使用SignalR推送服务器日志的更多相关文章

  1. asp.net core 五 SignalR 负载均衡

           SignalR : Web中的实时功能实现,所谓实时功能,就是所连接的客户端变的可用时,服务端能实时的推送内容到客户端,而不是被动的等待客户端的请求.Asp.net SignalR 源码 ...

  2. asp.net core 使用 signalR(一)

    asp.net core 使用 signalR(一) Intro SignalR 是什么? ASP.NET Core SignalR 是一个开源代码库,它简化了向应用添加实时 Web 功能的过程. 实 ...

  3. SingalR 构建 推送服务器初探

    项目需要用到推送,于是重新研究了下推送框架,最好能够独立成一个服务,与业务无关的服务,可以给所有的项目通用.找了好久最终决定用SinglR 框架. Signal 是微软支持的一个运行在 Dot NET ...

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

    微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. 使用SignalR从服务端主动推送警报日志到各种终端(桌面.移动.网页) 阅读导航 本文背景 ...

  5. Asp.Net Core使用SignalR进行服务间调用

    网上查询过很多关于ASP.NET core使用SignalR的简单例子,但是大部分都是简易聊天功能,今天心血来潮就搞了个使用SignalR进行服务间调用的简单DEMO. 至于SignalR是什么我就不 ...

  6. .NET 云原生架构师训练营(ASP .NET Core 整体概念推演)--学习笔记

    演化与完善整体概念 ASP .NET Core 整体概念推演 整体概念推演到具体的形式 ASP .NET Core 整体概念推演 ASP .NET Core 其实就是通过 web framework ...

  7. ASP.NET Core 网站发布到Linux服务器(转)

    出处;ASP.NET Core 网站发布到Linux服务器 长期以来,使用.NET开发的应用只能运行在Windows平台上面,而目前国内蓬勃发展的互联网公司由于成本的考虑,大量使用免费的Linux平台 ...

  8. 用 centrifugo 搭建 消息推送服务器 docker + rancher 搭建

    关于消息推送服务器 目前有很多第三方的开放成熟的推送服务.鉴于项目需要 我们项目需要自己搭建 自己的推送服务. 我们的推送应用场景 聊天消息 项目内部消息提醒 移动设备接受消息 应用到的相关软件工具知 ...

  9. asp.net core 使用 signalR(二)

    asp.net core 使用 signalR(二) Intro 上次介绍了 asp.net core 中使用 signalR 服务端的开发,这次总结一下web前端如何接入和使用 signalR,本文 ...

  10. [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件

    本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...

随机推荐

  1. 使用GPU训练Pytorch模型

    如何使用GPU训练Pytorch模型 这两天的深度学习实验真实让人头疼,传说中的"猫狗大战",对模型的训练用CPU的话9h起步,12h是常态,大学生哪耗得起,因此查找资料搭建了GP ...

  2. 5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备

    用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备 项目 ++wmproxy++ gite: https://gitee.com/tickbh/wmproxy github ...

  3. How to Install Python on Linux

    Summary Hostmonster uses the preinstalled version of Python that ships with CentOS. Because of this ...

  4. 简述location规则优先级-实现域名跳转-不同语言-终端跳转-错误页面返回首页-腾讯公益首页

    1.简述location的常见规则优先级,并且逐个验证: = :精确匹配(必须全部相等) #精准匹配优先级最高 ~ :大小写敏感(正则表达式) #一般使用~*忽略大小写匹配 (正则表达式 有上下区分, ...

  5. P4221 [WC2018]州区划分 题解

    题目链接 题目描述 给出 \(n\) 个城市,\(m\) 条边,一个划分合法当且仅当所有划分中的点集和集合中点之间存在的边集所构成的图不构成欧拉回路且联通. 定义一个点集的值为 划分的总值为其中所有点 ...

  6. JUC并发编程学习笔记(三)生产者和消费者问题

    生产者和消费者问题 synchronized版-> wait/notify juc版->Lock 面试:单例模式.排序算法.生产者和消费者.死锁 生产者和消费者问题 Synchronize ...

  7. NPOI获取Excel文件里的形状/图片的坐标/锚点

    有个妹纸找我请教如何获取图片坐标,因此我到家后花了点时间,写了这份代码. 实测下来,可以正确获取 Excel 2003 版本的形状和图片这两种的坐标/锚点,以及 Excel 2007 版本的图片的坐标 ...

  8. WPF应用开发之控件动态内容展示

    在我们开发一些复杂信息的时候,由于需要动态展示一些相关信息,因此我们需要考虑一些控件内容的动态展示,可以通过动态构建控件的方式进行显示,如动态选项卡展示不同的信息,或者动态展示一个自定义控件的内容等等 ...

  9. 神经网络入门篇:详解核对矩阵的维数(Getting your matrix dimensions right)

    核对矩阵的维数 当实现深度神经网络的时候,其中一个常用的检查代码是否有错的方法就是拿出一张纸过一遍算法中矩阵的维数. \(w\)的维度是(下一层的维数,前一层的维数),即\({{w}^{[l]}}\) ...

  10. 可视化学习:WebGL的基础使用

    引言 继续复习可视化的学习.WebGL和其他Web端的图形系统存在很大的不同,是OpenGL ES规范在浏览器的实现,它最大的不同就在于它更接近底层,可以由开发者直接操作GPU来实现绘图,性能很好,可 ...