系列

源码地址: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. MPI组操作

    进程组的创建 MPI_Comm_Group int MPI_Comm_group( MPI_Comm comm, MPI_Group *group ); 把相同的通信子进程放到一个组内. #inclu ...

  2. Android开发之设置应用设置全屏的两种解决方法 兼容android5.0等两种解决方法

    在开发中我们经常需要把我们的应用设置为全屏,有两种方法,一中是在代码中设置,另一种方法是在配置文件里改! 一.在代码中设置:  代码如下: package com.android.tutor; imp ...

  3. facebookPixel代码安装详解

    最近接到一个需求,优化独立站的facebookPixel代码,完成后对这个项目进行复盘.首先要介绍facebookPixel的理论知识. Facebook像素是一段JavaScript代码,其中加载了 ...

  4. 高可用集群corosync+pacemaker之crmsh使用(二)

    上一篇博客我们聊到了crmsh的安装以及配置一个资源到corosync+pacemaker高可用集群上的相关命令的用法,回顾请参考https://www.cnblogs.com/qiuhom-1874 ...

  5. ShaderLab 枚举常量

        public enum ZTest     {         Always = 0,         Less = 2,         Equal = 3,         LEqual  ...

  6. js 基础面试

    1.['1', '2', '3'].map(parseInt) let arr = ['1', '2', '3'] let res1 = arr.map(Number) // [ 1, 2, 3 ] ...

  7. 02 axios

    request.js import axios from 'axios' const config = require('@/config') const instance = axios.creat ...

  8. golang 内置print/println、fmt、log的区别

    fmt.Println与fmt.Print区别 换行区别不用多说,另外一个区别在于fmt.Print只有在参数间都不是字符串时才会产生间隔 fmt与log的重要区别 fmt没有做同步处理 fmt标准输 ...

  9. C003:计算球体体积 自行输入球体半径

    程序: #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float sphereRadius; do{ pri ...

  10. basicInterpreter1.01 支持分支语句

    源码:https://files.cnblogs.com/files/heyang78/basicInterpreter-20200531-1.rar 输入: count= print(count) ...