Asp.Net Core SignalR 系列博客
系列
- SignalR+Vue
- SignalR+Vue 服务端向客户端发送信息
- SignalR+Vue+Log4net 实时日志推送
- 待定......
源码地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo
效果
老规矩先看最后效果

步骤
配置log4net日志
实现日志推送,首先需要配置log4net日志,然后定义一个全局异常捕获器,用于捕获错误写入到日志文件。

先把nuget包安装一下。
然后需要配置log4net的xml信息,右键web项目“添加”->“新建项”

找到Web配置文件->“命名”->"点击添加"

然后把xml配置放入到config文件中,配置如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<log4net>
<appender name="DebugAppender" type="log4net.Appender.DebugAppender" >
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<!--全局异常日志-->
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<!--日志文件存放位置-->
<file value="../../../logs/system.log" />
<!--是否追加到日志文件中-->
<appendToFile value="true" />
<!--基于文件大小滚动设置-->
<rollingStyle value="Composite" />
<!--是否指定了日志文件名称-->
<staticLogFileName value="true" />
<!--根据日期生成日志文件-->
<!--<datePattern value="yyyyMMdd'.log'" />-->
<!--最多保留10个旧文件-->
<maxSizeRollBackups value="10" />
<!--日志文件的大小-->
<maximumFileSize value="1GB" />
<layout type="log4net.Layout.PatternLayout">
<!--日志模板,这个东西很重要后续读取日志文件的时候就是依据这个配置-->
<conversionPattern value="%n时间:%date{yyyy-MM-dd HH:mm:ss},%n线程Id:%thread,%n日志级别:%-5level,%n描述:%message|%newline"/>
</layout>
</appender>
<root>
<level value="All"/>
<appender-ref ref="DebugAppender" />
<appender-ref ref="RollingFile" />
</root>
</log4net>
</configuration>
想要更多配置的可以前往官网:http://logging.apache.org/log4net/release/config-examples.html
如果对生成多个文件夹有兴趣的可以看我另外:Asp.Net Core Log4Net 配置分多个文件记录日志(不同日志级别)
接下来就需要在Startup中配置log4net.
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Logger = LogManager.CreateRepository(Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
XmlConfigurator.Configure(Logger, new FileInfo("log4net.config"));
// _logger = LogManager.GetLogger(Logger.Name, typeof(Startup));
} public static ILoggerRepository Logger { get; set; }
按照我最开始说的,在配置好日志之后需要配置一个全局错误捕获器,直接上代码。
public class SysExceptionFilter : IAsyncExceptionFilter
{
readonly IHubContext<ChatHub> _hub;
//使用log4
ILog _log = LogManager.GetLogger(Startup.Logger.Name, typeof(SysExceptionFilter));
public SysExceptionFilter(IHubContext<ChatHub> hub)
{
_hub = hub;
}
public async Task OnExceptionAsync(ExceptionContext context)
{
//错误
var ex = context.Exception;
//错误信息
string message = ex.Message;
//请求方法的路由
string url = context.HttpContext?.Request.Path;
//写入日志文件描述 注意这个地方尽量不要用中文冒号,否则读取日志文件的时候会造成信息确实,当然你可以定义自己的规则
string logMessage = $"错误信息=>【{message}】,【请求地址=>{url}】";
//写入日志
_log.Error(logMessage);
//读取日志
var data = ReadHelper.Read();
//发送给客户端
await _hub.Clients.All.SendAsync("ReceiveLog", data);
//返回一个正确的200http码,避免前端错误
context.Result = new JsonResult(new { ErrCode = 0, ErrMsg = message, Data = true });
}
}
代码中的读取日志会在第二节中讲到。
在Startup服务中注册这个过滤器。
public void ConfigureServices(IServiceCollection services)
{
......
services.AddMvc(option =>
{
//添加错误捕获
option.Filters.Add(typeof(SysExceptionFilter));
//option.EnableEndpointRouting = false;
});
......
}
按照我这个配置将会在程序目录生成一个logs文件夹,以及一个system.log文件。

读取日志文件
在配置日志文件中已经将日志配置了,再看看生成日志文件内容。

跟我在log4net.config中配置的是一样的。
<layout type="log4net.Layout.PatternLayout">
<!--日志模板,这个东西很重要后续读取日志文件的时候就是依据这个配置-->
<conversionPattern value="%n时间:%date{yyyy-MM-dd HH:mm:ss},%n线程Id:%thread,%n日志级别:%-5level,%n描述:%message|%newline"/>
</layout>
然后需要读取日志文件的,把日志文件的内容转换成前端能够识别的数据。
public class ReadHelper
{
/// <summary>
/// https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8
/// 这里主要控制控制多个线程读取日志文件
/// </summary>
static ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim(); public static List<SysExceptionData> Read(string filePath="")
{
//日志对象集合
List<SysExceptionData> datas = new List<SysExceptionData>();
filePath = Directory.GetCurrentDirectory() + "\\logs\\system.log";
//判断日志文件是否存在
if (!File.Exists(filePath))
{
return datas;
}
_slimLock.EnterReadLock();
try
{ //获取日志文件流
var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
//读取内容
var reader = new StreamReader(fs);
var content = reader.ReadToEnd();
reader.Close();
fs.Close();
/*
*处理内容,换行符替换掉,然后在log4net配置文件中在每一写入日志结尾的地方加上 |
*这样做的好处是便于在读取日志文件的时候处理日志数据返回给客户端
*由于是在每一行结束的地方加上| 所有根据Split分割之后最后一个数据必然是空的
*所有Where去除一下。
*/
var contentList = content.Replace("\r\n", "").Split('|').Where(w => !string.IsNullOrEmpty(w));
foreach (var item in contentList)
{
//根据逗号分割单个日志数据的内容
var info = item.Split(',');
//实例化日志对象
SysExceptionData data = new SysExceptionData();
data.CreateTime = Convert.ToDateTime(info[0].Split(':')[1]);
data.Level = info[2].Split(':')[1];
data.Summary = info[3].Split(':')[1];
datas.Add(data);
}
}
finally
{
//退出
_slimLock.ExitReadLock();
}
return datas.OrderByDescending(bo=>bo.CreateTime).ToList();
}
}
public class SysExceptionData
{
/// <summary>
/// 时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 日志级别
/// </summary>
public string Level { get; set; }
/// <summary>
/// 日志描述
/// </summary>
public string Summary { get; set; }
}
这里需要说一下的是为什么要用ReaderWriterLockSlim,其实在写这篇博客之前我刚好看书学到这个东西。
来一段原文描述:
通常一个类型实例的并发读操作是线程安全的,而并发更新操作则不是。诸如文件这样的资源也具有相同的特点。
虽然可以简单的使用一个排它锁来保护对实例的任何形式的访问。
但是如果其读操作很多但是更新操作很少,则使用单一的锁限制并发性就不大合理了。
这种情况出现在业务应用服务器上,它会将常用的数据缓存在静态字段中进行快速检索。
ReaderWriterLockSlim是专门为这种情形设计的,它可以最大限度的保证锁的可用性。ReaderWriterLockSlim在.net3.5引入的它替代了笨重的ReaderWriterLock类。虽然两者功能相识,但是后者的执行速度比前置慢数倍。ReaderWriteLockSlim和ReaderWriterLock都拥有两种基本锁,读和写。
写锁是全局排它锁
读锁可以兼容其他的锁
因此,一个持有写锁的线程将阻塞其他任何试图获取读锁或写锁的京城。但是如果没有任何线程持有写锁的话,那么任意数量的线程都可以获得读锁。
ReaderWriterLockSlim和lock一样也有类似TryEnter之类的方法,来判断是否超时,如果超时就抛出错误(lock返回false)
这是关于ReaderWriterLockSlim官网最新的描述:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8
对了,我看的是孔雀鸟--《c# 7.0核心技术指南》c#想进阶强烈推荐这本书。
同时这部分代码也有参考老张的Blog.Core的源码,感谢!
接下来调试一下看看读取日志文件处理后的数据,我在TestController加了故意抛出错误的接口。

直接在浏览器输入 :http://localhost:13989/api/test/getLog

成功进入断点

shift+f9监听data看看数据

拿到这个数据,在客户端就直接可以用来展示,那么读取日志文件这部分就说完了,然后再说如何发送日志给客户端。
实时发送日志数据
在日志过滤器中有这样一段代码,玩过signalr的人都知道SendAsync的第一个字符串其实是集线器中方法(Hub)的名称,但是我们也是可以自定义它的名称的。
//发送给客户端
await _hub.Clients.All.SendAsync("ReceiveLog", data);
signalr强类型中心:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1#change-the-name-of-a-hub-method
之前用的Hub不是强类型中心,这次一并给他改造了。
/// <summary>
/// https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1
/// 强类型中心
/// </summary>
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task ReceiveMessage(object message);
Task ReceiveCaller(object message);
Task ReceiveLog(object data);
}
重构源码之前的方法。
public class ChatHub : Hub<IChatClient>
{
/// <summary>
/// 给所有客户端发送消息
/// </summary>
/// <param name="user">用户</param>
/// <param name="message">消息</param>
/// <returns></returns>
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
/// <summary>
/// 向调用客户端发送消息
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public async Task SendMessageCaller(string message)
{
await Clients.Caller.ReceiveCaller( message);
} /// <summary>
/// 客户端连接服务端
/// </summary>
/// <returns></returns>
public override Task OnConnectedAsync()
{
var id = Context.ConnectionId;
//_logger.Info($"客户端ConnectionId=>【{id}】已连接服务器!");
return base.OnConnectedAsync();
}
/// <summary>
/// 客户端断开连接
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override Task OnDisconnectedAsync(Exception exception)
{
var id = Context.ConnectionId;
//_logger.Info($"客户端ConnectionId=>【{id}】已断开服务器连接!");
return base.OnDisconnectedAsync(exception);
}
public async Task ReceiveLog(object data)
{
data = ReadHelper.Read();
await Clients.All.ReceiveLog(data);
}
}
ps:这个改动不会影响它在控制器注入,或者其它注入地方的使用。
其实服务端的配置差不多好了,现在需要想的是在客户端,首次进入页面的时候是应该手动给他调用一次发送日志,否则进入页面是没有数据的。
然后我在TestController中加上一个接口手动触发
[HttpGet]
public async Task<JsonResult> GetLogMessage()
{
var data = ReadHelper.Read();
await _hubContext.Clients.All.SendAsync("ReceiveLog", data);
return new JsonResult(0);
}
,接下来需要把注意力集中到客户端上了,
之前的两篇博客我是没有安装element-ui的,这一次我为了展示数据省事,就打算直接用element-table展示数据好了。
element官网:https://element.eleme.cn/#/zh-CN/component/installation
npm i element-ui -S
在mian.js添加配置
//element
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
vue 这里我不敢乱讲,这个我也不是很会,所以直接放代码了,我把客户端直接的代码进行了一下改造,加了个菜单,然后之前的内容都放在不同的菜单。
<template>
<div class="home">
<h1>服务端错误日志返回</h1>
<button @click="sendErr">执行一个错误</button>
<div class="table">
<el-table :data="tableData" border style="width: 100%">
<el-table-column type="index" label="序号" width="100"></el-table-column>
<el-table-column prop="createTime" label="日期" width="180"></el-table-column>
<el-table-column prop="level" label="级别" width="100"></el-table-column>
<el-table-column prop="summary" label="描述" width="300"></el-table-column>
</el-table>
</div>
</div>
</template> <script>
// @ is an alias to /src
import HelloWorld from "@/components/HelloWorld.vue";
import * as signalR from "@aspnet/signalr";
export default {
name: "Home",
components: {
HelloWorld,
},
data() {
return {
message: "", //消息
connection: "", //signalr连接
messages: [], //返回消息
tableData: [],
};
},
methods: {
//发出一个错误
sendErr: function () {
this.$http.get("http://localhost:13989/api/test/getLog").then((resp) => {
//console.log(resp);
});
},
//获取系统日志
getLog: function () {
this.$http
.get("http://localhost:13989/api/test/GetLogMessage")
.then((res) => {
console.log(res);
});
},
getdatalist: function () {
this.$http
.get("http://localhost:13989/api/test/GetLogMessage")
.then((res) => {
// console.log(res);
//this.tableData = res.data;
})
.catch((err) => {
console.log(err);
});
},
},
computed: {},
mounted: function () {
let thisVue = this;
this.connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:13989/chathub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.configureLogging(signalR.LogLevel.Information)
.build(); this.connection.start();
//连接日志发送事件 this.connection.on("ReceiveLog", function (message) {
console.log("listening receivelog");
thisVue.tableData = message;
}); //初始化表格数据
thisVue.getdatalist();
},
};
</script>
<style scoped>
.table {
margin: 20px;
}
</style>
启动看看效果。
这是日志接口展示的客户端页面

之前博客的内容在聊天中。。

来个gif看看效果

结语
今天的分享到这里就结束了,内心觉得写一篇博客真不容易,从这个想法的萌芽到写demo去实现大概花了一周,不断地去看资料,研究源码。
俗话说,人不逼自己一下,不知道有多少潜力。
最后希望博客能够帮助到需要的人,后续还想研究下signalr 配置jwt,redis,sqlserver等。
Dome源码地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo
学习使我快乐!!!

Asp.Net Core SignalR 系列博客的更多相关文章
- 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇
==== 目录 ==== 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之二 —— ...
- Django 系列博客(十四)
Django 系列博客(十四) 前言 本篇博客介绍在 html 中使用 ajax 与后台进行数据交互. 什么是 ajax ajax(Asynchronous Javascript And XML)翻译 ...
- Django 系列博客(十三)
Django 系列博客(十三) 前言 本篇博客介绍 Django 中的常用字段和参数. ORM 字段 AutoField int 自增列,必须填入参数 primary_key=True.当 model ...
- Django 系列博客(十)
Django 系列博客(十) 前言 本篇博客介绍在 Django 中如何对数据库进行增删查改,主要为对单表进行操作. ORM简介 查询数据层次图解:如果操作 mysql,ORM 是在 pymysql ...
- Django 系列博客(七)
Django 系列博客(七) 前言 本篇博客介绍 Django 中的视图层中的相关参数,HttpRequest 对象.HttpResponse 对象.JsonResponse,以及视图层的两种响应方式 ...
- Django 系列博客(三)
Django 系列博客(三) 前言 本篇博客介绍 django 的前后端交互及如何处理 get 请求和 post 请求. get 请求 get请求是单纯的请求一个页面资源,一般不建议进行账号信息的传输 ...
- 窥探Swift系列博客说明及其Swift版本间更新
Swift到目前为止仍在更新,每次更新都会推陈出新,一些Swift旧版本中的东西在新Swift中并不适用,而且新版本的Swift会添加新的功能.到目前为止,Swift为2.1版本.去年翻译的Swift ...
- Flutter 即学即用系列博客——05 StatelessWidget vs StatefulWidget
前言 上一篇我们对 Flutter UI 有了一个基本的了解. 这一篇我们通过自定义 Widget 来了解下如何写一个 Widget? 然而 Widget 有两个,StatelessWidget 和 ...
- Flutter 即学即用系列博客——04 Flutter UI 初窥
前面三篇可以算是一个小小的里程碑. 主要是介绍了 Flutter 环境的搭建.如何创建 Flutter 项目以及如何在旧有 Android 项目引入 Flutter. 这一篇我们来学习下 Flutte ...
随机推荐
- cni-ipam-etcd demo
链接:https://github.com/jeremyxu2010/cni-ipam-etcd 测试demo: package main import ( "fmt" " ...
- Shell编程—基础脚本
1. 使用多个命令 如果要两个命令或者多个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开. 2. 创建 shell 脚本文件 例如: #!/bin/bash # This script dis ...
- 洛谷 P3951 NOIP 2017 小凯的疑惑
洛谷 P3951 NOIP 2017 小凯的疑惑 题目描述 小凯手中有两种面值的金币,两种面值均为正整数且彼此互素.每种金币小凯都有 无数个.在不找零的情况下,仅凭这两种金币,有些物品他是无法准确支付 ...
- Spring boot程序的部署及运行
将 spring boot 应用程序打包成 jar 包 我们使用 spring boot 的 maven 插件来构建管理整个应用程序,使用 mvn package 将应用程序打包成一个 jar 包 将 ...
- 小程序商城系统CRMEB Pro v1.1全新重构,新增DIY功能
CRMEB ProV1.1全新升级发布,真正实现了后台可自由拖拽组合实现首页布局的DIY功能,这一功能的实现,将告别过去千篇一律的同质化界面布局,真正实现个性化.高自由的随心组合.本次发布的版本中我们 ...
- const定义的对象属性是否可以改变------是!
用const声明person对象,给age重新赋值是没问题的 但是重新给person赋值是不可以的 这里需要了解'基本数据类型'和'引用数据类型' 基本数据类型:string, number, boo ...
- JDK15就要来了,你却还不知道JDK8的新特性!
微信搜「烟雨星空」,白嫖更多好文. 现在 Oracle 官方每隔半年就会出一个 JDK 新版本.按时间来算的话,这个月就要出 JDK15 了.然而,大部分公司还是在使用 JDK7 和 8 . 之前去我 ...
- 自定义注解-方法重试@RetryProcess
背景 在项目开发中,有时候会出现接口调用失败,本身调用又是异步的,如果是因为一些网络问题请求超时,总想可以重试几次把任务处理掉. 一些RPC框架,比如dubbo都是有重试机制的,但是并不是每一个项目多 ...
- 使用jackson解析json串得到树模型,然后遍历树模型获得需要的数据
Problem:从网址 http://quotes.money.163.com/hs/service/marketradar_ajax.php?host=http%3A%2F%2Fquotes.mon ...
- GuestOS? HostOS?
起因 今天在网上看到一篇文章 有几个陌生的关键词不太熟悉,就随笔记一下. 名词解释 # OS :操作系统 # VM(虚拟机) 里的OS 称为 GuestOS # 物理机 ...