一个多月前接手了一个产线机器人项目,上位机以读写寄存器的方式控制机器人,服务器就是用 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. Chrome 手机端网页如何使用开发者模式

    chrome 手机端网页如何调试 在Chrome手机端,你可以使用Chrome开发者工具来调试网页.下面是一些步骤: 首先,确保你的手机已经开启开发者模式.打开USB调试功能或可以通过USB连接或无线 ...

  2. Mysql进击篇-存储引擎、索引、sql优化、视图、锁、innoDb、管理

    1.存储引擎 (1)连接层 最上层是一些客户端和连接服务,主要完成一些类似于连接处理,授权认证.以及相关的安全方案,服务器也会为安全接入的每个客户端验证它所具有的操作权限 (2)服务层 第二层架构主要 ...

  3. cmake构建32位应用程序

    1. 背景介绍 2. 工具介绍 3. 环境搭建 4. MinGW编译器版本 1. 背景介绍 最近需要使用第三方动态库文件G33DDCAPI.dll进行二次开发.由于这个动态库文件生成的时间比较早,且只 ...

  4. Springboot集成Netty实现TCP通讯

    Netty测试客户端 package com.coremain; import com.coremain.handler.ServerListenerHandler; import io.netty. ...

  5. Python开发之Django框架

    一. Django框架 01.网络软件开发架构演变过程 02.HTTP协议讲解 03.web应用与框架介绍及手撸web框架 04.Django入门项目创建与必会三板斧 05.Django静态文件配置与 ...

  6. Programming abstractions in C阅读笔记:p166-p175

    <Programming Abstractions In C>学习第58天,p166-p175总结. 一.技术总结 1.斐波那契数列(Fibonacci Sequenc) (1)斐波那契数 ...

  7. python系列:argparse详解 外部传参给python的库

    一.argparse简介 argparse 模块是 Python 内置的用于命令项选项与参数解析的模块,argparse 模块可以让人轻松编写用户友好的命令行接口,能够帮助程序员为模型定义参数. ar ...

  8. RK3588平台产测之ArmSoM产品高温环境测试

    1. 简介专栏总目录 ArmSoM团队在产品量产之前都会对产品做几次专业化的功能测试以及性能压力测试,以此来保证产品的质量以及稳定性 优秀的产品都要进行严苛的多次全方位的功能测试以及性能压力测试才能够 ...

  9. Java多线程笔记全过程(一)

    一.多线程最基础的基本概念 一个程序最少需要一个进程,而一个进程最少需要一个线程. 我们常说的高并发,也并不全是线程级别的并发,在很多开发语言中,比如PHP,很常见的都是进程级别的并发.但是在Java ...

  10. 初探富文本之React实时预览

    初探富文本之React实时预览 在前文中我们探讨了很多关于富文本引擎和协同的能力,在本文中我们更偏向具体的应用组件实现.在一些场景中比如组件库的文档编写时,我们希望能够有实时预览的能力,也就是用户可以 ...