系列

源码地址: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 系列博客的更多相关文章

  1. 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇

    ==== 目录 ==== 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之二 —— ...

  2. Django 系列博客(十四)

    Django 系列博客(十四) 前言 本篇博客介绍在 html 中使用 ajax 与后台进行数据交互. 什么是 ajax ajax(Asynchronous Javascript And XML)翻译 ...

  3. Django 系列博客(十三)

    Django 系列博客(十三) 前言 本篇博客介绍 Django 中的常用字段和参数. ORM 字段 AutoField int 自增列,必须填入参数 primary_key=True.当 model ...

  4. Django 系列博客(十)

    Django 系列博客(十) 前言 本篇博客介绍在 Django 中如何对数据库进行增删查改,主要为对单表进行操作. ORM简介 查询数据层次图解:如果操作 mysql,ORM 是在 pymysql ...

  5. Django 系列博客(七)

    Django 系列博客(七) 前言 本篇博客介绍 Django 中的视图层中的相关参数,HttpRequest 对象.HttpResponse 对象.JsonResponse,以及视图层的两种响应方式 ...

  6. Django 系列博客(三)

    Django 系列博客(三) 前言 本篇博客介绍 django 的前后端交互及如何处理 get 请求和 post 请求. get 请求 get请求是单纯的请求一个页面资源,一般不建议进行账号信息的传输 ...

  7. 窥探Swift系列博客说明及其Swift版本间更新

    Swift到目前为止仍在更新,每次更新都会推陈出新,一些Swift旧版本中的东西在新Swift中并不适用,而且新版本的Swift会添加新的功能.到目前为止,Swift为2.1版本.去年翻译的Swift ...

  8. Flutter 即学即用系列博客——05 StatelessWidget vs StatefulWidget

    前言 上一篇我们对 Flutter UI 有了一个基本的了解. 这一篇我们通过自定义 Widget 来了解下如何写一个 Widget? 然而 Widget 有两个,StatelessWidget 和 ...

  9. Flutter 即学即用系列博客——04 Flutter UI 初窥

    前面三篇可以算是一个小小的里程碑. 主要是介绍了 Flutter 环境的搭建.如何创建 Flutter 项目以及如何在旧有 Android 项目引入 Flutter. 这一篇我们来学习下 Flutte ...

随机推荐

  1. Spark on Yarn运行时加载的jar包

    spark on yarn运行时会加载的jar包有如下: spark-submit中指定的--jars $SPARK_HOME/jars下的jar包 yarn提供的jar包 spark-submit通 ...

  2. 如何以正确的姿势安装Vue的依赖并且启动下载好的项目

    首先,输入cd进入项目所在的目录. 然后输入   npm install --registry=https://registry.npm.taobao.org    // --后面表示使用淘宝镜像,下 ...

  3. 第5篇 Scrum 冲刺博客

    1.站立会议 照骗 进度 成员 昨日完成任务 今日计划任务 遇到的困难 钟智锋 完成技能 完全重构游戏逻辑代码,并编写调试模块 队友的代码已经和想法相去甚远 庄诗楷 制作了开始游戏的界面 进行了相关的 ...

  4. Latex — 写作编译过程中遇到问题记录与总结

    最近在训练的时候,又开始用Latex进行写作.碰到了很多问题,将问题进行记录与总结. 一.输出中文的问题 由于写作的时候用的是中文,而之前用的是英文,故碰到的第一个问题就是中文的问题.我之前下的是Wi ...

  5. Python语言中的关键字(自己做的读书笔记)

    电脑配置:联想笔记本电脑 windows8系统 Python版本:2.7.8 本文章撰写时间:2015.1.1 作者:陈东陈 阅读说明: 1.本文都是先解释,后放图片: 2.文中斜体部分要么为需要输入 ...

  6. 从零开始的SpringBoot项目 ( 二 ) 使用IDEA创建一个SpringBoot项目

    工欲善其事 , 必先利其器 . IntelliJ IDEA 2019.3.3 x64的安装与破解 下面详细说明下如何使用idea创建我们的第一个springboot项目: 首先打开idea主界面选择 ...

  7. 揭秘!containerd 镜像文件丢失问题,竟是镜像生成惹得祸

    导语 作者李志宇,腾讯云后台开发工程师,日常负责集群节点和运行时相关的工作,熟悉 containerd.docker.runc 等运行时组件.近期在为某位客户提供技术支持过程中,遇到了 contain ...

  8. Java 获取一段时间内的每一天

    有时候我们会遇到一些业务场景,需要去获取一段时间内的每一天日期 public static List<Date> findDates(Date dBegin, Date dEnd) { L ...

  9. HDU - 1019-Least Common Multiple(求最小公倍数(gcd))

    The least common multiple (LCM) of a set of positive integers is the smallest positive integer which ...

  10. 使用kubernetes-event-exporter将k8s的事件导出到elasticsearch日志系统中

    使用kubernetes-event-exporter将k8s的事件导出到elasticsearch日志系统中 前提 版本 kubernetes v1.17.9 kubernetes-event-ex ...