宝塔面板搭建自己的网站,并发布公网远程访问_宝塔本地测试-CSDN博客

mikel阅读(305)

来源: 宝塔面板搭建自己的网站,并发布公网远程访问_宝塔本地测试-CSDN博客

文章目录
1. 环境安装
2. 安装cpolar内网穿透
3. 内网穿透
4.固定http地址
5. 配置二级子域名
6.创建一个测试页面
宝塔面板简单几步搭建本地web站点,并做内网穿透,实现公网用户也可以正常远程访问,无需公网IP,无需设置路由器。
1. 环境安装
安装apache服务器,在宝塔面板中我们点击网站,然后会提示安装apache服务器。
选择极速安装
然后等待安装完成即可,安装完成在左边消息列表会提示
2. 安装cpolar内网穿透
cpolar官网:https://www.cpolar.com/
打开宝塔终端命令窗口,使用cpolar一件安装脚本:
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash
1
token认证
登录cpolar官网后台,点击左侧的验证,查看自己的认证token,之后将token贴在命令行里
cpolar authtoken xxxxxxx
1
向系统添加服务
sudo systemctl enable cpolar
1
启动cpolar服务
sudo systemctl start cpolar
1
开放9200端口
在宝塔面板中选择安全.然后开放9200端口
登录cpolar web UI 管理界面
然后局域网ip访问9200端口即可出现cpolar管理界面,输入cpolar邮箱账号进行登陆
3. 内网穿透
登录cpolar web UI管理界面后,我们创建一个http隧道,指向80端口,因为apache服务默认是80端口
隧道名称:可自定义,注意不要重复
协议:http
本地地址:80
端口类型:随机域名
地区:China vip
点击创建
创建成功后我们打开在线隧道列表复制创建的公网地址
然后我们打开宝塔面板,点击网站,选择添加站点,把复制的公网地址粘贴到域名的参数框,然后点击提交
这个时候我们可以看到站点创建成功
然后我们再使用复制的公网地址,打开浏览器访问,出现欢迎页表示成功
4.固定http地址
由于刚刚创建隧道使用的是随机临时地址,该地址会在24小时内发生变化,为了长期远程访问,我们接下来将这个公网地址配置为固定的。
需升级至基础套餐或以上才支持配置二级子域名
登录cpolar官网后台,点击左侧仪表盘的预留,找到保留二级子域名,为http隧道保留一个二级子域名。
地区:选择服务器地区
名称:填写您想要保留的二级子域名(可自定义)
描述:即备注,可自定义填写
本例保留一个名称为mywebsitegame的二级子域名。子域名保留成功后,我们将子域名复制下来,接下来需要将其配置到隧道中去。
5. 配置二级子域名
登录cpolar web ui管理界面。点击左侧仪表盘的隧道管理——隧道列表,找到需要配置二级子域名的隧道(本例中为apache website隧道),点击右侧的编辑
修改隧道信息,将二级子域名配置到隧道中:
域名类型:改为选择二级子域名
Sub Domain:填写我们刚刚所保留的二级子域名(本例为mywebsitegame)
修改完成后,点击更新
隧道更新成功后,点击左侧仪表盘的状态——在线隧道列表,可以看到隧道的公网地址,已经更新为二级子域名了,将公网地址复制下来。
然后我们打开宝塔面板,找到站点,点击设置
添加一个我们固定的公网地址域名
然后把之前创建的随机地址删除
然后我们打开浏览器,使用固定的公网地址进行访问,以上我们就配置好了站点远程访问
6.创建一个测试页面
点击站点根目录路径,直接点击
新建一个名字为game.html页面
然后双击文件编辑,把下面代码复制进去(贪吃蛇小游戏),然后Ctrl+S保存
<!DOCTYPE html>
<html>
<head>
<title>贪吃蛇</title>
<meta charset=”UTF-8″>
<meta name=”keywords” content=”贪吃蛇”>
<meta name=”Description” content=”这是一个初学者用来学习的小游戏”>
<style type=”text/css”>
*{margin:0;}
.map{margin:100px auto;
height:600px;
width:900px;
background:#00D0FF;
border:10px solid #AFAEB2;
border-radius:8px;
}
</style>
</head>
<body>
<div class=”map”>
<canvas id=”canvas” height=”600″ width=”900″>
</canvas>
</div>
<script type=”text/JavaScript”>
 //获取绘制工具
/*
var canvas = document.getElementById(“canvas”);
var ctx = canvas.getContext(“2d”);//获取上下文
ctx.moveTo(0,0);
ctx.lineTo(450,450);*/
var c=document.getElementById(“canvas”);
    var ctx=c.getContext(“2d”);
    /*ctx.beginPath();
    ctx.moveTo(0,0);
    ctx.lineTo(450,450);
    ctx.stroke();
    */
    var snake =[];//定义一条蛇,画蛇的身体
    var snakeCount = 6;//初始化蛇的长度
var foodx =0;
var foody =0;
    var togo =0;
    function drawtable()//画地图的函数
    {
    for(var i=0;i<60;i++)//画竖线
    {
    ctx.strokeStyle=”black”;
    ctx.beginPath();
    ctx.moveTo(15*i,0);
    ctx.lineTo(15*i,600);
    ctx.closePath();
    ctx.stroke();
    }
        for(var j=0;j<40;j++)//画横线
    {
    ctx.strokeStyle=”black”;
    ctx.beginPath();
    ctx.moveTo(0,15*j);
    ctx.lineTo(900,15*j);
    ctx.closePath();
    ctx.stroke();
    }
    for(var k=0;k<snakeCount;k++)//画蛇的身体
{
ctx.fillStyle=”#000″;
if (k==snakeCount-1)
{
ctx.fillStyle=”red”;//蛇头的颜色与身体区分开
}
ctx.fillRect(snake[k].x,snake[k].y,15,15);//前两个数是矩形的起始坐标,后两个数是矩形的长宽。
}
//绘制食物
    ctx.fillStyle =”black”;
     ctx.fillRect(foodx,foody,15,15);
     ctx.fill();
    }
    function start()//定义蛇的坐标
    {
    //var snake =[];//定义一条蛇,画蛇的身体
        //var snakeCount = 6;//初始化蛇的长度
for(var k=0;k<snakeCount;k++)
    {
    snake[k]={x:k*15,y:0};
            }
  drawtable();
          addfood();//在start中调用添加食物函数
    }
    function addfood()
{
foodx = Math.floor(Math.random()*60)*15; //随机产生一个0-1之间的数
foody = Math.floor(Math.random()*40)*15;
for (var k=0;k<snake;k++)
{
if (foodx==snake[k].x&&foody==sanke[k].y)//防止产生的随机食物落在蛇身上
{
addfood();
}
}
}
   function move()
   {
switch (togo)
{
case 1: snake.push({x:snake[snakeCount-1].x-15,y:snake[snakeCount-1].y}); break;//向左走
case 2: snake.push({x:snake[snakeCount-1].x,y:snake[snakeCount-1].y-15}); break;
case 3: snake.push({x:snake[snakeCount-1].x+15,y:snake[snakeCount-1].y}); break;
case 4: snake.push({x:snake[snakeCount-1].x,y:snake[snakeCount-1].y+15}); break;
case 5: snake.push({x:snake[snakeCount-1].x-15,y:snake[snakeCount-1].y-15}); break;
case 6: snake.push({x:snake[snakeCount-1].x+15,y:snake[snakeCount-1].y+15}); break;
default: snake.push({x:snake[snakeCount-1].x+15,y:snake[snakeCount-1].y});
}
    snake.shift();//删除数组第一个元素
    ctx.clearRect(0,0,900,600);//清除画布重新绘制
    isEat();
isDead();
drawtable();
   }
   function keydown(e)
   {
   switch(e.keyCode)
{
         case 37: togo=1; break;
case 38: togo=2; break;
case 39: togo=3; break;
case 40: togo=4; break;
case 65: togo=5; break;
case 68: togo=6; break;
}
   }
   function isEat()//吃到食物后长度加1
   {
    if(snake[snakeCount-1].x==foodx&&snake[snakeCount-1].y==foody)
   {
addfood();
snakeCount++;
snake.unshift({x:-15,y:-15});
   }
   }
   //死亡函数
   function isDead()
   {
    if (snake[snakeCount-1].x>885||snake[snakeCount-1].y>585||snake[snakeCount-1].x<0||snake[snakeCount-1].y<0)
{
window.location.reload();
}
   }
    document.onkeydown=function(e)
{
keydown(e);
}
window.onload = function()//调用函数
{
start();
setInterval(move,150);
drawtable();
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
然后我们浏览器使用公网地址加这个html文件访问,即可看到我们部署的小游戏。
————————————————
版权声明:本文为CSDN博主「橙 子_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhangxia_/article/details/130366548

在layui中使用 jquery 触发select 的 change事件无效(转载)-CSDN博客

mikel阅读(349)

来源: 在layui中使用 jquery 触发select 的 change事件无效(转载)-CSDN博客

使用layui.use监听select事件

在使用layui框架时,原生的js onchange无法起效。要使用
layui.use([‘layer’, ‘JQuery’, ‘form’], function () {
var layer = layui.layer,
$ = layui.JQuery,
form = layui.form;


<select lay-filter="demo" lay-verify="required">
 
<script>
	layui.use(['layer', 'jquery', 'form'], function () {
			var layer = layui.layer,
					$ = layui.jquery,
					form = layui.form;
 
			form.on('select(demo)', function(data){
				if(data.value == 1){
					$("#searchSessionNum").attr("disabled","true");
					form.render('select');
				}else{
					$("#searchSessionNum").removeAttr("disabled");
					form.render('select');//select是固定写法 不是选择器
				}
			});
		});
</script>

Failed to load resource: net::ERR_CACHE_READ_FAILURE解决办法-CSDN博客

mikel阅读(440)

来源: Failed to load resource: net::ERR_CACHE_READ_FAILURE解决办法-CSDN博客

在使用elasticsearch分词插件时,使用kibana作为客户端管理,突然报错Kibana did not load properly. Check the server output for more information.

打开后台console查看报错Failed to load resource: net::ERR_CACHE_READ_FAILURE。缓存读取失败。

原因是我之前清空了浏览器的缓存,又重启了电脑,导致kibana读取缓存失败。

解决办法:强制再刷新下浏览器缓存就好了。
————————————————
版权声明:本文为CSDN博主「ghjzzhg」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ghjzzhg/article/details/103009276

C#/.NET/.NET Core优秀项目和框架2023年11月简报 - 追逐时光者 - 博客园

mikel阅读(357)

来源: C#/.NET/.NET Core优秀项目和框架2023年11月简报 – 追逐时光者 – 博客园

前言

公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(公众号每周至少推荐两个优秀的项目和框架当然节假日除外),公众号推文有项目和框架的介绍、功能特点以及部分截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。

简报GitHub开源地址:https://github.com/YSGStudyHards/DotNetGuide/blob/main/docs/DotNet/DotNetProjectMonthly.md

CAP

  • 项目简介: CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级、易使用、高性能等特点。CAP 是一个EventBus,同时也是一个在微服务或者SOA系统中解决分布式事务问题的一个框架。它有助于创建可扩展,可靠并且易于更改的微服务系统。
  • 项目源码地址: https://github.com/dotnetcore/CAP
  • 公众号详细介绍: https://mp.weixin.qq.com/s/ONM9bLKidVCS4pAwJbG9tg

ZEQP.WMS

HandyControl

NETCore.Encrypt

WinMemoryCleaner

Hello算法

  • 项目简介: Hello算法一个开源免费、新手友好的数据结构与算法入门教程。全书采用动画图解,内容清晰易懂、学习曲线平滑,引导初学者探索数据结构与算法的知识地图。源代码可一键运行,帮助读者在练习中提升编程技能,了解算法工作原理和数据结构底层实现。支持 Java, C++, Python, Go, JS, TS, C#, Swift, Rust, Dart, Zig 等语言。
  • 项目源码地址: https://github.com/krahets/hello-algo
  • 公众号详细介绍: https://mp.weixin.qq.com/s/9lb5iu6tGNiSGcIrf7fQ3A

PaddleOCRSharp

  • 项目简介: PaddleOCRSharp 是一个基于百度飞桨PaddleOCR的.NET版本OCR工具类库。项目核心组件PaddleOCR.dll,由C++编写,根据百度飞桨PaddleOCR的C++代码修改并优化而成。目前已经支持C++、.NET、Python、Golang、Rust等开发语言的直接API接口调用。项目包含文本识别、文本检测、表格识别功能。本项目针对小图识别不准的情况下做了优化,比飞桨原代码识别准确率有所提高。包含总模型仅8.6M的超轻量级中文OCR,单模型支持中英文数字组合识别、竖排文本识别、长文本识别。同时支持中英文、纯英文以及多种语言文本检测识别。
  • 项目源码地址: https://gitee.com/raoyutian/paddle-ocrsharp
  • 公众号详细介绍: https://mp.weixin.qq.com/s/9F_rSB8Wm69jLdgsH4ufvg

MrHuo.OAuth

CoreShop

  • 项目简介: 核心商城系统(CoreShop) 是基于 ASP.NET 7.0、Uni-App开发、支持可视化布局的小程序商城系统;前后端分离,支持分布式部署,跨平台运行;拥有分销、代理、团购秒杀、接龙、拼团、直播、优惠券、自定义表单等众多营销功能,拥有完整SKU、下单、售后、物流流程,支持可视化自定义首页模块布局效果。
  • 项目源码地址: https://github.com/CoreUnion/CoreShop
  • 公众号详细介绍: https://mp.weixin.qq.com/s/iRxmWUXrqArZD_Ax4i6wwg

ToastFish

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。

.NET8 依赖注入 - xiaolipro - 博客园

mikel阅读(348)

来源: .NET8 依赖注入 – xiaolipro – 博客园

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于解耦组件(服务)之间的依赖关系。它通过将依赖关系的创建和管理交给外部容器来实现,而不是在组件(服务)内部直接创建依赖对象。

​ 咱就是通过 IServiceCollection 和 IServiceProvider 来实现的,他们直接被收入到了runtime libraries,在整个.NET平台下通用!

3.1 ServiceCollection

​ IServiceCollection 本质是一个 ServiceDescriptor 而 ServiceDescriptor 则是用于描述服务类型,实现和生命周期

public interface IServiceCollection : 
    IList<ServiceDescriptor>,
    ICollection<ServiceDescriptor>,
    IEnumerable<ServiceDescriptor>,
    IEnumerable;

​ 官方提供一些列拓展帮助我们向服务容器中添加服务描述,具体在 ServiceCollectionServiceExtensions

builder.Services.AddTransient<StudentService>();
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository>("a");
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository2>("b");
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopeService>();
builder.Services.AddSingleton<SingletonService>();

3.2 ServiceProvider

​ IServiceProvider 定义了一个方法 GetService,帮助我们通过给定的服务类型,获取其服务实例

public interface IServiceProvider
{
  object? GetService(Type serviceType);
}

​ 下面是 GetService 的默认实现(如果不给定engine scope,则默认是root)

public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);

​ 也就是

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
{
    if (_disposed)
    {
        ThrowHelper.ThrowObjectDisposedException();
    }
    // 获取服务标识符对应的服务访问器
    ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);
    // 执行解析时的hock
    OnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);
    DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);
    // 通过服务访问器提供的解析服务,得到服务实例
    object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);
    System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));
    return result;
}

​ 其中,服务标识符 ServiceIdentifier 其实就是包了一下服务类型,和服务Key(为了.NET8的键化服务)

internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>
{
    public object? ServiceKey { get; }
    public Type ServiceType { get; }
}

​ 显而易见的,我们的服务解析是由 serviceAccessor.RealizedService 提供,而创建服务访问器 serviceAccessor 只有一个实现 CreateServiceAccessor

private ServiceAccessor CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
{
    // 通过 CallSiteFactory 获取服务的调用点(CallSite),这是服务解析的一个表示形式。
    ServiceCallSite? callSite = CallSiteFactory.GetCallSite(serviceIdentifier, new CallSiteChain());
    
    // 如果调用站点不为空,则继续构建服务访问器。
    if (callSite != null)
    {
        DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceIdentifier.ServiceType, callSite);
        
        // 触发创建调用站点的相关事件。
        OnCreate(callSite);

        // 如果调用站点的缓存位置是根(Root),即表示这是一个单例服务。
        if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
        {
            // 直接拿缓存内容
            object? value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
            return new ServiceAccessor { CallSite = callSite, RealizedService = scope => value };
        }

        // 通过引擎解析
        Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite);
        return new ServiceAccessor { CallSite = callSite, RealizedService = realizedService };
    }
    
    // 如果调用点为空,则它的实现服务函数总是返回 null。
    return new ServiceAccessor { CallSite = callSite, RealizedService = _ => null };
}

3.2.1 ServiceProviderEngine

​ ServiceProviderEngine 是服务商解析服务的执行引擎,它在服务商被初始化时建立。有两种引擎,分别是动态引擎运行时引擎,在 NETFRAMEWORK || NETSTANDARD2_0 默认使用动态引擎。

        private ServiceProviderEngine GetEngine()
        {
            ServiceProviderEngine engine;

#if NETFRAMEWORK || NETSTANDARD2_0
            engine = CreateDynamicEngine();
#else
            if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine)
            {
                engine = CreateDynamicEngine();
            }
            else
            {
                // Don't try to compile Expressions/IL if they are going to get interpreted
                engine = RuntimeServiceProviderEngine.Instance;
            }
#endif
            return engine;

            [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
                Justification = "CreateDynamicEngine won't be called when using NativeAOT.")] // see also https://github.com/dotnet/linker/issues/2715
            ServiceProviderEngine CreateDynamicEngine() => new DynamicServiceProviderEngine(this);
        }

​ 由于.NET Aot技术与dynamic技术冲突,因此Aot下只能使用运行时引擎,但动态引擎在大多情况下仍然是默认的。

动态引擎使用了emit技术,这是一个动态编译技术,而aot的所有代码都需要在部署前编译好,因此运行时无法生成新的代码。那运行时引擎主要使用反射,目标是在不牺牲太多性能的情况下,提供一个在aot环境中可行的解决方案。

​ 我们展开动态引擎来看看它是如何解析服务的。

public override Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite)
{
    // 定义一个局部变量来跟踪委托被调用的次数
    int callCount = 0;
    return scope =>
    {
        // 当委托被调用时,先使用CallSiteRuntimeResolver.Instance.Resolve方法来解析服务。这是一个同步操作,确保在编译优化之前,服务可以被正常解析。
        var result = CallSiteRuntimeResolver.Instance.Resolve(callSite, scope);
        // 委托第二次被调用,通过UnsafeQueueUserWorkItem在后台线程上启动编译优化
        if (Interlocked.Increment(ref callCount) == 2)
        {
            // 将一个工作项排队到线程池,但不捕获当前的执行上下文。
            _ = ThreadPool.UnsafeQueueUserWorkItem(_ =>
            {
                try
                {
                    // 用编译优化后的委托替换当前的服务访问器,主要用到emit/expression技术
                    _serviceProvider.ReplaceServiceAccessor(callSite, base.RealizeService(callSite));
                }
                catch (Exception ex)
                {
                    DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());
                    Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");
                }
            },
            null);
        }
        return result;
    };
}

这个实现的关键思想是,第一次解析服务时使用一个简单的运行时解析器,这样可以快速返回服务实例。然后,当服务再被解析,它会在后台线程上启动一个编译过程,生成一个更高效的服务解析委托。一旦编译完成,新的委托会替换掉原来的委托,以后的服务解析将使用这个新的、更高效的委托。这种方法可以在不影响应用程序启动时间的情况下,逐渐优化服务解析的性能。

3.2.2 ServiceProviderEngineScope

​ ServiceProviderEngineScope 闪亮登场,他是我们服务商的代言人,从定义不难看出他对外提供了服务商所具备的一切能力

internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, 			IAsyncDisposable, IServiceScopeFactory
{
    // this scope中所有实现IDisposable or IAsyncDisposable的服务
    private List<object>? _disposables;
    // 解析过的服务缓存(其实就是scope生命周期的服务缓存,singleton生命周期的服务缓存都直接挂在调用点上了)
    internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }
    // 实锤服务商代言人
    public IServiceProvider ServiceProvider => this;
    // 没错啦,通过root scope我们又能继续创建无数个scope,他们彼此独立
    public IServiceScope CreateScope() => RootProvider.CreateScope();
}

​ 我们来观察他获取服务的逻辑,可以发现他就是很朴实无华的用着我们根服务商 ServiceProvider,去解析服务,那 engine scope 呢,就是 this。现在我们已经隐约可以知道engine scope,就是为了满足scope生命周期而生。而 ResolvedServices 中存的呢,就是该scope中的所有scope生命周期服务实例啦,在这个scope中他们是唯一的。

public object? GetService(Type serviceType)
{
    if (_disposed)
    {
        ThrowHelper.ThrowObjectDisposedException();
    }
    return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
}

​ 再来看另一个核心的方法:CaptureDisposable,实现disposable的服务会被添加到 _disposables。

internal object? CaptureDisposable(object? service)
{
    // 如果服务没有实现 IDisposable or IAsyncDisposable,那么不需要捕获,直接原路返回
    if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable))
    {
        return service;
    }
    bool disposed = false;
    lock (Sync)
    {
        if (_disposed) // 如果scope已经销毁则进入销毁流程
        {
            disposed = true;
        }
        else
        {
            _disposables ??= new List<object>();
            _disposables.Add(service);
        }
    }
    // Don't run customer code under the lock
    if (disposed) // 这表示我们在试图捕获可销毁服务时,scope就已经被销毁
    {
        if (service is IDisposable disposable)
        {
            disposable.Dispose();
        }
        else
        {
            // sync over async, for the rare case that an object only implements IAsyncDisposable and may end up starving the thread pool.
            object? localService = service; // copy to avoid closure on other paths
            Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();
        }
        // 这种case会抛出一个ObjectDisposedException
        ThrowHelper.ThrowObjectDisposedException();
    }
    return service;
}

​ 在engine scope销毁时,其作用域中所有scope生命周期且实现了disposable的服务(其实就是_disposable)呢,也会被一同的销毁。

public ValueTask DisposeAsync()
{
    List<object>? toDispose = BeginDispose(); // 获取_disposable
    if (toDispose != null)
    {
        // 从后往前,依次销毁服务
    }
}

​ 那么有同学可能就要问了:单例实例既然不存在root scope中,而是单独丢到了调用点上,那他是咋销毁的?压根没看到啊,那不得泄露了?

​ 其实呀,同学们并不用担心这个问题。首先,单例服务的实例确实是缓存在调用点上,但 disable 服务仍然会被 scope 捕获呀(在下文会详细介绍)。在 BeginDispose 中的,我们会去判断,如果是 singleton case,且root scope 没有被销毁过,我们会主动去销毁喔~

if (IsRootScope && !RootProvider.IsDisposed()) RootProvider.Dispose();

3.3 ServiceCallSite

​ ServiceCallSite 的主要职责是封装服务解析的逻辑,它可以代表一个构造函数调用、属性注入、工厂方法调用等。DI系统使用这个抽象来表示服务的各种解析策略,并且可以通过它来生成服务实例。

internal abstract class ServiceCallSite
{
    protected ServiceCallSite(ResultCache cache)
	 {
	     Cache = cache;
	 }
    public abstract Type ServiceType { get; }
	 public abstract Type? ImplementationType { get; }
	 public abstract CallSiteKind Kind { get; }
	 public ResultCache Cache { get; }
	 public object? Value { get; set; }
	 public object? Key { get; set; }
	 public bool CaptureDisposable => ImplementationType == null ||
    	typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
    	typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}

3.3.1 ResultCache

​ 其中 ResultCache 定义了我们如何缓存解析后的结果

public CallSiteResultCacheLocation Location { get; set; } // 缓存位置
public ServiceCacheKey Key { get; set; } // 服务key(键化服务用的)

​ CallSiteResultCacheLocation 是一个枚举,定义了几个值

  1. Root:表示服务实例应该在根级别的 IServiceProvider 中缓存。这通常意味着服务实例是单例的(Singleton),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。
  2. Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。
  3. Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。
  4. None:表示没有缓存服务实例。

​ ServiceCacheKey 结构体就是包了一下服务标识符和一个slot,用于适配多实现的

internal readonly struct ServiceCacheKey : IEquatable<ServiceCacheKey>
{
    public ServiceIdentifier ServiceIdentifier { get; }
    public int Slot { get; } // 那最后一个实现的slot是0
}

3.3.2 CallSiteFactory.GetCallSite

​ 那我们来看看调用点是怎么创建的吧,其实上面已经出现过一次了:

private ServiceCallSite? CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
{
    if (!_stackGuard.TryEnterOnCurrentStack()) // 防止栈溢出
    {
        return _stackGuard.RunOnEmptyStack(CreateCallSite, serviceIdentifier, callSiteChain);
    }
    // 获取服务标识符对应的锁,以确保在创建调用点时的线程安全。
    // 是为了保证并行解析下的调用点也只会被创建一次,例如:
    // C -> D -> A
    // E -> D -> A
    var callsiteLock = _callSiteLocks.GetOrAdd(serviceIdentifier, static _ => new object());
    lock (callsiteLock)
    {
        // 检查当前服务标识符是否会导致循环依赖
        callSiteChain.CheckCircularDependency(serviceIdentifier);
        // 首先尝试创建精确匹配的服务调用站点,如果失败,则尝试创建开放泛型服务调用站点,如果还是失败,则尝试创建枚举服务调用站点。如果所有尝试都失败了,callSite将为null。
        ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??
                                   TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??
                                   TryCreateEnumerable(serviceIdentifier, callSiteChain);
        return callSite;
    }
}

​ 那服务点的创建过程我就简单概述一下啦

  1. 查找调用点缓存,存在就直接返回啦
  2. 服务标识符会被转成服务描述符 ServiceDescriptor (key化服务不指定key默认取last)
  3. 计算ServiceCallSite,依次是:
    1. TryCreateExact
      1. 计算 ResultCache
      2. 如果已经有实现实例了,则返回 ConstantCallSite:表示直接返回已经创建的实例的调用点。
      3. 如果有实现工厂,则返回 FactoryCallSite:表示通过工厂方法创建服务实例的调用点。
      4. 如果有实现类型,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    2. TryCreateOpenGeneric
      1. 根据泛型定义获取服务描述符 ServiceDescriptor
      2. 计算 ResultCache
      3. 使用服务标识符中的具体泛型参数来构造实现的闭合类型
      4. AOT兼容性测试(因为不能保证值类型泛型的代码已经生成)
      5. 如果成功闭合,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    3. TryCreateEnumerable
      1. 确定类型是 IEnumerable<T>
      2. AOT兼容性测试(因为不能保证值类型数组的代码已经生成)
      3. 如果 T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact
      4. 否则,方向循环 TryCreateExact,然后方向循环 TryCreateOpenGeneric

3.4 CallSiteVisitor

​ 好了,有了上面的了解我们可以开始探索服务解析的内幕了。服务解析说白了就是引擎围着 CallSiteVisitor 转圈圈,所谓的解析服务,其实就是访问调用点了。

protected virtual TResult VisitCallSite(ServiceCallSite callSite, TArgument argument)
{
    if (!_stackGuard.TryEnterOnCurrentStack()) // 一些校验,分栈啥的
    {
        return _stackGuard.RunOnEmptyStack(VisitCallSite, callSite, argument);
    }
    switch (callSite.Cache.Location)
    {
        case CallSiteResultCacheLocation.Root: // 单例
            return VisitRootCache(callSite, argument);
        case CallSiteResultCacheLocation.Scope: // 作用域
            return VisitScopeCache(callSite, argument);
        case CallSiteResultCacheLocation.Dispose: // 瞬态
            return VisitDisposeCache(callSite, argument);
        case CallSiteResultCacheLocation.None: // 不缓存(ConstantCallSite)
            return VisitNoCache(callSite, argument);
        default:
            throw new ArgumentOutOfRangeException();
    }
}

​ 为了方便展示,我们这里的解析器都拿运行时来说,因为内部是反射,而emit、expression实在是难以观看。

3.4.1 VisitRootCache

​ 那我们来看看单例的情况下,是如何访问的:

protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
    if (callSite.Value is object value)
    {
        // Value already calculated, return it directly
        return value;
    }
    var lockType = RuntimeResolverLock.Root;
    // 单例都是直接放根作用域的
    ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;
    lock (callSite)
    {
        // 这里搞了个双检锁来确保在多线程环境中,同一时间只有一个线程可以执行接下来的代码块。
        // Lock the callsite and check if another thread already cached the value
        if (callSite.Value is object callSiteValue)
        {
            return callSiteValue;
        }
        object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
        {
            Scope = serviceProviderEngine,
            AcquiredLocks = context.AcquiredLocks | lockType
        });
        // 捕获可销毁的服务
        serviceProviderEngine.CaptureDisposable(resolved);
        // 缓存解析结果到调用点上
        callSite.Value = resolved;
        return resolved;
    }
}

​ 好,可以看到真正解析调用点的主角出来了 VisitCallSiteMain,那这里的 CallSiteKind 上面计算 ServiceCallSite 时呢已经讲的很清楚啦,咱对号入座就行了

protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
{
    switch (callSite.Kind)
    {
        case CallSiteKind.Factory:
            return VisitFactory((FactoryCallSite)callSite, argument);
        case  CallSiteKind.IEnumerable:
            return VisitIEnumerable((IEnumerableCallSite)callSite, argument);
        case CallSiteKind.Constructor:
            return VisitConstructor((ConstructorCallSite)callSite, argument);
        case CallSiteKind.Constant:
            return VisitConstant((ConstantCallSite)callSite, argument);
        case CallSiteKind.ServiceProvider:
            return VisitServiceProvider((ServiceProviderCallSite)callSite, argument);
        default:
            throw new NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));
    }
}

​ 我们就看看最经典的通过构造函数创建服务实例的调用点 ConstructorCallSite,很显然就是new嘛,只不过可能构造中依赖其它服务,那就递归创建呗。easy,其它几种太简单了大家自己去看看吧。

protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
{
    object?[] parameterValues;
    if (constructorCallSite.ParameterCallSites.Length == 0)
    {
        parameterValues = Array.Empty<object>();
    }
    else
    {
        parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];
        for (int index = 0; index < parameterValues.Length; index++)
        {
            // 递归构建依赖的服务
            parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);
        }
    }
    // new (xxx)
    return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);
}

3.4.2 VisitScopeCache

​ 在访问单例缓存的时候呢,仅仅通过了一个double check lock就搞定了,因为人家全局的嘛,咱再来看看访问作用域缓存,会不会有什么不一样

protected override object? VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
    // Check if we are in the situation where scoped service was promoted to singleton
    // and we need to lock the root
    return context.Scope.IsRootScope ?
        VisitRootCache(callSite, context) :
        VisitCache(callSite, context, context.Scope, RuntimeResolverLock.Scope);
}

​ 哈哈,它果然很不一般啊,上来就来检查我们是否是 root scope。如果是这种case呢,则走 VisitRootCache。但是奇怪啊,为什么访问 scope cache,所在 engine scope 能是 root scope?

​ 还记得 ServiceProvider 获取的服务实例的核心方法吗?engine scope 他是传进来的,如果我们给一个 root scope,自然就出现的这种case,只是这种 case 特别罕见。

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)

​ VisitCache 的同步模型写的实在是酷,我们看 RuntimeResolverLock 的枚举就两个:Scope = 1 和 Root = 2

  • AcquiredLocks=Scope时
  • 那 AcquiredLocks&false==0 显然成立,申请锁,也就是尝试独占改作用域的ResolvedServices
  • 申请成功进入同步块,重新计算AcquiredLocks|true=1
  • 如此,在该engine scope 中这条链路上的调用点都被占有,直到结束
  • AcquiredLocks=Root 时
    • 那显然 engine scope 也应该是 root scope,那么走 VisitRootCache case
    • 在 VisitRootCache 通过DCL锁住 root scope 上链路涉及的服务点,直至结束

​ 至此我们应该不难看出这个设计的精妙之处,即在非 root scope(scope生命周期)中,scope之间是互相隔离的,没有必要像 root scope(singleton生命周期)那样,在所有scope中独占服务点。

private object? VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine
{
    bool lockTaken = false;
    object sync = serviceProviderEngine.Sync;
    Dictionary<ServiceCacheKey, object?> resolvedServices = serviceProviderEngine.ResolvedServices;

    if ((context.AcquiredLocks & lockType) == 0)
    {
        Monitor.Enter(sync, ref lockTaken);
    }
    try
    {
        // Note: This method has already taken lock by the caller for resolution and access synchronization.
        // For scoped: takes a dictionary as both a resolution lock and a dictionary access lock.
        if (resolvedServices.TryGetValue(callSite.Cache.Key, out object? resolved))
        {
            return resolved;
        }
        // scope服务的解析结果是放在engine scope的ResolvedServices上的,而非调用点
        resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
        {
            Scope = serviceProviderEngine,
            AcquiredLocks = context.AcquiredLocks | lockType
        });
        serviceProviderEngine.CaptureDisposable(resolved);
        resolvedServices.Add(callSite.Cache.Key, resolved);
        return resolved;
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(sync);
        }
    }
}

3.4.3 VisitDisposeCache

​ 我们看最后一个,也就是 Transient case

protected override object? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
{
    return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));
}
C# 复制 全屏

​ 异常的简单,我们沿用了scope的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。

【ASP.NET Core】MVC过滤器:常见用法 - 东邪独孤 - 博客园

mikel阅读(322)

来源: 【ASP.NET Core】MVC过滤器:常见用法 – 东邪独孤 – 博客园

前面老周给大伙伴们演示了过滤器的运行流程,大伙只需要知道下面知识点即可:

1、过滤器分为授权过滤、资源访问过滤、操作方法(Action)过滤、结果过滤、异常过滤、终结点过滤。上一次咱们没有说异常过滤和终结点过滤,不过老周后面会说的。对这些过滤器,你有印象就行了。

2、所有过滤器接口都有同步版本和异步版本。为了让伙伴不要学得太累,咱们暂时只说同步版本的。

3、过滤器的应用可以分为全局和局部。全局先运行,局部后运行。全局在应用程序初始化时配置,局部用特性类来配置。

4、实际应用中,我们不需要实现所有过滤器接口,需要啥就实现啥即可。比如,你想在 Action 调用后修改一些东西,那实现 IActionFilter 接口就好了,其他不用管。

 

本篇咱们的重点在于“用”,光知道是啥是不行的,得拿来用才是硬道理。

我们先做第一个练习:阻止控制器的参数从查询字符串获取数据。

什么意思呢?咱们知道,MVC 在模型绑定时,会从 N 个 ValueProvider 中提取数据值,包括 QueryString、Forms、RouteData 等。其中,QueryString 就是URL的查询字符串。比如,咱们写一个这样的控制器:

复制代码
public class GameController : ControllerBase
{
    [HttpGet("game/play")]
    public string Play(Game g)
    {
        if(ModelState.IsValid == false)
        {
            return "你玩个寂寞";
        }
        return $"你正在玩{g.Year}年出的《{g.GameName}》游戏";
    }
}

public class Game
{
    /// <summary>
    /// 游戏序列号
    /// </summary>
    public string? GameSerial { get; set; }

    /// <summary>
    /// 游戏名称
    /// </summary>
    public string? GameName { get; set; }

    /// <summary>
    /// 谁发行的
    /// </summary>
    public string? Publisher { get; set; }

    /// <summary>
    /// 哪一年发行的
    /// </summary>
    public int Year { get; set; }
}
复制代码

这个通过 /game/play?gameserial=DDSBYCL-5K2FF&gamename=伏地魔三世&publisher=无德无能科技有限公司&year=2017 这样的URL就能传递数据给 g 参数。

这里我不做 HTML 页了,直接通过 MapGet 返回 HTML 内容。

复制代码
app.MapGet("/", async (HttpContext context) =>
{
    string html = """
    <!DOCTYPE html>
    <html>
        <head>
            <title>试试看</title>
            <style>
                label {
                    min-width: 100px;
                    display: inline-block;
                }
            </style>
        </head>
        <body>
            <div>
                <label for="gserial">游戏序列号:</label>
                <input id="gserial" type="text" />
            </div>
            <div>
                <label for="gname">游戏名称:</label>
                <input id="gname" type="text" />
            </div>
            <div>
                <label for="pub">发行者:</label>
                <input type="text" id="pub" />
            </div>
            <div>
                <label for="year">发行年份:</label>
                <input id="year" type="text"/>
            </div>
            <div>
                <button onclick="reqTest()">确定</button>
            </div>
            <p id="res"></p>

            <script>
                function reqTest() {
                    let serial= document.getElementById("gserial").value;
                    let name = document.getElementById("gname").value;
                    let pub = document.getElementById("pub").value;
                    let year = parseInt(document.getElementById("year").value, 10);
                    let result = document.getElementById("res");
                    const url = `/game/play?gameSerial=${serial}&gamename=${name}&publisher=${pub}&year=${year}`;
                    fetch(url, { method: "GET" })
                        .then(response => {
                            response.text().then(txt => {
                                result.innerHTML = txt;
                            });
                        });
                }
            </script>
        </body>
    </html>
    """;
    var response = context.Response;
    response.Headers.ContentType = "text/html; charset=UTF-8";
    await response.WriteAsync(html);
});
复制代码

设置响应的 Content-Type 头时一定要指定字符集是 UTF-8 编码,这样可以免去 99.999% 的乱码问题。向服务器发送请求是通过 fetch 函数实现的。

比如,咱们在页面上填写:

然后点一下“确定”按钮,提交成功后服务将响应:

你正在玩2022年出的《法外狂徒大冒险》游戏

模型绑定的数据是从查询字符串提取出来的。现在,咱们写一个过滤器,阻止 QueryStringValueProvider 提供查询字符串数据。而 QueryStringValueProvider 实例是由 QueryStringValueProviderFactory 工厂类负责创建的。因此,需要写一个过滤器,在模型绑定之前删除 QueryStringValueProviderFactory 对象,这样模型绑定时就不会读取 URL 中的查询字符串了。

于是,重点就落在选用哪种过滤器。关键点是:必须在模型绑定前做这项工作。所以,Action过滤器、结果过滤器就别指望了,而且肯定不是授权过滤器,那就剩下资源过滤器了。

咱们写一个自定义的资源过滤器—— RemoveQueryStringProviderFilter,实现的接口当然是 IResourceFilter 了。

复制代码
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;

public class RemoveQueryStringProviderFilter : IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        // 空空如也
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var qsValueProviders = context.ValueProviderFactories.OfType<QueryStringValueProviderFactory>();
        if (qsValueProviders != null && qsValueProviders.Any())
        {
            context.ValueProviderFactories.RemoveType<QueryStringValueProviderFactory>();
        }
    }
}
复制代码

我们要做的事情是在模型绑定之前才有效,所以 OnResourceExecuted 方法不用管,留白即可。在 OnResourceExecuting 方法中,首先用 ValueProviderFactories.OfType<T> 方法找出现有的 QueryStringValueProviderFactory 对象,若找到,用 RemoveType 方法删除。

把这个过滤器应用于全局。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
    options.Filters.Add<RemoveQueryStringProviderFilter>();
});
var app = builder.Build();

现在,咱们再运行应用程序,输入游戏信息。

点击“确定”按钮后,发现服务未响应正确的内容。

你正在玩0年出的《》游戏

由于无法从查询字符串中提取到数据,所以返回默认属性值。为什么不是返回“你玩个寂寞”呢?因为模型绑定并没有出错,也未出现验证失败的值,只是未提供值而已,即 ModelState.IsValid 的值依然是 true 的。

 

下面咱们看看异常过滤器怎么用。

异常过滤器最好定义为局部的,而非全局。使用局部过滤器的好处是针对性好,全局的话你用一个万能异常处理好像过于简单。当然了,如果你的项目可以这样做,就随便用。

这里我定义一个局部的异常过滤器,通过特性方式应用到操作方法上。

复制代码
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CustExceptionFilterAttribute : Attribute, IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        // 看看有没有异常
        if (context.Exception is null || context.ExceptionHandled)
            return;
        // 有异常哦,修改一下返回结果
        ContentResult result = new();
        result.Content = "出错啦,伙计。错误消息:" + context.Exception.Message;
        // 设置返回结果
        context.Result = result;
        // 标记异常已处理
        context.ExceptionHandled = true;
    }
}
复制代码

首先,context 参数是一种上下文对象,它在多上异常过滤器间共享实例(你可能实现了很多个异常过滤器)。context.Exception 属性引用的是异常类对象;注意一个有趣的属性 ExceptionHandled,它是一个 bool 类型的值,表示“我这个异常过滤器是否已处理过了”。这个有啥用呢?由于上下文是共享的,当你的某个异常过滤器设置了 ExceptionHandled 为 true,那么,其他异常过滤器也可以读这个属性。这样就可以实现:啊,原来有人处理过这个异常了,那我就不处理了。即 A 过滤器处理异常后设置为已处理,B 过滤器可以检查这个属性的值,如果没必要处理就跳过。

下面写一个控制器,并在方法成员上应用前面定义的异常过滤器。

复制代码
public class AbcController : ControllerBase
{
    [HttpGet("calc/add"), CustExceptionFilter]
    public int Add(int x, int y)
    {
        int r = x + y;
        if(r >= 1000)
        {
            throw new Exception("计算结果必须在1000以内");
        }
        return r;
    }
}
复制代码

此处我设定抛出异常的条件是:x、y 相加结果大于或等于 1000。

咱们以 GET 方式调用,URL 为 /calc/add?x=599&y=699,这样会发生异常。服务器的响应为:

 

最后一个例子是结果过滤器。咱们要实现在响应消息中添加自定义 Cookie。

复制代码
public class SetCookieResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        // 不能在这里写 Cookie
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        HttpResponse response = context.HttpContext.Response;
        // 设置Cookie
        response.Cookies.Append("X-VER", "3.0");
    }
}
复制代码

这里要注意,咱们要写 Cookie 必须在 OnResultExecuting 方法中处理,不能在 OnResultExecuted 方法中写。因为当 OnResultExecuted 方法执行时,响应消息头已经被锁定了,Cookie 是通过 set-cookie 标头实现的,此时无法修改 HTTP 头了,写 Cookie 会抛异常。所以只能在 OnResultExecuting  方法中写。

定义一个控制器。

public class DemoController : ControllerBase
{
    [HttpGet("abc/test")]
    public string Work() => "山重水复疑无路";
}

将自定义的结果过滤器添加为全局。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
    options.Filters.Add<SetCookieResultFilter>();
});
var app = builder.Build();

好,试试看。

 

Layui实现图片列表并且可以放大查看 - Core、陈 - 博客园

mikel阅读(400)

来源: Layui实现图片列表并且可以放大查看 – Core、陈 – 博客园

首先建一个DIV层

复制代码
 1 <div class="layui-row layui-col-space10">
 2     <div class="layui-col-md12">
 3         <div class="layui-card">
 4             <div class="layui-card-body">
 5                 <fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
 6                     <legend>图片列表</legend>
 7                 </fieldset>
 8                 <div class="layui-row layui-col-space30" style="height: 300px; overflow:auto" id="LAY_Images">                            
 9                 </div>
10             </div>
11         </div>
12     </div>
13 </div>
复制代码

 

然后写一个数据请求的方法

复制代码
 1          //请求图像数据
 2                 $.ajax({
 3                     url: "接口路径",
 4                     data: { 'Id': 1 },
 5                     type: "post",
 6                     dataType: "json",
 7                     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 8                     success: function (data) {
 9                         $("#LAY_Images").empty();
10                         $.each(data.data, function (index, item) {
11                             $("#LAY_Images").append(
12                                 "<div class='layui-col-md2 ew-datagrid-item'>" +
13                                 "<div class='project-list-item'>" +
14                                 "<img class='project-list-item-cover' src='" + item.imgname + "' onclick='previewImg(this)' />" +
15                                 "</div>" +
16                                 "</div>"
17                             );
18                         })
19                         form.render($('#LAY_Images'));
20                     },
21                     error: function (data) {
22 
23                     }
24                 });
复制代码

 

这样就渲染好图片的列表了,如果不想要放大功能,去掉onclick事件就可以了,如果需要,加上下面的方法

复制代码
 1      //点击图片放大查看
 2         function previewImg(obj) {
 3             var img = new Image();
 4             img.src = obj.src;
 5             var height = img.height; //获取图片高度
 6             var width = img.width; //获取图片宽度
 7             if (height > 1000 || width > 800) {
 8                 height = height / 1.5;
 9                 width = width / 1.5;
10             }
11             var imgHtml = "<img src='" + obj.src + "' style='width: " + width + "px;height:" + height + "px'/>";
12             //弹出层
13             layer.open({
14                 type: 1,
16                 offset: 'auto',
17                 area: [width + 'px', height + 'px'],
18                 shadeClose: true,//点击外围关闭弹窗
19                 scrollbar: true,//不现实滚动条
20                 title: false, //不显示标题
21                 content: imgHtml, //捕获的元素,注意:最好该指定的元素要存放在body最外层,否则可能被其它的相对元素所影响
22                 cancel: function () {
23                     
24                 }
25             });
26         }
复制代码

这样基本就可以做出一个图片列表框出来了

 

原生js使用FileReader将文件转成base64_js filereader base64-CSDN博客

mikel阅读(305)

来源: 原生js使用FileReader将文件转成base64_js filereader base64-CSDN博客

一、FileReader介绍
FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容。可以使用File对象指定要读取的文件。

File对象可以是来自用户在一个type类型为file的input元素上选择文件后返回的FileList对象。

<input type=”file” name=”” id=””>

注意: FileReader仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中直接使用路径名简单地读取文件。

二、FileReader的使用
FileReader是一个构造函数,需要new之后拿到FileReader的实例。

var reader = new FileReader();
1
实例的方法:
reader.abort():中止读取操作。
reader.readAsArrayBuffer():读取完成后,触发onload事件,result 属性中保存的是文件的 ArrayBuffer 数据对象。
reader.readAsDataURL():读取完成后,触发onload事件,result属性中保存的是Base64文件内容。
reader.readAsText():读取完成后,触发onload事件,result属性中保存的是字符串文件内容。

实例的事件:

reader.onabort:在读取操作被中断时触发。
reader.onerror:在读取操作发生错误时触发。
reader.onload:在读取操作完成时触发。
reader.onloadstart:在读取操作开始时触发。
reader.onloadend:在读取操作结束时(要么成功,要么失败)触发。
实例的属性:

reader.error:返回读取文件时的错误信息
reader.readyState:表示FileReader状态的数字。0:还没有加载;1:正在加载;2:加载完成
reader.result:文件的内容
三、使用范例
obtn.onclick = function(){

var reader = new FileReader();
reader.readAsDataURL(f.files[0]); // 解析成base64格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

obtn.onclick = function(){

var reader = new FileReader();
reader.readAsArrayBuffer(f.files[0]);// 解析成ArrayBuffer格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

obtn.onclick = function(){

var reader = new FileReader();
reader.readAsText(f.files[0]); // 解析成Text格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

以上,如有错误,请留言交流…
————————————————
版权声明:本文为CSDN博主「杨树林er」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_41636483/article/details/112589158

【Windows系统】查看和关闭139、445端口的方法_关闭139端口_jin_study的博客-CSDN博客

mikel阅读(318)

来源: 【Windows系统】查看和关闭139、445端口的方法_关闭139端口_jin_study的博客-CSDN博客

文章目录
前言
一、 Windows查看139、445端口的方法
二、关闭445端口的方法
三、关闭139端口的方法
前言
“航天派”公众号上一期文章介绍了“麒麟操作系统查看和关闭139、445端口的方法”,今天介绍“Windows系统查看和关闭139、445端口的方法”。

前期,尝试使用一些软件关闭139、445端口,结果效果不理想;尝试使用网络上的一些操作方法,效果也不佳。今天介绍的关闭端口方法是经过多次实践验证过的,是真实有效的。需要提前说明,使用本文关闭139、445端口的方法,要在操作完成之后进行系统重启才能生效。

一、 Windows查看139、445端口的方法
按Windows+R键,弹出运行窗口,输入cmd回车,然后在命令提示符窗口输入如下命令,查看139、445端口是否存在:

netstat -na | findstr 139
netstat -na | findstr 445
1
2
如下图,可以看到该计算机TCP协议下均存在139、445端口,且处于LISTENING状态,接下来将封闭该计算机的139、445端口。

二、关闭445端口的方法
(1)在命令提升符号窗口接着输入regedit,将弹出注册表编辑器。

(2)在注册表编辑器找到HKEY_LOCAL_MACHINE—>SYSTEM—>ControlSet001—>services,如下图。

(3)在services中,找到NetBT—>Parameters,然后选中Parameters右键新建DWORD(32-位)值,如下图。

(4)将新建的DWORD命名为“SMBDericeEnabled”,并将其值修改为0(默认就是0,可以不用改),如下图。

(5)打开控制面板,选择管理工具—>服务—>Server—属性,如下图。

(6)在属性窗口将启动类型改为“禁用”,将服务状态选为“暂停”,然后点击应用、确定即可,如下图。

(7)完成以上操作之后,重启计算机,再次使用netstat -na | findstr 445查询,发现445端口已经关闭。当然可以等关闭完139端口之后,一起重启计算机,下面介绍关闭139端口的方法。

三、关闭139端口的方法
关闭139端口的方法,相比关闭445端口的方法,要简单很多。

(1)通过控制面板或者直接双击状态栏中网络连接图标,打开“网络和共享中心”,选择“更改适配器设置”—>本地连接—>属性,然后选择Internet协议版本4(TCP/IPv4)—>高级—>WINS—>禁用TCP/IP上的NetBIOS(S),然后点击确定即可,如下图。

(2)完成以上操作之后,重启计算机,然后在命令提示符窗口输入netstat -na | findstr 139、netstat -na | findstr 445查看139、445端口是否已关闭。如下图,经查询,139、445端口已经不存在,已经被关闭。

以上介绍了Windows系统查看和关闭139、445端口的方法,是经过多次实践验证过的,方法靠谱,值得分享。
关注“航天派”微信公众号,了解以下更好实用内容:
————————————————
版权声明:本文为CSDN博主「jin_study」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jin_study/article/details/124198383

MVC将Base64 保存为图片 - 跑着的小强 - 博客园

mikel阅读(352)

来源: MVC将Base64 保存为图片 – 跑着的小强 – 博客园

前台传来Base64字符串。本来可以直接保存数据库返回给图片 但是这样对数据库负担太重 传输也费时间。一搬都是存个地址在数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ActionResult Injpg(string base64str ) { 
        string data=base64str//要处理下字符串 ,之前的要截取掉 不然会报错
            byte[] arr = Convert.FromBase64String(data);
            using (MemoryStream ms = new MemoryStream(arr))
            {
                Bitmap bmp = new Bitmap(ms);
                string p = "/text.jpg";
                var w = Server.MapPath(p);
                bmp.Save(w, System.Drawing.Imaging.ImageFormat.Jpeg);
                //bmp.Save(@"d:\"test.bmp", ImageFormat.Bmp);
                //bmp.Save(@"d:\"test.gif", ImageFormat.Gif);
                //bmp.Save(@"d:\"test.png", ImageFormat.Png);
                ms.Close();
                return Content(p);
            }

解锁新技能 哈哈哈 不用后台压缩了,,,整理下代码  搭建微动态数据库表了