系列

源码地址: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. Open MPI 4.0 编译安装

    电脑上目前使用的mpi环境是2.1.1版本的openmpi,是我之前直接使用系统的包管理工具安装的.但是系统包版本一般都比较老旧,现在openmpi最新版已经出到了4.0,即将出4.1了,所以我打算升 ...

  2. Ubuntu 统计文件夹下文件个数的命令

    查看当前目录下的文件数量(不包含子目录中的文件) ls -l|grep "^-"| wc -l 查看当前目录下的文件数量(包含子目录中的文件) 注意:R,代表子目录 ls -lR| ...

  3. HTML5移动开发之路(1)——jqMobi中Side Menu实现(类似人人网)

    记得以前在做Native App的时候类似于人人网侧边滑动的效果非常的热,很多app仿照该效果进行开发,在jqMobi中也有类似的效果被称为Side Menu.下面我们来一步一步实现该效果. 首先新建 ...

  4. 记录一次CDH集群邮件报警功能的设置

    1.通用的配置CDH邮件报警设置 进入cloudera manager service页面,选择配置 左侧菜单Alert Publisher 勾选[启用电子邮件警报] 邮件服务协议smtp,如果使用s ...

  5. C语言复习-字符串与指针

    C语言复习-字符串与指针 例一: [字符串处理 去除C代码中的注释] C/C++代码中有两种注释,/* */和//.编译器编译预处理时会先移除注释.就是把/*和*/之间的部分去掉,把//以及之后的部分 ...

  6. JS获取时间(当前-过去-未来)

    /** * 获取时间格式为:1970-01-01 00:00 * @param {参数} params * 属性 类型 默认值 必填 说明 * date Date new Date() 否 Date对 ...

  7. MES系统与喷涂设备软件基于文本文件的数据对接方案

    产品在生产过程中除了记录产品本身的一些数据信息,往往还需要记录下生产设备的一些参数和状态,这也是MES系统的一个重要功能.客户的药物支架产品,需要用到微量药物喷涂设备,客户需要MES系统能完整记录下每 ...

  8. android开发之edittext弹出输入框遮挡住文字。解决方法

    在ManiFest清单文件中修改被遮挡的类的EditText android:windowSoftInputMode="adjustPan|stateHidden"

  9. cookies、sessionStorage和localStorage

    浏览器的缓存机制提供了可以将用户数据存储在客户端上的方式,可以利用cookie,session等跟服务端进行数据交互.浏览器查看方式:  HTML4的本地存储 cookie 一.cookie和sess ...

  10. rpc之负载均衡

    使用集群,比如zk来控制注册中心,当一个服务有多个请求地址的时候,会返回多个地址. 那么就需要负载均衡来控制我们要请求哪台机器来得到请求. 方案一:随机 传入key值和key所包含的ip地址值,该地址 ...