记一次 .NET 某餐饮小程序 内存暴涨分析 - 一线码农 - 博客园

mikel阅读(274)

来源: 记一次 .NET 某餐饮小程序 内存暴涨分析 – 一线码农 – 博客园

一:背景

1. 讲故事

前些天有位朋友找到我,说他的程序内存异常高,用 vs诊断工具 加载时间又太久,让我帮忙看一下到底咋回事,截图如下:

确实,如果dump文件超过 10G 之后,市面上那些可视化工具分析起来会让你崩溃的,除了时间久之外这些工具大多也不是用懒加载的方式,比如 dotmemory 会把数据全部灌入内存,针对这种dump,你没个32G内存就不要分析了,这也是 windbg 在此类场景下的用武之地。

闲话不多说,朋友的dump到了,赶紧分析一波。

2. 到底是谁吃了内存

还是那句话,用 !address -summary 看下是托管内存还是非托管内存的问题。


0:000> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                    366     7dbf`3e6cb000 ( 125.747 TB)           98.24%
<unknown>                              5970      240`99b78000 (   2.252 TB)  99.97%    1.76%
Stack                                   159        0`136a0000 ( 310.625 MB)   0.01%    0.00%
Image                                  1943        0`0a2e8000 ( 162.906 MB)   0.01%    0.00%
Heap                                     89        0`0a1e0000 ( 161.875 MB)   0.01%    0.00%
Other                                    12        0`001da000 (   1.852 MB)   0.00%    0.00%
TEB                                      53        0`0006a000 ( 424.000 kB)   0.00%    0.00%
PEB                                       1        0`00001000 (   4.000 kB)   0.00%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                366     7dbf`3e6cb000 ( 125.747 TB)           98.24%
MEM_RESERVE                             608      23d`fda87000 (   2.242 TB)  99.52%    1.75%
MEM_COMMIT                             7619        2`c3e9e000 (  11.061 GB)   0.48%    0.01%

从卦中看 ntheap=161M,看样子是托管堆的问题了,继续使用 !eeheap -gc 看下托管堆。


0:000> !eeheap -gc
Number of GC Heaps: 8
------------------------------
Heap 0 (00000277134AD330)
Small object heap
         segment             begin         allocated         committed    allocated size    committed size
generation 0:
000002B727864BB0  00000279A4000020  00000279A43FFFD0  00000279A4400000  0x3fffb0(4194224)  0x400000(4194304)
000002B727869500  00000279BD800020  00000279BDBFFF70  00000279BDC00000  0x3fff50(4194128)  0x400000(4194304)
...
000002B727852950  000002793F000020  000002793F3FFFA0  000002793F400000  0x3fff80(4194176)  0x400000(4194304)
000002B727853080  0000027941800020  00000279419B6FA0  00000279419C1000  0x1b6f80(1798016)  0x1c1000(1839104)
Frozen object heap
         segment             begin         allocated         committed    allocated size    committed size
Large object heap
         segment             begin         allocated         committed    allocated size    committed size
000002B7277F53C0  0000027737800020  00000277378580A8  0000027737879000  0x58088(360584)  0x79000(495616)
Pinned object heap
         segment             begin         allocated         committed    allocated size    committed size
000002B7277F1480  0000027721800020  0000027721833A80  0000027721841000  0x33a60(211552)  0x41000(266240)
Allocated Heap Size:       Size: 0x4e17d578 (1310184824) bytes.
Committed Heap Size:       Size: 0x4effd000 (1325387776) bytes.
------------------------------
GC Allocated Heap Size:    Size: 0x280020b18 (10737552152) bytes.
GC Committed Heap Size:    Size: 0x28835f000 (10875170816) bytes.

我去,一下子刷了好几屏,从卦中可以看到内存占用高达 10G+, 往细处看都是 Small object heap 给吃掉了,既然是SOH堆,看样子都是热和着呢,潜台词就是他们的根很可能在线程栈里,经验之谈哈。

有了这些猜测,接下来观察下托管堆,看看谁的占比最大,使用 !dumpheap -stat 即可。


0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
...
00007ffc41beaa68     4894      1732200 System.Object[]
00007ffc41fc0468     7058      2368001 System.Byte[]
00007ffc41dbf7b8    24209      2517736 System.Reflection.RuntimeMethodInfo
00007ffc43429178        3    536870984 xxxLogEntity[]
000002771340e900 46106634   1866065488      Free
00007ffc41c6fd10 55920839   2125832534 System.String
00007ffc42ddc0b8 50634021   6076082520 xxxxxxxLogEntity

不看不知道,一看吓一跳,这 xxxxxxLogEntity 对象居然高达 5063w,占据着 6G 的内存,那为什么会有这么多的对象呢?用 !gcroot 抽几个看看便知。


0:000> !dumpheap -mt 00007ffc42ddc0b8
         Address               MT     Size
00000279a405b010 00007ffc42ddc0b8      120    
...
00000279c31648a0 00007ffc42ddc0b8      120     
00000279c3164968 00007ffc42ddc0b8      120     
00000279c3164a30 00007ffc42ddc0b8      120     
00000279c3164af8 00007ffc42ddc0b8      120     
00000279c3164bc0 00007ffc42ddc0b8      120     
00000279c3164c88 00007ffc42ddc0b8      120     
00000279c3164d50 00007ffc42ddc0b8      120     

0:000> !gcroot 00000279c3164d50
Thread a65c:
    0000009BA592BD80 00007FFC458F99C8 xxx+<xxx>d__14.MoveNext()
        rbx: 
            ->  0000027723C9B8F8 System.Collections.Generic.List`1[[xxx]]
            ->  00000278F2000040 xxxxxxLogEntity[]
            ->  00000279C3164D50 xxxxxxLogEntity

Found 1 unique roots (run '!gcroot -all' to see all roots).

0:000> !do 0000027723C9B8F8
Name:        System.Collections.Generic.List`1[[xxx]]
MethodTable: 00007ffc43024ec0
EEClass:     00007ffc41d956b0
Tracked Type: false
Size:        32(0x20) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.4\System.Private.CoreLib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffc420fac80  4002149        8     System.__Canon[]  0 instance 00000278f2000040 _items
00007ffc41bee8d0  400214a       10         System.Int32  1 instance         50634020 _size
00007ffc41bee8d0  400214b       14         System.Int32  1 instance         50634020 _version
00007ffc420fac80  400214c        8     System.__Canon[]  0   static dynamic statics NYI 

从卦象中可以看到,这 5063w 个对象都被这个 list 持有,更有意思的是果然被我猜到了,这个list的根在 a65c 这个线程里,接下来的问题是这个线程正在做什么?

3. a65c 线程正在做什么

要想看这个神秘线程正在做什么,可以用 ~ 命令切过去看看线程栈,看看哪一个方法在引用这个 list。


0:036> ~~[a65c]s
00007ffc`451fefe6 482bc2          sub     rax,rdx

0:036> !clrstack -a
OS Thread Id: 0xa65c (36)
0000009BA592BD80 00007ffc458f99c8 xxxxBase+d__14.MoveNext()
    PARAMETERS:
        this (<CLR reg>) = 0x0000027723c515b8
    LOCALS:
        <no data>
        <CLR reg> = 0x00000277287cd6d8
        <no data>
        <no data>
        ...
        <no data>
        <CLR reg> = 0x0000027723c9b8f8
        <no data>

找到了是 xxxxBase+d__14.MoveNext 方法之后,接下来就需要仔细研读代码,终于找到了,写了一个死循环,真是无语了,截图如下:

终于真相大白,程序员误以为使用 dateTime.AddDays(1.0); 就可以修改 dateTime 的时间,犯了一个低级错误呀。

改成 dateTime=dateTime.AddDays(1.0); 即可。

三:总结

这次内存暴涨把生产服务器弄崩了,就是因为这么个 低级错误导致实属不应该,本以为程序员不会写出什么死循环,还真的遇到了,提高开发人员的代码敏感性迫在眉睫。

如何用Fetch API从Vue组件中获取API数据(附代码示例) - 掘金

mikel阅读(489)

来源: 如何用Fetch API从Vue组件中获取API数据(附代码示例) – 掘金

如果你是一个网络开发者,你很有可能在某些时候不得不从API中获取数据。如果你使用的是Vue框架,你可能想知道如何在Vue组件中获取数据并保存它。

幸运的是,使用本地的Fetch API,这很容易做到。在这篇文章中,我们将学习如何用Options和Composition API来做,这是编写Vue组件的两种官方方式。

如果你想了解如何将用Vue构建的前端和外部API连接起来,请继续阅读!

为什么使用Fetch API?

如果你不熟悉fetch API,它是一种在JavaScript中进行异步HTTP请求的方法。从本质上讲,它允许你从外部来源获取数据,然后在你的代码中对这些数据做一些处理。

fetch API在大多数现代浏览器中都是可用的,但如果你使用的是旧的浏览器,你可能需要使用一个polyfill。

要使用Fetch API,我们首先需要创建一个Request对象。这个对象将接受一个url和options对象作为参数。url参数是我们指定我们想要调用的API的端点的地方。选项对象是我们可以指定诸如HTTP方法(如GET或POST)、头文件、正文等的地方。

一旦我们有了我们的Request对象,我们就可以调用本地的 获取方法,它将返回一个Promise。这个Promise将以一个Response对象来解决。响应对象有一个json()方法,我们可以调用它来获得我们想要的JSON数据。

这就是一个简单的GET调用在实践中的样子:

JavaScript

复制代码
fetch("https://api.example.com/users")
.then(res => res.json())
.then(data => console.log(data))

这将简单地在我们的本地控制台显示API数据。

然而,你如何在Vue组件中存储和使用这些数据?让我们通过创建一个非常简单的例子来学习如何做到这一点:一个调用JSON占位符API并显示我们从特定端点检索到的项目列表的应用程序。

让我们直接进入,看看如何使用Options API调用一个API。

使用选项语法调用API

正如你可能知道的,Options语法是创建Vue组件的传统方式。在这里,你需要根据你的组件中的数据类型来声明一些选项(因此而得名)。

让我们看看我们如何从API中获取数据,并在用选项语法编写的组件中使用它。

首先,我们需要创建一个新的Vue实例并定义一个变量。我们将它初始化为一个空数组,因为我们将在这里添加我们从JSON占位符API中获取的所有信息:

markup

复制代码
<script>
  export default {
    data() {
      return {
        listItems: []
      }
    }
  }
</script>

然后,在我们的方法部分,我们将创建一个方法,从实际的API中获取数据。在这种情况下,我们将使用Async/Await语法来增加清晰度:

markup

复制代码
<script>
  export default {
    data() {
      return {
        listItems: []
      }
    },
    methods: {
      async getData() {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts");
        const finalRes = await res.json();
        this.listItems = finalRes;
      }
    }
  }
</script>

你可能已经注意到了,在最后一步中,我们已经将API响应直接保存在我们之前生成的变量中。这意味着数据现在在我们的组件中是全局可用的,我们可以在我们的模板中使用v-for语法渲染它。

markup

复制代码
<template>
  <div v-for="item in listItems">
    {{item.title}}
  </div>
</template>

你的HTML没有显示任何东西吗?在我们的应用程序真正显示任何东西之前,我们必须调用 getData方法。我们可以在加载我们的组件时使用 装入钩子:

markup

复制代码
<script>
  export default {
    data() {
      return {
        listItems: []
      }
    },
    methods: {
      async getData() {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts");
        const finalRes = await res.json();
        this.listItems = finalRes;
      }
    },
    mounted() {
      this.getData()
    }
  }
</script>

就这样了!我们现在有一个列表,显示我们从外部API获取的数据。

使用组合句法调用一个API

使用组合API调用外部数据源,在某些方面甚至比使用选项更容易。我们唯一需要改变的是我们的

下面是同样的代码在使用Composition API时的样子。注意我们使用的是

markup

复制代码
<script setup>
  import { ref } from 'vue';

  const listItems = ref([]);

  async function getData() {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts");
    const finalRes = await res.json();
    listItems.value = finalRes;
  }

 getData()
</script>

很简单,是吧?正如你所看到的,使用Fetch API在Vue项目中获取数据实际上是非常容易实现的。我们希望这篇文章能帮助你

作者:Jovie
链接:https://juejin.cn/post/7176277379168337979
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

.NET Core appsettings.json 获取数据库连接字符串_.net core sql connection string_枯叶轮回的博客-CSDN博客

mikel阅读(407)

来源: .NET Core appsettings.json 获取数据库连接字符串_.net core sql connection string_枯叶轮回的博客-CSDN博客

本文主要介绍.NET Core中,通过appsettings.json配置文件获取数据库连接字符串。

 

1、在项目的根目录中创建appsettings.json文件

{
  "MssqlConnectionString": "Server=yourip; Database=yourdbname; User Id=yourusername; Password=yourpassword; Pooling=true;",
  "Db2ConnectionString": "Database=yourdbname;UserID=yourusername;Password=yourpassword;Server=yourip:yourport",
  "SomeOtherKey": "SomeOtherValue"
}

2、安装Microsoft.Extensions.Configuration.Json的Nuget包

Install-Package Microsoft.Extensions.Configuration.Json -Version 2.2.0

3、添加AppSettingsJson类

using Microsoft.Extensions.Configuration;
using System.IO;
namespace RutarBackgroundServices.AppsettingsJson
{
    public static class AppSettingsJson
    {
        public static string ApplicationExeDirectory()
        {
            var location = System.Reflection.Assembly.GetExecutingAssembly().Location;
            var appRoot = Path.GetDirectoryName(location);
            return appRoot;
        }
        public static IConfigurationRoot GetAppSettings()
        {
            string applicationExeDirectory = ApplicationExeDirectory();
            var builder = new ConfigurationBuilder()
            .SetBasePath(applicationExeDirectory)
            .AddJsonFile("appsettings.json");
            return builder.Build();
        }

    }
}

4、使用AppSettingsJson获取连接字符串

var appSettingsJson = AppSettingsJson.GetAppSettings();
//方法一
var connectionString = appSettingsJson.GetConnectionString("MssqlConnectionString");
//方法二
var connectionString = appSettingsJson["MssqlConnectionString"];

.NET Core WebApi中实现数据库的操作(之SqlServer)_c# webapi数据库操作通用api_牛奶咖啡13的博客-CSDN博客

mikel阅读(358)

来源: .NET Core WebApi中实现数据库的操作(之SqlServer)_c# webapi数据库操作通用api_牛奶咖啡13的博客-CSDN博客

3.3.3、具体的业务逻辑
①设计业务的数据库表

 

②创建业务表的实体类

其中创建好私有的字段属性后,可以选中所有的私有字段,然后同时按下【Ctrl+R+E】键即可一次性自动生成所有字段的公有属性内容。

关于创建的实体中,如果你的属性名称与表中的字段不一致,则需要标识出来,具体内容请参考:从零开始 – 实体配置 – 《SQLSugar 5.0 文档》

/***
* Title:”.NET Core WebApi” 项目
* 主题:学生实体类
* Description:
* 功能:XXX
* Date:2021
* Version:0.1版本
* Author:Coffee
* Modify Recoder:
*/

using SQLSugar;
using System;
using System.Collections.Generic;
using System.Text;

namespace WebApiEntity
{
[SugarTable(“Test_Student”)]
public class StudentEntity
{

private int _iD = 0;
private string _name = string.Empty;
private string _number = string.Empty;
private int _age = 0;
private int _sex = 0;
private string _address = string.Empty;

/// <summary>
/// 主键
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int ID { get => _iD; set => _iD = value; }

/// <summary>
/// 姓名
/// </summary>
public string Name { get => _name; set => _name = value; }

/// <summary>
/// 学号
/// </summary>
public string Number { get => _number; set => _number = value; }

/// <summary>
/// 年龄
/// </summary>
public int Age { get => _age; set => _age = value; }

/// <summary>
/// 性别
/// </summary>
public int Sex { get => _sex; set => _sex = value; }

/// <summary>
/// 家庭住址
/// </summary>
[SugarColumn(ColumnName = “Test_Address”)]
public string Address { get => _address; set => _address = value; }

}//Class_end

}
③创建业务的接口服务(方便业务扩展)

/***
* Title:”.NET Core WebApi” 项目
* 主题:学生服务接口
* Description:
* 功能:
*
* Date:2021
* Version:0.1版本
* Author:Coffee
* Modify Recoder:
*/

using System;
using System.Collections.Generic;
using System.Text;
using WebApiService.Common;
using WebApiEntity;

namespace WebApiService.Interfaces
{
public interface IStudentService : IBaseDbService<StudentEntity>
{
/// <summary>
/// 测试
/// </summary>
void Test();

}//Class_end

}
④实现业务服务

/***
* Title:”.NET Core WebApi” 项目
* 主题:学生服务
* Description:
* 功能:XXX
* Date:2021
* Version:0.1版本
* Author:Coffee
* Modify Recoder:
*/

using SQLSugar;
using System;
using System.Collections.Generic;
using System.Text;
using WebApiEntity;
using WebApiService.Common;
using WebApiService.Interfaces;
using WebApiUtils;

namespace WebApiService.Implements
{
class StudentService : BaseDbService<StudentEntity>, IStudentService
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name=”dbType”>数据类类型</param>
public StudentService(DbType dbType=DbType.SQLServer):base(dbType)
{

}

/// <summary>
/// 测试
/// </summary>
public void Test()
{
LogHelper.Debug($”this is { this.GetType().Name} 测试”);
}
}//Class_end

}
⑤实现具体的业务逻辑内容

/***
* Title:”.NET Core WebApi” 项目
* 主题:测试学生服务【数据库操作】
* Description:
* 功能:XXX
* Date:2021
* Version:0.1版本
* Author:Coffee
* Modify Recoder:
*/

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApiEntity;
using WebApiService.Interfaces;
using WebApiUtils.Entity;

namespace WebApi_Learn.Controllers.Test
{
[Route(“api/[controller]/[action]”)]
[ApiController]
public class Test_Db_StudentService
{
#region 私有方法
//学生的接口服务
private readonly IStudentService _studentService;

#endregion

#region 公有方法

/// <summary>
/// 构造函数
/// </summary>
/// <param name=”studentService”>学生服务接口</param>
public Test_Db_StudentService(IStudentService studentService)
{
this._studentService = studentService;
}

/// <summary>
/// 插入数据
/// </summary>
/// <param name=”studentEntity”>学生实体</param>
/// <returns>返回当前学生的ID</returns>
[HttpPost, Route(“AddInfo”)]
public ActionResult<int> Insert(StudentEntity studentEntity)
{
int id = _studentService.Insert(studentEntity);

return id;
}

/// <summary>
/// 修改数据
/// </summary>
/// <param name=”studentEntity”>学生实体</param>
/// <returns>返回修改结果</returns>
[HttpPost, Route(“UpdateInfo”)]
public ActionResult<bool> Update(StudentEntity studentEntity)
{
return _studentService.Update(studentEntity);
}

/// <summary>
/// 删除数据(根据主键)
/// </summary>
/// <param name=”id”>主键</param>
/// <returns></returns>
[HttpGet]
public ActionResult<bool> Delete(int id)
{
return _studentService.Delete(id, true);
}

/// <summary>
/// 查询数据(单条数据)
/// </summary>
/// <param name=”field”>过滤字段</param>
/// <param name=”fieldValue”>过滤字段对应的值</param>
/// <returns></returns>
[HttpGet]
public ActionResult<StudentEntity> QuaryData(string field,int fieldValue)
{
StudentEntity studentEntity = new StudentEntity();

//显示字段
string strFields = “Name,Age,Test_Address”;

//根据条件查询到数据
SqlFilterEntity sqlFilterEntity = new SqlFilterEntity();
sqlFilterEntity.Append($”{field}=@{field}”);
sqlFilterEntity.Add(field,fieldValue);

studentEntity = _studentService.GetEntity(strFields, sqlFilterEntity);

return studentEntity;
}

/// <summary>
/// 查询数据(多条数据)
/// </summary>
/// <param name=”field”>字段</param>
/// <param name=”fieldValue”>字段对应的值</param>
/// <returns></returns>
[HttpGet]
public ActionResult<List<StudentEntity>> QuaryDatas(string field, string fieldValue)
{
List<StudentEntity> studentEntityList = new List<StudentEntity>();

//根据条件查询到数据
SqlFilterEntity sqlFilterEntity = new SqlFilterEntity();
sqlFilterEntity.Append($”{field}=@{field}”);
sqlFilterEntity.Add(field, fieldValue);
studentEntityList= _studentService.GetList(null, sqlFilterEntity);

return studentEntityList;
}

/// <summary>
/// 获取开始数据
/// </summary>
/// <returns></returns>
[HttpGet]
public List<StudentEntity> GetStartDatas()
{
List<StudentEntity> studentEntities = new List<StudentEntity>();
return _studentService.GetStartList(2);

}

/// <summary>
/// 分页查看
/// </summary>
/// <param name=”pageIndex”></param>
/// <param name=”pageSize”></param>
/// <param name=”strOrder”></param>
/// <returns></returns>
[HttpPost]
public List<StudentEntity> GetPageList(int pageIndex, int pageSize,
string strOrder=”Age DESC”)
{
//显示字段
string strField = “ID,Name,Age”;

//过滤条件
SqlFilterEntity sqlFilterEntity = new SqlFilterEntity();
sqlFilterEntity.Append($”Age>@Age”);
sqlFilterEntity.Add(“Age”, 21);

int totalCount=0;

return _studentService.GetPageList(pageIndex,pageSize, strField, sqlFilterEntity,strOrder,out totalCount);

}

#endregion

}//Class_end
}
3.3.4、服务的依赖注入
主要实现统一管理业务的接口与实现服务的对应关系。

/***
* Title:”.NET Core WebApi” 项目
* 主题:服务的依赖注入
* Description:
* 功能:XXX
* Date:2021
* Version:0.1版本
* Author:Coffee
* Modify Recoder:
*/

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
using WebApiService.Implements;
using WebApiService.Interfaces;

namespace WebApiService.Common.Depends
{
public class ServiceInjection
{
public static void ConfigureRepository(IServiceCollection services)
{
services.AddSingleton<IStudentService, StudentService>();

}

}//Class_end

}
对于依赖注入的简要补充如下所示:

方法 说明
Transient 每一次调用都会创建一个新的实例
Scoped 一个作用域中只实例化一个
Singleton 整个应用程序生命周期以内只创建一个实例
①在Startup类中【ConfigureServices】方法中注册【服务的依赖注入】

 

3.3.5、运行程序执行测试
比如:只显示【名称,年龄、地址】内容,查看条件是【字段为:ID;且该ID字段的值为:2021003165】信息:

 

 

参考文章:

①net core Webapi基础工程搭建(六)——数据库操作_Part 1

②net core Webapi基础工程搭建(六)——数据库操作_Part 2
————————————————
版权声明:本文为CSDN博主「牛奶咖啡13」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaochenXIHUA/article/details/119574119

vue中给buttion按钮添加键盘回车(enter)事件_vue 回车 button_依旧平凡的博客-CSDN博客

mikel阅读(451)

来源: vue中给buttion按钮添加键盘回车(enter)事件_vue 回车 button_依旧平凡的博客-CSDN博客

项目中遇到点击查询按钮可以查出数据,点击回车键也能查出数据,所以就想点击回车键时调用查询方法。

以下代码可实现此功能。

首先,button上有click事件,点击可实现搜索查询;

created(){}函数里面调用回车按下的事件方法

关键的地方就是按下回车键的方法

methods:{
keyupEnter(){
document.onkeydown = e =>{
let body = document.getElementsByTagName('body')[0]
if (e.keyCode === 13 && e.target.baseURI.match(/inputbook/) && e.target === body) {
console.log('enter') // match(此处应填写文件在浏览器中的地址,如 '/home/index'),不写的话,其他页面也会有调用回车按下的方法
this.handleAddBook() //调用查询方法
}
}
},
handleAddBook(){

}
}

 

示例代码如下:

一、第一步: 给button按钮绑定@keyup.enter

  1. <div class=“btn”> <!–如果是封装过的按钮,不是原生的按钮,需要加上.native才能生效–>
  2. <Button type=“primary” @click=“handleAddBook” @keyup.enter.native=“handleAddBook”>录入</Button>
  3. </div>

 

二、第二步:浏览器url:event.target.baseURI; 获取浏览器的路径地址

  1. // 创建时
  2. created(){
  3. this.keyupEnter()//页面在创建时就调用键盘的回车事件,在结构代码中也可以不写@keyup.enter.native=”handleAddBook”
  4. },
  5. methods:{
  6. keyupEnter(){
  7. document.onkeydown = e =>{
  8. let body = document.getElementsByTagName(‘body’)[0]
  9. if (e.keyCode === 13 && e.target.baseURI.match(/inputbook/) && e.target === body) {
  10. console.log(‘enter’) // match(此处应填写文件在浏览器中的地址,如 ‘/home/index’)
  11. this.handleAddBook()
  12. }
  13. }
  14. },
  15. handleAddBook(){
  16. if(this.validate()){
  17. this._printQrcode()
  18. }
  19. }
  20. }

 

转载于:https://www.cnblogs.com/wangdashi/p/9646219.html

canvas如何监听键盘事件 - 掘金

mikel阅读(463)

来源: canvas如何监听键盘事件 – 掘金

对于canvas元素,它支持JavaScript所有鼠标事件,但是如果监听键盘事件则并不会生效。

JavaScript

复制代码
// 有效
canvas.addEventListener('click', (e) => {
  console.log('触发点击了')
})

// 无效
canvas.addEventListener('keydown', (e) => {
  console.log('触发按键了')
})

其原因在于,键盘输入事件只发生在当前拥有焦点的HTML元素上,如果没有元素拥有焦点,那么事件将会上移至windows和document对象,所以有两种常用方法来解决这个问题:

  1. 如果canvas元素和windows长宽基本一致,可以通过在windows对象上绑定键盘事件来代替对canvas元素的监听与处理。
    javascript

    复制代码
    window.addEventListener('keydown', doKeyDown, true)
    
  2. 让canvas元素处于聚焦状态,并给它绑定键盘事件
    html

    复制代码
    <canvas tabindex="0"></canvas>
    

    tabindex设置为0或更大。

下面通过示例详细的介绍第二种方法:

html

复制代码
<!-- html部分 -->
<canvas id="canvas" tabindex="0"></canvas>
javascript

复制代码
// js部分
const canvas = document.getElementById('canvas')
canvas.focus()
canvas.addEventListener('keydown', (e) => {
  console.log(`keyCode: ${e.keyCode}`)
})

这样就可以让canvas在一开始处于聚焦状态,并相应键盘输入事件。

不过tabindex聚焦的元素会有一层默认的外框,标识该元素处于聚焦状态。如果不想要显示外框,可以通过css样式去除:

css

复制代码
canvas:focus {
  outline:none;
}

可以写一个实际应用来测试,比如用键盘的上下左右或者wsad键操作一个小方块,在canvas画布中移动。

image-20210527180217366

html

复制代码
<!-- html部分 -->
<canvas id="canvas" tabindex="0"></canvas>

<!-- css部分 -->
<style>
  #canvas{
    width: 100vw;
    height: 100vh;
    background-color: #4ab7bd;
  }
  #canvas:focus{
    outline: none;
  }
</style>

<!-- js部分 -->
<script>
window.onload = function() {
  // 画布的长宽
  const canvas = document.getElementById('canvas')
  const canvasWidth = canvas.clientWidth
  const canvasHeight = canvas.clientHeight
  // 在画布上移动的方块的长宽
  const [rectWidth, rectHeight] = [40, 40]
  // 方块的横纵坐标
  let [rectX, rectY] = [0, 0]
  // 初始化
  canvas.width = canvasWidth
  canvas.height = canvasHeight
  let context = canvas.getContext('2d')
  // 给方块设置颜色和初始坐标(中心点),绘制
  context.fillStyle = 'red'
  rectX = (canvasWidth - rectWidth) / 2
  rectY = (canvasHeight - rectHeight) / 2
  context.fillRect(rectX, rectY, rectWidth, rectHeight)

  // canvas元素上监听键盘输入事件
  canvas.addEventListener('keydown', doKeyDown, true)
  canvas.focus() // 让canvas聚焦

  function clearCanvas() {
    context.clearRect(0, 0, canvasWidth, canvasHeight)
  }

  function doKeyDown(e) {
    // 获取keyCode
    const keyCode = e.keyCode ? e.keyCode : e.which

    // 向上箭头 / w,让纵坐标向上移动10
    if (keyCode === 38 || keyCode === 87) {
      clearCanvas()
      rectY -= 10
      if (rectY < 0) {
        rectY = 0
      }
      context.fillRect(rectX, rectY, rectWidth, rectHeight)
    }

    // 向下箭头 / s,让纵坐标向下移动10
    if (keyCode === 40 || keyCode === 83) {
      clearCanvas()
      rectY += 10
      if (rectY > canvasHeight - rectHeight) {
        rectY = canvasHeight - rectHeight
      }
      context.fillRect(rectX, rectY, rectWidth, rectHeight)
    }

    // 向左箭头 / a,让纵坐标向左移动10
    if (keyCode === 37 || keyCode === 65) {
      clearCanvas()
      rectX -= 10
      if (rectX < 0) {
        rectX = 0
      }
      context.fillRect(rectX, rectY, rectWidth, rectHeight)
    }

    // 向右箭头 / d,让纵坐标向右移动10
    if (keyCode === 39 || keyCode === 68) {
      clearCanvas()
      rectX += 10
      if (rectX > canvasWidth - rectWidth) {
        rectX = canvasWidth - rectWidth
      }
      context.fillRect(rectX, rectY, rectWidth, rectHeight)
    }
  }		  
}
</script>

当canvas元素处于聚焦状态时,可以监听到键盘事件,当其失去焦点时,则也会失去键盘监听。

我们可以基于此进行canvas小游戏开发,比如贪吃蛇、推箱子、走迷宫、射击、俄罗斯方块等等。

作者:有刃有鱼阮小六
链接:https://juejin.cn/post/6966986477662109709
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

使用 Vue 创建 ASP.NET Core 应用 - Visual Studio (Windows) | Microsoft Learn

mikel阅读(473)

来源: 使用 Vue 创建 ASP.NET Core 应用 – Visual Studio (Windows) | Microsoft Learn

适用范围:yesVisual Studio noVisual Studio for Mac noVisual Studio Code

在本文中,你将了解如何生成 ASP.NET Core 项目来充当 API 后端,并生成 Vue 项目来充当 UI。

目前,Visual Studio 包含支持 Angular、React 和 Vue 的 ASP.NET Core 单页应用程序 (SPA) 模板。 这些模板在 ASP.NET Core 项目中提供内置的客户端应用文件夹,其中包含每个框架的基本文件和文件夹。

可以使用本文中所述的方法创建具有以下功能的 ASP.NET Core 单页应用程序:

  • 将客户端应用放在 ASP.NET Core 项目之外的独立项目中
  • 基于计算机上安装的框架 CLI 创建客户端项目

 备注

本文介绍使用 Visual Studio 2022 版本 17.7 中的模板创建项目的过程,该版本使用 Vite CLI。

先决条件

确保安装以下内容:

  • 安装了“ASP.NET 和 Web 部署”工作负载的 Visual Studio 2022 版本 17.7 或更高版本。 请转到 Visual Studio 下载页,进行免费安装。 如果需要安装工作负载,但已安装 Visual Studio,请转到“工具”>“获取工具和功能…”,这会打开 Visual Studio 安装程序。 选择“ASP.NET 和 web 开发”工作负载,然后选择“修改” 。
  • npm (https://www.npmjs.com/),随 Node.js 提供。

创建前端应用

  1. 在“开始”窗口中(选择“文件”>“开始窗口”并打开),选择“新建项目”。

    Screenshot showing Create a new project

  2. 在顶部的搜索栏中搜索“Vue”,然后选择“Vue 和 ASP.NET Core (预览版)”,并将 JavaScript 或 TypeScript 作为所选语言。

    Screenshot showing choosing a template

  3. 为项目和解决方案命名,然后选择“创建”。

    创建项目后,解决方案资源管理器应如下所示:

    Screenshot showing Solution Explorer

    独立 Vue 模板相比,可以看到一些用于与 ASP.NET Core 集成的新增和修改文件:

    • aspnetcore-https.js
    • vite.config.json(已修改)
    • HelloWorld.vue(已修改)
    • package.json(已修改)

设置项目属性

  1. 在解决方案资源管理器中,右键单击 ASP.NET Core 项目 (webapi),然后选择“属性”。

    Screenshot showing Open project properties

  2. 在“属性”页中,打开“调试”选项卡,然后选择“打开调试启动配置文件 UI”选项。 清除“启动浏览器”选项。

    Screenshot showing Debug launch profiles UI

    这会阻止打开包含源天气数据的网页。

     备注

    在 Visual Studio 中,launch.json 存储与“调试”工具栏中的“开始”按钮关联的启动设置。 目前,launch.json 必须位于 .vscode 文件夹下。

启动项目

若要启动项目,请按 F5 或选择窗口顶部的“开始”按钮 。 将显示两个命令提示符:

  • 正在运行的 ASP.NET Core API 项目
  • Vite CLI 显示一条消息,例如“VITE v4.4.9 ready in 780 ms

 备注

检查消息的控制台输出,例如指示你更新 Node.js 版本的消息。

Vue 应用随即显示,该应用通过 API 填充。 如果未显示该应用,请参阅疑难解答

发布项目

从 Visual Studio 2022 版本 17.3 开始,可以使用 Visual Studio 发布工具发布集成解决方案。

 备注

要使用发布,请使用 Visual Studio 2022 版本 17.3 或更高版本创建 JavaScript 项目。

  1. 在解决方案资源管理器中,右键单击 ASP.NET Core 项目,然后选择“添加”>“项目引用”。
  2. 选择 Vue 项目并选择“确定”。
  3. 在解决方案资源管理器中,右键单击 ASP.NET Core 项目,然后选择“卸载项目”。

    这将打开项目的 .csproj 文件。

  4. 在 .csproj 文件中,更新项目引用,然后添加 <ReferenceOutputAssembly> 并将值设置为 false

    更新引用后,它如下所示(替换你自己的项目文件夹和项目名称)。

    XML

    <ProjectReference Include="..\vueprojectfolder\vueprojectname.esproj">
        <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
    </ProjectReference>
    
  5. 右键单击 ASP.NET Core 项目并选择“重新加载项目”。
  6. 在 Program.cs 中,更新 Environment.IsDevelopment 的检查,使其类似以下内容。
    C#

    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
       app.UseSwagger();
       app.UseSwaggerUI();
    }
    else
    {
       app.UseDefaultFiles();
       app.UseStaticFiles();
    }
    
  7. 若要发布,请右键单击 ASP.NET Core 项目,选择“发布”,然后选择与所需的发布场景相匹配的选项,例如 Azure、发布到文件夹等。

    与在 ASP.NET Core 项目中发布相比,该发布过程需要更多时间,因为发布时会调用 npm run build 命令。

    可以使用 VUE 项目属性中的生产环境生成命令修改 npm run build 命令。 若要进行修改,请在解决方案资源管理器中右键单击 Vue 项目并选择“属性”。

故障排除

代理错误

你可能会看到以下错误:

[HPM] Error occurred while trying to proxy request /weatherforecast from localhost:4200 to https://localhost:5173 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)

如果看到此问题,很可能前端在后端之前启动。 看到后端命令提示符启动并运行后,只需在浏览器中刷新 Vue 应用即可。

否则,如果端口正在使用中,请尝试在 launchSettings.json 和 vite.config.js 中将端口号增加 1。

隐私错误

可能会看到以下证书错误:

Your connection isn't private

尝试从 %appdata%\local\asp.net\https 或 %appdata%\roaming\asp.net\https 中删除 Vue 证书,然后重试。

验证端口

如果天气数据未正确加载,可能还需要验证端口是否正确。

  1. 确保端口号匹配。 转到 ASP.NET Core 的 webapi 项目中的 launchSettings.json 文件(在 Properties 文件夹中)。 从 applicationUrl 属性获取端口号。

    如果有多个 applicationUrl 属性,请使用 https 终结点查找一个。 它看起来应该类似于 https://localhost:7142

  2. 然后,转到 Vue 项目的 vite.config.js 文件。 更新 target 属性,以匹配 launchSettings.json 中的 applicationUrl 属性。 在进行更新时,该值应如下所示:
    JavaScript

    target: 'https://localhost:7142/',
    

过时的 Vue 版本

如果在创建项目时看到控制台消息“找不到文件 ‘C:\Users\Me\source\repos\vueprojectname\package.json’”,则可能需要更新 Vite CLI 的版本。 更新 Vite CLI 后,可能还需要删除 C:\Users\[yourprofilename] 中的 .vuerc 文件。

Docker

如果在创建 Web API 项目时启用 Docker 支持,则后端可能会开始使用 Docker 配置文件,而不侦听已配置的端口 5173。 若要解决问题,请执行以下操作:

通过添加以下属性编辑 launchSettings.json 中的 Docker 配置文件:

JSON

"httpPort": 5175, 
"sslPort": 5173  

或者,使用以下方法进行重置:

  1. 在解决方案属性中,将后端应用设置为启动项目。
  2. 在“调试”菜单中,使用“开始”按钮下拉菜单将配置文件切换到后端应用的配置文件。
  3. 接下来,在解决方案属性中,重置为多个启动项目。

后续步骤

有关 ASP.NET Core 中的 SPA 应用程序的详细信息,请参阅开发单页应用。 链接的文章为项目文件(如 aspnetcore-https.js)提供了更多上下文,尽管由于项目模板和 Vue.js 框架与其他框架之间的差异,实现的详细信息有所不同。 例如,Vue 文件包含在一个单独的项目中,而不是在 ClientApp 文件夹中。

【逆向专题】【危!!!刑】(一)使用c#+Win32Api实现进程注入到wechat - 四处观察 - 博客园

mikel阅读(338)

来源: 【逆向专题】【危!!!刑】(一)使用c#+Win32Api实现进程注入到wechat – 四处观察 – 博客园

引言

自从上篇使用Flaui实现微信自动化之后,这段时间便一直在瞎研究微信这方面,目前破解了Window微信的本地的SQLite数据库,使用Openssl,以及Win32Api来获取解密密钥,今天作为第一张,先简单写一下,获取微信的一些静态数据,以及将自己写的c语言dll通过Api注入到微信进程里面去,最后调用我们的dll的方法。话不多说,让我们开始吧。

逆向

静态数据的话,需要用到的软件 CE,全称是Cheat Engine,图标如下所示。接下来我们打开CE,可以看到左上角有一块绿色的按钮,我们点击按钮是附加进程到CE,然后我们点击附加微信到CE,在下面的图中,我们看到已经把微信进程加载到了CE里面去,然后我们要开始获取静态数据了。

 

在获取静态数据之前,我们先开始讲几个概念,就是内存的概念,我们都知道,在进程启动的时候,操作系统会给我们的进程分配虚拟内存,默认应该是4g,具体是和操作系统位数也有关系,然后在运行时也会动态的分配内存空间,我们学过计算机原理的肯定知道,我们的内存存储结构就像是一个链表或者数组,我们在给这个进程分配内存空间的时候,他的样子也是是类似数组的这种结构,首先假如我们的进程现在有一个主模块,主模块里面又有自己的方法,自己的类,属性等信息,那分配的这个主模块的内存就是一个数组,然后我们主模块有一个基础地址,你可以将这个基础地址看作是这块内存数组的索引0,而我们主模块的其他的方法,类,变量信息,都是在这个0的索引进行移动到指定的地址,这个地址指向我们的内存,这个内存存储着我们要的信息。简而言之,就是主模块是的地址就是索引0,而其他变量信息可能在5,7,9等等,我们就需要判断从0到5有多少间隔,这个就叫偏移量,我们通过属性或者方法的内存地址减去主模块的地址,这个就是我们的偏移量,借这个例子就是5-0就是5,偏移量是5。

然后我们回来,我们加载微信进程到了我们的CE之后,在wechat有一个模块叫Wechatwin,这个是window操作系统下的微信用到的主要模块,我们的和微信相关的基本都在这里,当然不包括一些resource,这个有一个专门的模块,我们在此不多赘述,所以我们假如要找我们的静态数据,例如微信昵称,微信号,或者手机号,所在地区,就需要找到我们的wechatwin的地址,这个就是这个模块的基址,然后我们需要在CE中,检索字符串找到我们要的数据,例如昵称,手机号等信息。然后用他的地址减去基址,得到偏移量。从而我们就可以在代码中获取到这些信息,接下来,我先带大家在CE中找到我们想要找的数据。

在CE上方右侧,有一个输入框,我们在这里输入我们需要检索的信息,支持的格式有byte,string,以及array,double等数据类型,我们需要找到是string,所以在ValueType那里,我们选择string。我的微信昵称是云淡风轻,所以在这里搜索云淡风轻,可以看到,就检索出来我们的昵称信息了,找到了这么多,这里我们往最下面拉,有一个绿色开头的,Address是WechatWin的,就是我们要找的地址了,其他的也有的是绿色基于Wechatwin有的不是,有的就需要一个一个测试修改数据从而得到验证了。

我们双击那条绿色记录,Wechatwin 将他加入到下面的列表去,代表我们选中的检测的内存,接下来我们验证一下,是否是找的正确的,双击Value,云淡风轻,我们修改我们的Value,将云淡风轻,改为good man 点击ok,可以在下面看到,我们的微信昵称已经同步改为了good Man,说明我们找到的是对的,接下来,我们双击Address,

 

弹出Change address姐妹,我们复制WechatWin.dll,需要我们找到我们这个模块的基址。然后在右边有一个Add Address Manually,手动添加地址,,我们把复制的WeChatWin.dll复制过去,然后点击ok,在下面的列表我们就看到了这个模块的基址,接下来,我们需要判断这个基址和昵称之间的偏移量,按照我们刚才所说的方式计算,转换16进制就是0x7ffd3d668308-0x7ffd39b40000,随便找一个16进制计算器,算下来的结果就是3B28308,也就是Address里面显示的那个,实际上CE已经给我们把偏移算出来了,接下来按照同样的方式,去搜索我们的所在地区,以及手机号,如果有的信息找不到的话,我们选择我们的昵称哪一行数据,右键,选择Browse this Memory region,在内存页显示这个内存记录,然后我们在旁边就可以看到我们的国家,以及省份地区信息了,如果有查看地址,在右侧,选择我们要复制的记录,右键,有一个goto Address,然后就导航到了我们的内存,然后复制地址即可。

 

C#代码获取数据以及远程注入

在上面我们讲了,如何使用CE,去获取我们微信的一些静态数据,接下来,我们就需要使用C#代码,去实现我们获取静态数据,以及最后写的一个远程注入,来调用我们写的一个库。首先我们需要用到的有几个Api函数,

WaitForSingleObject,等待某个句柄多长时间,在我们创建远程线程的时候需要使用这个函数来等待线程执行结束。参数是等待的句柄,我们填写我们的线程句柄。

GetProcAddress,需要使用这个函数来调用kernel32.dll的LoadLibraryA方法,来加载我们的自己写的dll,因为在每个进程启动的时候,都会去调用这个方法来加载程序所依赖的dll,还有一个方法是LoadLibraryW,和这个方法区别在于不同针对不同的编码来进行调用,W结尾主要是针对UNICODE的编码,A结尾对应Ascii编码,所以各位在调用的时候根据自己的编码去调用,如果一个找不到就试试另一个。

GetModuleHandle,这个函数是用来获取kernel32.dll,结合上面的GetProdAddress来使用。

OpenProcess,这个方法是根据指定的PID,对应就是Process类的Id,打开指定的进程,同时指定以什么权限打开这个进程,参数是三个,第一个是权限,第二个是返回值是否可以被继承,返回的进程句柄是否可以被继承,第三个参数就是我们的PID。

VirtualAllocEx,给指定的进程分配虚拟内存,第一个参数是进程的句柄,OpenProcess返回值,第二个参数指定进程内那个内存地址分配的内存,此处我们只是加载dll调用方法,并不注入到某个方法或者哪里所以是Intptr.Zero,第三个参数是,分配的内存长度,我们加载dll需要dll的路径,这里就选择路径.Length就行,字符串的长度就可以,第三个参数是内存分配的一些配置,可选值在后面会有,此处我们选择Memory_Commit,第四个参数是内存权限相关,内存是只读还是可以读写,以及用来执行代码或者怎么样,这里我们选择可以读写。

ReadProcessMemory,读指定进程的内存,第一个参数进程句柄,OpenProcess返回值,第二个参数是这个进程某个内存的地址,第三个是数据缓冲区,读取之后的内容就在这个缓冲区,我们读取这个缓冲区就可以拿到数据,第四就是缓冲区的长度,第五个就是读取的字节数量。

GetLastError,用来获取Win32api调用的时候的errorcode,错误编码,

CloseHandle,关闭某一个句柄,关闭基础,关闭线程。

WriteProcessMemory,写入内存,我们需要将我们的dll地址写入到指定内存中去,第一个参数进程句柄,OpenProcess返回值,第二个参数,要写入的内存地址基址,例如我们后期需要在某个方法进行注入,这块就需要写入这个方法的内存地址,第三个参数,写入的byte数据,第四个参数是第三个参数的长度,最后一个参数是写入的数据数量。

CreateRemoteThread,在指定的进程中创建远程线程,第一个参数 OpenProcess返回值,第二个参数是线程安全的一些特性描述,按网上所说,一般null或者 IntPtr.Zero,第三个参数设置线程堆栈大小,默认是0,即使用默认的大小,第四个参数是线程函数的地址,我们要通过这个方法去调用Kernel32的LoadLibrary方法加载我们的dll,那这个参数就填写我们的GetProcAddress返回值,第四个参数就是创建这个线程的参数,就是分配的远程内存的地址VirtualAllocEx返回值,就是说通过创建远程线程来调用LoadLibrary方法加载我们写入指定内存地址的dll库,来实现注入,是这样一个逻辑,第五个参数是线程创建的一些参数,是创建后挂起还是直接运行等,最后一个参数是输出参数,记录创建的远程线程的ID。

以上是我们所需要用到的所有的Win32Api函数,接下来我们进入代码阶段。

在下面的窗体,窗体会在加载的时候就去调用注入我们的dll,同时界面在加载的时候就获取获取我们的静态信息。我们的dll地址是E盘下面的一个dll,这个Dll使用c语言编写。在启动的时候我们去获取我们的微信进程,拿到的ID,然后去注入我们的Dll,在下面的代码里,我们判断是否模块是WechatWin.dll,如果是,就定义了phone,NickName,Provice,Area等int值,这个其实就是我们在CE拿到的静态数据的内存地址,减去我们的Wechatwin.Dll的出来的偏移量,然后定义了我们各个静态数据的缓冲区,用来读取从微信进程读取的内存数据。然后我们调用了ReadProcessMemory函数读取内存,获取我们需要的静态数据。然后使用Utf8转为字符串,显示到界面上。这就是获取静态数据的源码,然后关闭我们的进程句柄,并不是关闭微信,而是关闭我们获取的这个进程句柄。

复制代码
 string dllpath = @"E:\CoreRepos\ConsoleApplication2\x64\Debug\Inject.dll";
 var process = Process.GetProcessesByName("wechat").FirstOrDefault();
           InjectDll(process.Id, dllpath);
            var pid = OpenProcess(ProcessAccessFlags.PROCESS_ALL_ACCESS, false, process.Id);
            int bytesRead;
            int bytesWritten;
            foreach (ProcessModule item in process.Modules)
            {
                if (item.ModuleName.ToLower() == "WechatWin.dll".ToLower())
                {
                    int phone = 0x3B28248;
                    int NickName = 0x3b28308;
                    int provice = 0x3B282A8;
                    int Area = 0x3B282C8;
                    var Nickbuffer = new byte[12];

                    var Phonebuffer = new byte[11];
                    var proviceBuffer= new byte[12];
                    var areaBuffer=new byte[12];
                    ReadProcessMemory(process.Handle, item.BaseAddress + NickName, Nickbuffer, Nickbuffer.Length, out bytesRead);
                    ReadProcessMemory(process.Handle, item.BaseAddress + phone, Phonebuffer, Phonebuffer.Length, out bytesRead);
                    ReadProcessMemory(process.Handle, item.BaseAddress + provice, proviceBuffer, proviceBuffer.Length, out bytesRead);
                    ReadProcessMemory(process.Handle, item.BaseAddress + Area, areaBuffer, areaBuffer.Length, out bytesRead);
                    var Nickvalue = Encoding.UTF8.GetString(Nickbuffer); 

                    var Phonevalue = Encoding.UTF8.GetString(Phonebuffer); 
                    var Provicevalue = Encoding.UTF8.GetString(proviceBuffer); 
                    var Areavalue = Encoding.UTF8.GetString(areaBuffer); 
                    label1.Text = Nickvalue;
                    label2.Text = Phonevalue;
                    label3.Text = Provicevalue;
                    label4.Text = Areavalue;
                    var buf = Encoding.UTF8.GetBytes("我是你爹");
                    CloseHandle(process.Handle);
                }
            }
复制代码

 

 

然后我们开始看看注入DLL的代码,我们先引入了诸多函数,然后定义了OpenProcess第一个参数权限的枚举,定义了INFINITE 用来WaitForSingleObject等待指定的句柄进行某些操作的执行结束,当然有一些我没有定义完整,只定义我们此处需要的,完整的可以参考官网api去进行看。在刚进入这段代码,我们调用OpenProcess指定最高权限打开这个进程,然后获取我们的dll地址的byte数组,并将分配内存VirtualAllocEx到我们这个进程里面,同时最后两个参数代表分配内存的一些操作,例如内存是Memory_Commit,0x1000,以及内存是可以读写的0x04,分配好内存之后,我们去往我们分配好的内存写入我们的dll路径,调用WriteProcessMemory方法,传入进程句柄,内存地址,写入的数据等,在下面GetProcAddress和GetModuleHandle用来加载kernel32的LoadraryA方法句柄,最后我们调用了CreateRemoteThread函数将我们的dll注入到远程进程中去。

复制代码
 #region 32 api
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, int flAllocationType, int flProtect);
        [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool ReadProcessMemory(
       IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead
   );
        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(
       ProcessAccessFlags dwDesiredAccess,
       bool bInheritHandle,
       int dwProcessId
   );
        [DllImport("kernel32.dll")]
        static extern uint GetLastError();
        [DllImport("kernel32.dll")]
        public static extern bool CloseHandle(IntPtr hObject);

        [DllImport("kernel32.dll")]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
        [DllImport("kernel32.dll")]
        public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        // 进程访问权限标志位
        [Flags]
        public enum ProcessAccessFlags : uint
        {
            PROCESS_ALL_ACCESS = 0x1F0FFF,
            PROCESS_CREATE_PROCESS = 0x0080,
            PROCESS_QUERY_INFORMATION = 0x0400,
        }
        const uint INFINITE = 0xFFFFFFFF;
        #endregionpublic  bool InjectDll(int processId, string dllPath)
        {
            IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_ALL_ACCESS, false, processId);

            if (hProcess == IntPtr.Zero)
            {
                Console.WriteLine("打开失败");
                return false;
            }

            byte[] dllBytes = Encoding.UTF8.GetBytes(dllPath); ;
            IntPtr remoteMemory = VirtualAllocEx(hProcess, IntPtr.Zero, dllBytes.Length, 0x1000, 0x04);


            int bytesWritten;

            if (!WriteProcessMemory(hProcess, remoteMemory, dllBytes, dllBytes.Length, out bytesWritten))
            {
                var ooo = GetLastError();
                Console.WriteLine("写入失败");
                return false;
            }
            IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
            var ooaa = GetLastError();
            if (loadLibraryAddr == IntPtr.Zero)
            {
                Console.WriteLine("获取LoadraryA失败");
            }

            // 创建远程线程,在目标进程中调用 LoadLibraryA 加载 DLL
            var hRemoteThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLibraryAddr, remoteMemory, 0, IntPtr.Zero);
            var ooaa1 = GetLastError();
            if (hRemoteThread == IntPtr.Zero)
            {
                Console.WriteLine("目标进程创建远程线程失败");
            }

            // 等待远程线程执行完毕
            WaitForSingleObject(hRemoteThread, 0xFFFFFFFF);


            WaitForSingleObject(hRemoteThread, INFINITE);

            CloseHandle(hRemoteThread);
            CloseHandle(hProcess);

            Console.WriteLine("注入成功");

            return true;
        }
复制代码

我们看看我写的dll里面是包括了什么内容,我们的dll内容很简单,就是创建一个txt文件,然后写入一个数据就行,这里需要注意的是,在使用vs创建dll的时候 选项必须是选择的是动态链接库,这样才有DLLMain方法,这样在调用LoadraryA方法的时候才会调用我们的dll,自动调用DLLMain方法,同时里面还有一个switch case语句是进程加载线程加载,以及线程卸载,进程卸载的判断 我们可以在这里去去一些我们的逻辑判断,此处我并没有写,只是在外层创建了一个文件夹,接下来运行一下我们的winform,看看有没有获取到静态数据,以及将我们的dll注入进去。马赛克手机号。

 

 

可以看到我们启动了界面之后,查看我们的Process.Modules,可以看到我们注入的Inject.dll,那我们看看有没有创建txt呢。在下面可以看到,我们已经成功注入到微信进程并且创建了一个example.txt,并且写入的内容和上图定义的内容是一致的,到此,我们将我们dll注入到了微信进程中去了。

结语

在上面我们讲了一些如何找到静态数据,以及根据基址,偏移量在进程启动的时候找到我们想要的数据,并且将我们的dll成功注入到进程里面去,在后面,我可能还会在深入研究一下逆向,到时候会继续发文,感兴趣的朋友可以关注一波,同时,近期,还破解了微信SQLite本地数据库获取了一些内容,下面是获取的数据内容,这个我应该不会开源,但是会有一个c语言的写的解密demo开源,同时可能会分享一部分C#获取解密密钥的代码,同时也需要一些逆向的知识,win32api,这个东西由于涉及个人隐私,所以我尚不确定是否开源,因为存在有的人如果挂马,可以窃取他人的隐私,所以后续再说,同时在写的,讲的不对的地方,欢迎各位大佬指正。

 

一份关于DDD的FAQ | 码如云文档中心

mikel阅读(361)

来源: 一份关于DDD的FAQ | 码如云文档中心

前几年,我全程采用DDD开发了一个一物一码管理平台——码如云,并成功上线。在这个过程中,我们学了很多,积累了很多,也踩了不少的坑。在本文中,我希望通过FAQ(Frequently Asked Questions,常见问题)的方式给大家分享一下我对DDD的一些看法。如果对DDD落地感兴趣,可以参考我的DDD落地文章系列


问:DDD最大的受益者是谁?

答:架构师和程序员,请注意,这里的架构师是那些工作在前线,至少会参与软件模型设计的架构师,最好是依然参与代码编写工作。


问:搞DDD需要哪些条件?

答:建议先写3年左右的代码,在熟悉了面向对象技术后再学习DDD可能更加容易上手。


问:DDD为什么有时也被称为”玄学”?

答:很多对DDD的解读比较空洞,纸上谈兵,无法指导软件项目的实际落地,这种对DDD虚无缥缈的解读被有些人称为”玄学”。


问:哪些人可以做领域专家?

答:任何熟悉业务的人,比如银行的前台工作人员对于银行业务系统来说则可认为是领域专家。


问:业务分析师(BA)需要了解DDD吗?

答:不需要,BA需要做的事情是理清业务,然后将业务传递给架构师或程序员。


问:DDD的战略设计是什么?

答:DDD的战略设计只在解决一个问题,即软件的模块化划分问题。


问:DDD与微服务是什么关系?

答:没关系,业界有个说法是“DDD的限界上下文可以帮助指导微服务的划分”,但是这个说法过于笼统和牵强,基本不具现实指导意义。


问:DDD当下为什么这么火?

答:可能是跟风者比较多吧,或者是前面提到的与微服务那种牵强的关系,DDD就是一个工具而已,原本不应该这么火的。


问:DDD与TOGAF有什么关系?

答:没有关系,TOGAF主要用于企业整体架构,而DDD侧重于一个具体业务系统的软件设计和落地。


问:搞DDD必须搞事件风暴吗?

答:不用,事件风暴主要用于帮助我们了解业务流程,在实践中事件风暴很容易陷入“为了搞事件风暴而搞事件风暴”的陷阱,建议采用更加朴素的方式梳理业务。


问:DDD与面向对象是什么关系?

答:可以认为DDD是面向对象进阶,这也是为什么前面有建议说熟悉了OO之后再搞DDD。


问:DDD的战术设计包含哪些概念?

答:包含应用服务,聚合根,领域服务,实体,值对象,工厂,领域事件,资源库等。


问:为什么建议将业务概念优先建模为值对象而不是实体?

答:因为值对象是不可变的,可以大大降低系统的信息熵进而降低程序员的负担,并且可以方便逻辑推理和系统调试。


问:怎么理解聚合根?

答:聚合根表示业务中那些顶级的实体对象,其内部数据相互紧密联系,即“聚合”在一起。比如,电商系统中的“订单(Order)”,CRM系统中的“客户(Customer)”均是典型的聚合根对象。


问:不变条件是什么意思?

答:不变条件表示在聚合根中,那些具有业务互动性的业务逻辑,不变条件必须在同一个聚合根的公有方法中得到满足,否则容易导致业务逻辑的泄漏。一个老生常谈的例子是订单(Order),修改订单项内容后,订单价格也应该随之变化,因此对订单项的修改和对价格的修改应该放到同一个方法(比如updateOrderItems())中。


问:事务边界应该放在哪里?

答:应用服务,因为应用服务中的共有方法和业务用例一一对应,而业务用例又与事务一一对应。


问:什么时候应该用领域服务?

答:当将业务逻辑放在聚合根中不合适的时候,才考虑创建领域服务来存放这些业务逻辑。比如,在更新成员手机号时,需要检查手机号是否已经被他人占用,这种跨聚合根的业务逻辑无法放到某一个成员对象中,此时应该采用领域服务。


问:应用服务和领域服务的区别是什么?

答:应用服务和领域服务是很不一样的概念,应用服务是领域模型的门面,所有外部请求都由应用服务的调度编排后进入领域模型中,而领域服务是属于领域模型的一部分。应用服务不包含业务逻辑,领域服务则相反。


问:如何保证发送领域事件和更新聚合根之间一致性?

答:采用事件发送表,即先将事件保存到与聚合根相同的数据库中,这样通过数据库的本地事务即可完成它们之间的数据一致性,然后再通过一个单独的组件从事件表中加载领域事件再发送出去。更多详情,请参考这里


问:DDD和CQRS是什么关系?

答:没关系,不过在DDD项目中通常会采用CQRS,以得到更加纯粹的领域模型,当然CQRS的作用并不止于此。


问:有推荐的DDD书籍吗?

答:《领域驱动设计:软件核心复杂性应对之道》(蓝皮书),《实现领域驱动设计》(红皮书),《领域驱动设计模式、原理与实践》,《解构领域驱动设计》等。

DDD项目中使用Lombok的正确姿势 | 码如云文档中心

mikel阅读(322)

来源: DDD项目中使用Lombok的正确姿势 | 码如云文档中心

写过Java的程序员都知道,Java的语法是比较繁琐的, 各种getter、setter、equals()和hashCode()满天飞,有时甚至将真正的业务逻辑代码完全淹没,让读代码的人很难直观地了解到代码所完成的功能。于是我们有了Lombok,只需在Java类或方法上加上一些简单的注解,Lombok便会在编译时介入,并根据所添加的注解自动生成相应的字节码(比如getter和setter代码等)。

比如,对于**颜色(Color)**对象来说,当我们加上Lombok的@Data注解后,Lombok将自动为Color类生成getter、setter、toString()、eqauls()、hashCode()和构造函数等众多方法,进而让程序员将关注点放在更有价值的业务逻辑相关的代码中,岂不乐哉?

@Data
public class Color {
    int red;
    int green;
    int blue;
}

不过,也有人反对使用Lombok,其中一个很重要的原因是Lombok自动生成的setter方法只是简单的赋值操作,并没有对数据的合法性进行检查,进而可能使对象处于一种非法的状态,比如对于上例的Color对象来说,调用方可以在调用setRed()方法时,传入一个大于255的值,而我们都知道颜色的RGB值最大只能是255,因此当传入256时Color对象则不合法了。对于对业务合法性要求极高的DDD来说,使用Lombok的风险尤其突出。

于是,我们来到了一个两难的境界,一方面使用Lombok的确可以减少大量的重复性编码,另一方面Lombok又可能给程序带来非法数据的风险。要解决这个问题的,我们需要对Lombok的使用进行管控,以便可以更加安全地使用之。在日常DDD编码的过程中,我们(码如云)积累了多种Lombok使用模式,在本文中分享给大家。如果对DDD本身感兴趣,读者可以参考笔者的另一个DDD落地文章系列

DDD中包含多种概念,其中可能用到Lombok的概念有聚合根(Aggregate Root)、实体(Entity)和值对象(Value Object)等,本文将针对这些对象分别给出相应的Lombok使用建议。

禁止使用@Setter@Data #

DDD社区对setter方法可谓是深恶痛绝,其中主要有以下2个原因:

  1. setter方法只是机械式的赋值操作,无法体现代码的业务意图,而业务意图却正是DDD所强调的;
  2. setter方法只是简单的赋值操作,并没有对业务的合法性进行检查,如果使用不当可能导致Bug,如上例中的Color

Lombok中的@Data由于包含了@Setter的功能,因此也不建议使用。在不使用@Setter@Data时,实体对象可以通过业务方法予以代替,比如要更新**成员(Member)**的姓名时,可以使用一个单独编写的updateName()方法完成,在该方法中对姓名进行合法性验证后,再对name字段赋值。

//Member

public void updateName(String newName) {
    if (newName.length() > 100) {
        throw new RuntimeException("姓名不能超过100个字符。");
    }

    this.name = newName;
    raiseEvent(new MemberNameChangedEvent(this.getId(), newName));
}

可以看到,在更新Member的姓名(name)时,首先对姓名的长度进行合法性检查,如果合法才进行赋值,赋值后还需要通过raiseEvent()向外部发出领域事件,而这些操作是一个简单的setter所无法完成的。

对于值对象来说,则更不应该使用setter了,因为根据DDD原则,值对象一旦被创建便不能修改了,使用setter明显违背了这一原则。

值对象使用Lombok #

在DDD中,值对象表示那些起描述作用的对象,值对象之间通过所包含的数据内容进行相等性判断。比如,上例的Color则是一个值对象,当两个Color对象所携带的RGB值均相同时,则可认为这两个对象相等,也即可互换。值对象存在一个非常重要的约束:不变性,也即在值对象被创建出来之后,便不能再改变其状态了,如需改变,则需要创建出另一个值对象(比如下例中的changeRedTo()方法)。

//Color

@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class Color {
    int red;
    int green;
    int blue;

    public Color changeRedTo(int red) {
        if (red > 255) {
            throw new RuntimeException("Invalid Red value.");
        }
        return Color.builder()
                .red(red)
                .green(this.green)
                .blue(this.blue)
                .build();
    }
}

在使用Lombok时,通过@Value表示一个值对象,@Value并不包含生成setter方法。另外,通过@Builder自动生成Builder方法。

需要注意的是,我们通过access = PRIVATE将全参构造函数设置为了私有的,外部无法访问,因为我们只希望外部通过一种方式创建值对象——Builder。这样做其实还可以解决Lombok无法解决的一个问题:在本例中,假设Color对象使用的是@AllArgsConstructor(access = PUBLIC)生成的全参构造函数,那么调用方可以通过以下方式创建一个Color对象:

Color aColor = new Color(12, 23, 34);

此时,如果我们将Color中的redgreen字段调换一下顺序,由于redgreen均是int类型,那么通过@AllArgsConstructor所生成的全参构造函数的签名其实是无变化的,这也意味着上面通过构造函数创建Color的代码是不会产生编译错误的,但是其所表示的业务含义已经随着字段顺序的变化而变化了,如果程序员自己不知道需要做相应的修改,那么Bug也就因此而生。

因此,在使用Lombok时,我们更推荐使用Builder进行对象的创建,而不是全参构造函数。具体落地时,由于@Value会自动引入@AllArgsConstructor,因此需要通过@AllArgsConstructor(access = PRIVATE)将其显式地隐藏起来。

聚合根使用Lombok #

聚合根可以说是DDD中最重要的概念了,它表示领域模型中那些最重要的实体性对象(比如电商系统中的订单Order,CRM系统中的客户Customer等),其他DDD概念都围绕着聚合根展开。

聚合根是有生命周期的对象,通常会被持久化到数据库中,也就是说通常有2种情况涉及到聚合根的创建:

  1. 业务上从无到有新建一个聚合根对象;
  2. 从数据库中加载一个既有的聚合根对象。

对于从业务上新建来说,新建过程是一个显著的业务过程,并且一般不需要全参数构造函数,而是基于场景所需数据完成创建。因此,此时对聚合根的新建过程通常采用我们自己编写的构造函数完成创建。

对于从数据库加载聚合根对象来说,由于Spring Data框架会自动调用无参构造函数,因此可以通过Lombok的@NoArgsConstructor(access = PRIVATE)自动生成。

另外,由于外部通常会获取聚合根中的各种数据,因此可以使用比较安全的@Getter向外暴露各个字段。

//Member

@Getter
@NoArgsConstructor(access = PRIVATE)
public class Member extends AggregateRoot {
    private String name;//姓名
    private List<Roles> roles; //角色

    public Member(String name) {
        if (name.length() > 100) {
            throw new RuntimeException("Name must be less than 100 characters in length.");
        }
        this.name = name;
    }
}

可以看到,对聚合根而言,我们只使用了Lombok的@GetterNoArgsConstructor注解,并且NoArgsConstructor所生成的无参构造函数被声明为了私有的,因此其作用只是便于框架调用而不是编码时直接调用。

事实上,在聚合根中使用Lombok所得到的好处并不多,反而有可能带来一定风险。一是无参的构造函数通过自己手动编写也非常简单;二是在使用@Getter时可能将诸如List之类的容器字段一并暴露出去,使得外部可以直接操作这些容器字段,违背了聚合根的基本原则——外部只能通过聚合根完成对其内部状态的改变,而不能直接操作聚合根内部的字段。因此,对于Memberroles字段来说,比使用@Getter更安全的方式是返回一个不变的List容器:

    public List<Role> getRoles(){
        return List.copyOf(this.roles); //通过copyOf()返回一个不变的List容器
    }

不过,这个问题也可以通过将roles字段本身建模为不变对象来解决,比如使用Guava的ImmutableList,这样外部即便通过@Getter拿到了roles字段,在向其中添加数据元素时,程序也将报错。

码如云,我们做了妥协,也即依然在聚合根中使用了@Getter方法,因为我们的程序员能够自觉的遵守聚合根的各种编码原则。

命令对象和查询对象 #

在DDD中,命令对象(Command)用于封装外部向系统发起的一次请求,其中包含了请求所需数据;而查询对象(Query)则用于向外部返回查询的结果数据。在技术层面,这两种对象都属于值对象类型,因此可以使用与值对象相同的Lombok注解。

//命令对象:CreateMemberCommand

@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class CreateMemberCommand implements Command {
    @NotBlank
    @Size(max = MAX_GENERIC_NAME_LENGTH)
    private final String name;
    
}

总结 #

在本文中,我们对DDD中的各种对象使用Lombok进行了针对性的管控,从而减少了产生Bug的风险。当然,这些管控手段不见得适合于所有的项目,但是与这些实践手法本身相比,我们更希望传达的一个思想是:使用Lombok可以,但是要慎重。你得知道一个简单的Lombok注解可能给程序带来的风险,进而通过自己的手段进行规避,并形成一定的原则和套路,以让团队的所有成员通过一致的方式使用Lombok。