当前位置: 首页 > news >正文

SignalR注册成Windows后台服务,并实现web前端断线重连

注意下文里面的 SignalR 不是 Core 版本,而是 Framework 下的

本文使用的方式是把 SignalR 写在控制台项目里,再用 Topshelf 注册成 Windows 服务

这样做有两点好处

  1. 传统 Window 服务项目调试时需要“附加到进程”,开发体验比较差,影响效率
  2. 使用控制台不仅可以随时打断点调试,还可以随时打印调试信息,非常方便

Topshelf 的使用方法这里不再阐述,在控制台里使用 Topshelf 三个步骤 :

  1. 定义一个 Owin 自托管作为 SignalR的宿主,里面设置允许跨域,起名为 Startup 
    using Microsoft.AspNet.SignalR;
    using Microsoft.Owin.Cors;
    using Owin;
    using System;
    using System.Diagnostics;namespace HenryMes.SignalR.Hosting
    {/// <summary>/// 配置跨域请求、SignalR Server/// </summary>class Startup{public void Configuration(IAppBuilder app){//app.UseErrorPage();app.UseCors(CorsOptions.AllowAll);// 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888//Hub Modeapp.MapSignalR("/lcc", new HubConfiguration());app.Map("/signalr", map =>{var config = new HubConfiguration{// You can enable JSONP by uncommenting this line// JSONP requests are insecure but some older browsers (and some// versions of IE) require JSONP to work cross domainEnableJSONP = true};//config.EnableCrossDomain = true;// Turns cors support on allowing everything// In real applications, the origins should be locked downmap.UseCors(CorsOptions.AllowAll).RunSignalR(config);});Make long polling connections wait a maximum of 110 seconds for aresponse. When that time expires, trigger a timeout command andmake the client reconnect.//GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(110);Wait a maximum of 30 seconds after a transport connection is lostbefore raising the Disconnected event to terminate the SignalR connection.//GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(30);For transports other than long polling, send a keepalive packet every10 seconds. This value must be no more than 1/3 of the DisconnectTimeout value.//GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(10);// Turn tracing on programmaticallyGlobalHost.TraceManager.Switch.Level = SourceLevels.Information;}}
    }
    

  2. 建立可在服务里运行的服务类,使用了上面的Startup配置实例化宿主对象,里面定义了服务的启动,暂停,关闭等触发时的一些动作,本文就建立一个 JobManager 类来完成这些工作 
    using HenryMes.Utils;
    using Microsoft.Owin.Hosting;
    using System;
    using System.Threading.Tasks;namespace HenryMes.SignalR.Hosting
    {public class JobManager{private const string displayName = "SignalR 状态监控";IDisposable SignalR { get; set; }public bool Start(){try{//signalr server地址,端口可以更换,确保不被占用,否则服务启动不了
    #if DEBUGvar url = $"http://{JsonConfig.Instance.Root()?.Debug?.Ip}:{JsonConfig.Instance.Root()?.Debug?.Port}";var Port = $"{JsonConfig.Instance.Root()?.Debug?.Port}";
    #elsevar url = $"http://{JsonConfig.Instance.Root()?.Release?.Ip}:{JsonConfig.Instance.Root()?.Release?.Port}";var Port = $"{JsonConfig.Instance.Root()?.Release?.Port}";
    #endifStartOptions options = new StartOptions();options.Urls.Add(url);options.Urls.Add($"http://+:{Port}");//此处需要用一个全局变量来保存WebApp,否则在发布为后台服务的时候生命周期会提前结束,被系统回收掉SignalR = WebApp.Start<Startup>(options);Task.Delay(TimeSpan.FromSeconds(1)).Wait();Console.WriteLine("Server running on {0}", url);Console.WriteLine($"{displayName}服务开始");Console.ReadLine();LogHelper.GetInstance().Information($"{displayName}服务开始,地址 {url}");return true;}catch (Exception ex){LogHelper.GetInstance().Error(ex);}return false;}public bool Stop(){SignalR.Dispose();LogHelper.GetInstance().Information($"{displayName}服务停止");System.Threading.Thread.Sleep(1500);return true;}public bool Shutdown(){SignalR.Dispose();LogHelper.GetInstance().Information($"{displayName}服务停止");System.Threading.Thread.Sleep(1500);return true;}}
    }
    
  3. 在 Program.cs 文件,也就是入口函数 main 调用 Topshelf 对服务进行配置
using Topshelf;namespace HenryMes.SignalR.Hosting
{internal class Program{private const string displayName = "HenryMes.SignalR.Hosting";static void Main(string[] args){HostFactory.Run(x => {x.Service<JobManager>(s =>{s.ConstructUsing(name => new JobManager());s.WhenStarted(tc => tc.Start());s.WhenShutdown(tc => tc.Shutdown());s.WhenStopped(tc => tc.Stop());});x.RunAsLocalSystem();x.StartAutomatically();x.SetDescription(displayName);x.SetDisplayName(displayName);x.SetServiceName(displayName);});}}
}

下面定义一个 SignalR 的 Hub 基类,里面管理了SignalR 的连接和断开,一个线程管理一个连接,连接断开,线程自动取消,建立一个抽象类 BaseHub

using HenryMes.Utils;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;namespace HenryMes.SignalR.Hosting
{/// <summary>/// /// </summary>[HubName(nameof(T))]public abstract class BaseHub<T> : Hub where T : IHub{/// <summary>/// 线程安全版本的字典,管理SignalR的连接/// </summary>protected static readonly ConcurrentDictionary<string, CancellationTokenSource> Connections =new ConcurrentDictionary<string, CancellationTokenSource>();/// <summary>/// 异步锁/// </summary>public static readonly object AsyncObj = new object();/// <summary>/// 设置超时时间/// </summary>abstract protected int millisecondsTimeout { get; }/// <summary>/// 设置线程轮询时间/// </summary>abstract protected int intervalTime { get; }/// <summary>/// 跑单个Task/// </summary>abstract protected Func<object> RunMethod { get; }/// <summary>/// 跑多个Task, 返回是否超时/// </summary>abstract protected Func<CancellationTokenSource, (bool, object)> RunMultiTaskMethod { get; }/// <summary>/// 是否跑多任务/// </summary>abstract protected bool runMultiTask { get; }//当客户端与服务器建立连接后执行的方法public override Task OnConnected(){//获取客户端IDConsole.WriteLine("{0}已连接", Context.ConnectionId);LogHelper.GetInstance().Information($"服务端与客户端:【{typeof(T).Name}】{Context.ConnectionId} 成功建立连接!");return base.OnConnected();}public override Task OnReconnected(){Console.WriteLine("{0}已重连", Context.ConnectionId);LogHelper.GetInstance().Information($"服务端与客户端:【{typeof(T).Name}】{Context.ConnectionId} 已重连!");Send(Context.ConnectionId);return base.OnReconnected();}/// <summary>/// 所有任务执行完是否超时/// </summary>/// <param name="tokenSource"></param>/// <param name="allTasks"></param>/// <returns></returns>public bool IsCompletedAllTasks(CancellationTokenSource tokenSource, Task[] allTasks){try{return Task.WaitAll(allTasks, millisecondsTimeout, tokenSource.Token);}catch (AggregateException ex){LogHelper.GetInstance().Error($"系统错误:{this.GetType().Name},{ex.Flatten().InnerException.Message}");tokenSource.Cancel();}return false;}/// <summary>/// 向客户端发送消息/// </summary>/// <param name="connectId"></param>public void Send(string connectId){lock (AsyncObj){var tokenSource = new CancellationTokenSource();Connections.TryAdd(connectId, tokenSource);Task.Run(() =>{while (!tokenSource.Token.IsCancellationRequested){try{// 是否是多任务if (runMultiTask == false){var result = RunMethod();var message = $"【{typeof(T).Name}】 {connectId} 正在回传数据!";LogHelper.GetInstance().Information(message);// 把组装好的数据推送到前端BaseNotifer<T>.Refresh(connectId, JsonConvert.SerializeObject(result));tokenSource.Token.WaitHandle.WaitOne(intervalTime);}else{// 是否超时var (isCompleted, result) = RunMultiTaskMethod(tokenSource);if (isCompleted){var message = $"【{typeof(T).Name}】 {connectId} 正在回传数据!";LogHelper.GetInstance().Information(message);// 把组装好的数据推送到前端BaseNotifer<T>.Refresh(connectId, JsonConvert.SerializeObject(result));// 下一次推送等待N秒后进行tokenSource.Token.WaitHandle.WaitOne(intervalTime);}else{// 等待超时tokenSource.Cancel();// 打印超时错误日志LogHelper.GetInstance().Error($@"{this.GetType().Name} 推送超时! 当前超时时间设置为{millisecondsTimeout}毫秒!");// 重新执行Connections.TryRemove(connectId, out tokenSource);Send(connectId);}}}catch(AggregateException ex){LogHelper.GetInstance().Error($"系统错误:{this.GetType().Name},{ex.Flatten().InnerException.Message}");tokenSource.Token.WaitHandle.WaitOne(intervalTime);}}}, tokenSource.Token);}}/// <summary>/// 连接断开事件/// </summary>/// <param name="stopCalled"></param>/// <returns></returns>public override Task OnDisconnected(bool stopCalled){lock (AsyncObj){try{var tokenSource = Connections[Context.ConnectionId];Connections.TryRemove(Context.ConnectionId, out tokenSource);tokenSource.Cancel();LogHelper.GetInstance().Information($"服务端与客户端:【{typeof(T).Name}】{Context.ConnectionId} 连接已断开!");}catch (Exception ex){if (Connections.ContainsKey(Context.ConnectionId)){var tokenSource = Connections[Context.ConnectionId];Connections.TryRemove(Context.ConnectionId, out tokenSource);}// 打印错误日志LogHelper.GetInstance().Error($@"{this.GetType().Name} 已断开! {ex.Message}!");}}return base.OnDisconnected(stopCalled);}}
}

以一个具体的 Hub 为例,继承上面的 BaseHub, 建立一个 具体实现的 Hub 名为 OperationKanBanHub ,使用 RunMultiTaskMethod 并行执行一些任务,这是项目里的一个真实案例,不必关心细节

using HenryMes.Entitys;
using HenryMes.WebApi.Controllers;
using HenryMes.WebApi.Controllers.Other;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;namespace HenryMes.SignalR.Hosting.Hubs
{/// <summary>/// 运营看板/// </summary>public class OperationKanBanHub : BaseHub<OperationKanBanHub>{/// <summary>/// 设置超时时间/// </summary>protected override int millisecondsTimeout => 10000;/// <summary>/// 设置线程轮询时间/// </summary>protected override int intervalTime => 5000;/// <summary>/// 是否跑多任务/// </summary>protected override bool runMultiTask => true;/// <summary>/// 跑单个Task/// </summary>protected override Func<object> RunMethod => throw new NotImplementedException();/// <summary>/// 跑多个Task/// </summary>protected override Func<CancellationTokenSource, (bool, object)> RunMultiTaskMethod => (TokenSource) =>{#region Task取数// 菜籽收购var taskSum4Rapeseed = Task.Run(() =>{KanbanController controller = new KanbanController();dynamic data = controller.ControlCenter_Center_Sum4Rapeseed();return data.Content;});// 油品生产,销售var taskSum4Oil = Task.Run(() =>{KanbanController controller = new KanbanController();dynamic data = controller.ControlCenter_Center_Sum4Oil();return data.Content;});// 库存量前10的存货var taskSum4Top = Task.Run(() =>{KanbanController controller = new KanbanController();dynamic data = controller.ControlCenter_Center_Sum4Top();return data.Content;});// 罐区存油,读取 mongodbvar taskTankOilQuantity = Task.Run(() =>{return new List<dynamic>{new { tank = "Tank1001",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1002",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1003",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1004",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1005",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1006",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1007",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1008",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1009",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1010",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1011",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1012",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1013",temperature = "21.4℃",pressure = "21PA", quantity = "100" },new { tank = "Tank1014",temperature = "21.4℃",pressure = "21PA", quantity = "100" },};});// 近一年产出销售 1-12月var taskSaleDispatch4Month = Task.Run(() =>{KanbanController controller = new KanbanController();dynamic data = controller.ControlCenter_Center_SaleDispatch4Month();return data.Content;});// 最近10条采购信息var taskPreInStore4Lately = Task.Run(() =>{KanbanController controller = new KanbanController();dynamic data = controller.ControlCenter_Center_PreInStore4Lately();return data.Content;});// 最近10条生产计划var taskProductionTask = Task.Run(() =>{ProductionTaskController controller = new ProductionTaskController();dynamic data = controller.QueryTake(new SqlSugarPageRequest{PageIndex = 1,PageSize = 10,Filter = new List<SqlSugar.ConditionalModel>()});return data.Content;});#endregion#region 同步阻塞等待所有Task执行完// 所有线程任务是否完成 默认falsevar isCompleted = IsCompletedAllTasks(TokenSource, new Task[] {taskSum4Rapeseed,taskSum4Oil,taskSum4Top,taskTankOilQuantity,taskSaleDispatch4Month,taskPreInStore4Lately,taskProductionTask});#endregionif (isCompleted){#region 所有Task已完成// 菜籽var RapeseedResult = taskSum4Rapeseed.Result;var Rapeseed = new{GYS = new{PurchaseReceiveQuantity = RapeseedResult?.Data?.Rapeseed_GYS?.PurchaseReceiveQuantity,BalanceQuantity = RapeseedResult?.Data?.Rapeseed_GYS?.BalanceQuantity,},SD = new{PurchaseReceiveQuantity = RapeseedResult?.Data?.Rapeseed_SD?.PurchaseReceiveQuantity,BalanceQuantity = RapeseedResult?.Data?.Rapeseed_SD?.BalanceQuantity,}};// 油品生产,销售dynamic Sum4OilResult = taskSum4Oil.Result;var Sum4Oil = new{// 产出成品油TankOilQuantity = Sum4OilResult?.Data?.TankOil?.ProductReceiveQuantity,// 产出包装油PackageOilQuantity = Sum4OilResult?.Data?.PackageOil?.ProductReceiveQuantity,// 销售包装油SaleOilQuantity = Sum4OilResult?.Data?.PackageOil?.SaleDispatchQuantity};// 存货中库存量前10的存货var Sum4TopResult = taskSum4Top.Result;var Sum4Top = new{Sum4TopResult?.Data?.DataSource};// 罐区存油var TankOilQuantity = taskTankOilQuantity.Result;// 近一年产出销售 1-12月var TaskSaleDispatch4MonthResult = taskSaleDispatch4Month.Result;var SaleDispatch4Month = new{Sale = TaskSaleDispatch4MonthResult?.Data?.SaleDispatch.Details,Product = TaskSaleDispatch4MonthResult?.Data?.ProductReceive.Details};// 最近10条采购信息var TaskPreInStore4LatelyResult = taskPreInStore4Lately.Result;var PreInStore4Lately = TaskPreInStore4LatelyResult?.Data?.DataSource;// 最近10条生产计划var taskProductionTaskResult = taskProductionTask.Result;var ProductionTask = taskProductionTaskResult?.Data;return (isCompleted, new{Rapeseed,Sum4Oil,Sum4Top,TankOilQuantity,SaleDispatch4Month,PreInStore4Lately,ProductionTask});#endregion}return (isCompleted, new { });};}
}

 此时 SignalR 的后台推送基本就完成了,再来就是web前端的接收推送和断线下的自动重新连接(比如说后台服务程序做了更新,此时需要关闭服务再启动服务,这个时候要求web端不断尝试重新连接,直到后台服务启动并重新连接上为止)

前端使用 Vue 2.0 + jQuery.signalR 2.4.2 , 只列一下关键代码

import $ from "jquery";
import "signalr";
import echarts from "../../pages/kanban/OperationKanBanEcharts.vue";
export default {components: { echarts },data() {return {connection: null,proxy: null,// 是否需要断线重连的标记,当页面关闭时是不需要继续推送的tryReconnect : true}},methods: {// 从SignalR推送过来的数据,刷新看板refreshKanban(message) {// 刷新时间this.getDateTime()let obj = JSON.parse(message)// 省略无关代码......},},mounted() {this.$nextTick(() => {this.connection = $.hubConnection(process.env.SignalR);// 定义服务器端SignalR推送过来的消息接收代理this.proxy = this.connection.createHubProxy("OperationKanBanHub");this.proxy.on("Refresh", (message) => {console.log(`接收到来自服务端 ${this.connection.id} 的数据!`)this.refreshKanban(message)});// 创建连接到服务器端SignalR的连接this.connection.start().done(() => {// 客户端发送信息到服务器this.proxy.invoke("Send", this.connection.id);}).fail((err) => {console.log(err);});this.connection.disconnected(() => {if(this.tryReconnect) {setTimeout(() => {console.log('连接已断开,正尝试重新连接!')this.connection.start().done(() => {this.proxy.invoke("Send", this.connection.id); // 客户端发送信息到服务器}).fail((err) => {console.log(err);});}, 5000); // Restart connection after 5 seconds.}});});},deactivated() {if (this.connection) {// 关闭SignalR连接this.tryReconnect = falsethis.connection.stop();// 清除缓存this.$vnode.parent.componentInstance.cache = {};this.$vnode.parent.componentInstance.keys = [];}},
};

最后的一个步骤,怎么把后台的控制台SignalR宿主程序安装成 Windows 服务?在项目里建立两个批处理文件,Install.bat 安装服务,UnInstall.bat 卸载服务,点击右键点文件属性,把他们的编码改为 ansi(不要问我为什么......因为不改的话,打开批处理命令窗口的时候中文会显示成乱码)

 Install.bat

@echo onrem 设置DOS窗口的背景颜色及字体颜色
color 2frem 设置DOS窗口大小 
mode con: cols=80 lines=25@echo off
echo 请按任意键开始安装 HenryMes.SignalR.Hosting 服务rem 以管理员身份运行
%1 mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c %~s0 ::","","runas",1)(window.close)&&exit
:Adminrem 输出空行
echo.
pausecd /d %~dp0
HenryMes.SignalR.Hosting install --autostart start
net start HenryMes.SignalR.Hostingpause

UnInstall.bat

@echo onrem 设置DOS窗口的背景颜色及字体颜色
color 2frem 设置DOS窗口大小 
mode con: cols=80 lines=25@echo off
echo 请按任意键开始卸载 HenryMes.SignalR.Hosting 服务rem 以管理员身份运行
%1 mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c %~s0 ::","","runas",1)(window.close)&&exit
:Adminrem 输出空行
echo.
pausecd /d %~dp0
net stop HenryMes.SignalR.Hosting
HenryMes.SignalR.Hosting uninstallpause

相关文章:

SignalR注册成Windows后台服务,并实现web前端断线重连

注意下文里面的 SignalR 不是 Core 版本&#xff0c;而是 Framework 下的 本文使用的方式是把 SignalR 写在控制台项目里&#xff0c;再用 Topshelf 注册成 Windows 服务 这样做有两点好处 传统 Window 服务项目调试时需要“附加到进程”&#xff0c;开发体验比较差&#xf…...

【前端笔试题二】从一个指定数组中,每次随机取一个数,且不能与上次取数相同,即避免相邻取数重复

前言 本篇文章记录下我在笔试过程中遇到的真实题目&#xff0c;供大家参考。 1、题目 系统给定一个数组&#xff0c;需要我们编写一个函数&#xff0c;该函数每次调用&#xff0c;随机从该数组中获取一个数&#xff0c;且不能与上一次的取数相同。 2、思路解析 数组已经有了…...

专栏关注学习

Node学习专栏&#xff08;全网最细的教程&#xff09; 【spring系列】 SpringCloud 前端框架Vue java学习过程 RocketMQ Spring Tomcat websocket 从头开始学Redisson 从头开始学Oracle 跟着大宇学Shiro 吃透Shiro源代码 Git基础与进阶 Java并发编程 Spring系列 手写…...

【手写 Vuex 源码】第八篇 - Vuex 的 State 状态安装

一&#xff0c;前言 上一篇&#xff0c;主要介绍了 Vuex 模块安装的实现&#xff0c;针对 action、mutation、getter 的收集与处理&#xff0c;主要涉及以下几个点&#xff1a; Vuex 模块安装的逻辑&#xff1b;Vuex 代码优化&#xff1b;Vuex 模块安装的实现&#xff1b;Vue…...

Mac下拉式终端的安装与配置 (iTerm2)

Mac下拉式终端的安装与配置 使用效果如图所示 安装前置软件 iTerm2 很可惜&#xff0c;如此炫酷的功能在原终端中并不能实现&#xff0c;我们需要借助iTerm2这个软件来实现。 官网链接&#xff1a;iTerm2 - macOS Terminal Replacement 我们点击download下载即可 配置 当我…...

使用 Spring 框架结合阿里云 OSS 实现文件上传的代码示例

使用 Spring 框架结合阿里云 OSS 实现文件上传的代码示例POM文件配置文件上传工具类控制层使用yaml配置文件&#xff08;第二种用法&#xff0c;看公司要求&#xff09;注入 OSSClient 对象及工具类&#xff08;第二种用法&#xff0c;看公司要求&#xff09;使用 Vue 前端代码…...

神经网络基础知识

神经网络基础知识 文章目录神经网络基础知识一、人工神经网络1.激活函数sigmod函数Tanh函数Leaky Relu函数分析2.过拟合和欠拟合二、学习与感知机1.损失函数与代价函数2. 线性回归和逻辑回归3. 监督学习与无监督学习三、优化1.梯度下降法2.随机梯度下降法(SGD)3. 批量梯度下降法…...

SpringBoot开发规范部分通用模板+idea配置【项目通用-1】

SpringBoot开发规范通用模板 1 分页插件使用 通过MybatisPlus配置分页插件拦截器 Configuration MapperScan("com.xuecheng.content.mapper") //拦截的mapper层 public class MybatisPlusConfig {//定义分页的拦截器Beanpublic MybatisPlusInterceptor getMybatisPl…...

程序的机器级表示part3——算术和逻辑操作

目录 1.加载有效地址 2. 整数运算指令 2.1 INC 和 DEC 2.2 NEG 2.3 ADD、SUB 和 IMUL 3. 布尔指令 3.1 AND 3.2 OR 3.3 XOR 3.4 NOT 4. 移位操作 4.1 算术左移和逻辑左移 4.2 算术右移和逻辑右移 5. 特殊的算术操作 1.加载有效地址 指令效果描述leaq S, DD…...

基于YOLOV5的钢材缺陷检测

数据和源码见文末 1.任务概述 数据集使用的是东北大学收集的一个钢材缺陷检测数据集,需要检测出钢材表面的6种划痕。同时,数据集格式是VOC格式,需要进行转化,上传的源码中的数据集是经过转换格式的版本。 2.数据与标签配置方法 在数据集目录下,train文件夹下有训练集数据…...

Session与Cookie的区别(三)

中场休息 让我们先从比喻回到网络世界里&#xff0c;HTTP 是无状态的&#xff0c;所以每一个 Request 都是不相关的&#xff0c;就像是对小明来说每一位客人都是新的客人一样&#xff0c;他根本不知道谁是谁。 既然你没办法把他们关联&#xff0c;就代表状态这件事情也不存在。…...

七大设计原则之接口隔离原则应用

目录1 接口隔离原则介绍2 接口隔离原则应用1 接口隔离原则介绍 接口隔离原则&#xff08;Interface Segregation Principle, ISP&#xff09;是指用多个专门的接口&#xff0c;而不使用单一的总接口&#xff0c;客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口时…...

【Shell1】shell语法,ssh/build/scp/upgrade,环境变量,自动升级bmc

文章目录1.shell语法&#xff1a;shell是用C语言编写的程序&#xff0c;是用户使用Linux的桥梁&#xff0c;硬件>内核(os)>shell>文件系统1.1 变量&#xff1a;readonly定义只读变量&#xff0c;unset删除变量1.2 函数&#xff1a;shell脚本传递的参数中包含空格&…...

JavaScript HTML DOM - 改变CSS

JavaScript 是一种动态语言&#xff0c;它可以动态地修改网页的外观&#xff0c;并且使用HTML DOM&#xff08;文档对象模型&#xff09;可以更方便地控制HTML元素的样式。 JavaScript 通过在HTML DOM中更改CSS属性来更改样式&#xff0c;这些CSS属性包括颜色、位置、字体大小…...

mycat连接mysql 简单配置

mycat三个配置文件位于conf下 可通过Notepad操作 首先配置service.xml中的user标签&#xff0c;设置用户名&#xff0c;密码&#xff0c;查询权限&#xff0c;是否只读等 只是设置了root用户&#xff0c;有所有权限 配置schema.xml <?xml version"1.0"?&g…...

Spring常用注解

文章目录一、Bean交给Spring管理1、Component2、Bean3、Controller4、Service5、Repository6、Configuration7、ComponentScan二、作用域1、Lazy(false)Scope三、依赖注入1、Autowired2、Resource3、Qualifier四、读取配置文件值1、Value一、Bean交给Spring管理 1、Component …...

I.MX6ULL内核开发9:kobject-驱动的基石

目录 一、摘要 二、重点 三、驱动结构模型 四、关键函数分析 kobject_create_and_add()函数 kobject_create()函数 kobject_init&#xff08;&#xff09;函数 kobject_init_internal(&#xff09;函数 kobject_add&#xff08;&#xff09;函数 kobject_add_varg&am…...

Docker-harbor私有仓库

一、Harbor概述 1、Harbor的概念 • Harbor是VMware公司开源的企业级Docker Registry项目&#xff0c;其目标是帮助用户迅速搭建一个企业级的Docker Registry服务 • Harbor以 Docker 公司开源的Registry 为基础&#xff0c;提供了图形管理UI、基于角色的访问控制(Role Base…...

Java之动态规划之子序列问题

目录 0.动态规划问题 一.最长递增子序列 1.题目描述 2.问题分析 3.代码实现 二.最长递增子序列 1.题目描述 2.问题分析 3.代码实现 三.最长重复子数组 1.题目描述 2.问题分析 3.代码实现 4.代码的优化(滚动数组) 四.最长公共子序列 1.题目描述 2.问题分析 3.代…...

java ArrayList

目录 一.简单介绍 二.ArrayList的底层结构 2.1ArrayList的底层结构和操作分析 2.ArrayList 底层源码分析 三.ArrayList 方法 四.代码使用方法 一.简单介绍 ArrayList 类是一个可以动态修改的数组&#xff0c;与普通数组的区别就是它是没有固定大小的限制&#xff0c;我们…...

高校vs中小学气象站:核心区别

绝大多数普通校园气象站仅适合中小学可视化科普展示&#xff0c;数据精度低、无原始数据导出、无开放接口、参数单一&#xff0c;完全无法满足高校教学科研需求。中小学设备&#xff1a;侧重外观展示、简单数据观看、趣味科普&#xff0c;精度普通、数据封闭、无科研溯源能力&a…...

企业微信打卡数据API实战:用Java+FastJSON手把手教你拉取员工考勤记录

企业微信考勤数据集成实战&#xff1a;JavaFastJSON构建高可用API对接方案 考勤数据作为企业管理的重要基础数据&#xff0c;其自动化采集与处理能力直接影响人力资源管理的效率。企业微信作为国内主流的企业级通讯工具&#xff0c;其开放的打卡数据API为开发者提供了便捷的集成…...

如何快速掌握ComfyUI-WanVideoWrapper:AI视频生成从入门到精通

如何快速掌握ComfyUI-WanVideoWrapper&#xff1a;AI视频生成从入门到精通 【免费下载链接】ComfyUI-WanVideoWrapper 项目地址: https://gitcode.com/GitHub_Trending/co/ComfyUI-WanVideoWrapper ComfyUI-WanVideoWrapper 是一款专为ComfyUI设计的强大AI视频生成插件…...

基于RAG与LangChain的AI阅读助手BookWith架构与实现

1. 项目概述&#xff1a;当AI成为你的阅读伙伴作为一名深度阅读爱好者和技术实践者&#xff0c;我一直在寻找一种能真正“理解”内容&#xff0c;并与我进行深度对话的阅读工具。传统的电子书阅读器&#xff0c;无论是Kindle还是其他应用&#xff0c;本质上都只是将纸质书数字化…...

深入u-boot目录结构:以全志V3s的LicheePi Zero为例,理解每个文件夹的作用

深入解析u-boot目录结构&#xff1a;全志V3s平台下的LicheePi Zero实践指南 当你第一次打开u-boot源码仓库时&#xff0c;面对密密麻麻的目录结构可能会感到无从下手。作为嵌入式系统开发中至关重要的启动加载程序&#xff0c;u-boot的架构设计既体现了通用性又兼顾了平台特异…...

从MWC 2016看5G与物联网:技术演进、产业博弈与生态构建

1. 从巴塞罗那看2016年移动通信的十字路口 时间回到2016年初&#xff0c;如果你身处通信行业&#xff0c;那么2月底的日程表上&#xff0c;巴塞罗那的“移动世界大会”绝对是一个绕不开的焦点。那不是一个普通的展会&#xff0c;更像是一个行业在技术迭代、市场转型和地缘政治多…...

GitHub 被分号击穿信任防线,AI 逆向工具敲响闭源系统安全警钟

GitHub 被分号击穿三层信任&#xff0c;AI 填平逆向护城河敲响闭源系统安全警钟 2026 年 3 月 4 日&#xff0c;GitHub 收到 Wiz 通过 Bug Bounty 提交的报告&#xff0c;报告描述的攻击入口极其简单&#xff1a;一条构造过的 git push&#xff0c;带一个 push option&#xff…...

Switch大气层系统完整教程:从零开始打造稳定自制系统环境

Switch大气层系统完整教程&#xff1a;从零开始打造稳定自制系统环境 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 大气层系统&#xff08;Atmosphere&#xff09;是任天堂Switch平台上最…...

AI 写论文哪个软件最好?2026 毕业论文实测:真文献 + 真图表 + 全流程,虎贲等考 AI 稳占首选

&#x1f4cc; 配图 1&#xff1a;首图海报 ——AI 写论文哪个最好&#xff5c;虎贲等考 AI&#xff5c;毕业论文神器&#xff5c;真实文献 实证图表 每年毕业季&#xff0c;所有人都在问&#xff1a;AI 写论文哪个软件最好&#xff1f;市面上工具看似很多&#xff0c;可一用…...

ANSYS Workbench网格进阶:巧用‘Face Meshing’与‘Sweep’扫掠,让你的轴承座仿真既快又准

ANSYS Workbench网格进阶&#xff1a;巧用‘Face Meshing’与‘Sweep’扫掠提升轴承座仿真效率 轴承座作为机械传动系统中的关键部件&#xff0c;其应力分布与变形分析的准确性直接影响设备可靠性评估。传统四面体网格虽能快速生成&#xff0c;但在应力集中区域往往需要极高密度…...