e盾服务端源码_gRPC服务注册发现及负载均衡的实现方案与源码解析

今天聊一下gRPC的服务发现和负载均衡原理相关的话题,不同于NginxLvs或者F5这些服务端的负载均衡策略,gRPC采用的是客户端实现的负载均衡。什么意思呢,对于使用服务端负载均衡的系统,客户端会首先访问负载均衡的域名/IP,再由负载均衡按照策略分发请求到后端具体某个服务节点上。而对于客户端的负载均衡则是,客户端从可用的后端服务节点列表中根据自己的负载均衡策略选择一个节点直连后端服务器。

Etcd软件包的naming组件里提供了一个命名解析器(naming resolver)结合gRPC本身自带的RoundRobin 轮询调度负载均衡器,让使用者能方便地搭建起一套服务注册/发现和负载均衡体系。如果轮询调度满足不了调度需求或者不想使用Etcd作为服务的注册中心和命名解析器的话,可以通过写代码实现gRPC定义的ResolverBalancer接口来满足系统的自定义需求。

本文引用的源码对应的版本为:gRPC v1.2.x、 Etcd v3.3
如果你对gRPC和Etcd还不了解,可以先看看我很早之前写的gRPC入门和Etcd入门 系列的文章。

gRPC服务注册发现

先来简单的说明一下用Etcd实现服务注册和发现的原理。服务注册和发现这个流程可以用下面这个示意图简单描述出来:

4bf9d2ea19f40cb2ee796992126338d9.png

上图的服务A包含了两个节点,服务在节点上启动后,会以包含服务名加节点IP的唯一标识作为Key(比如/service/a/114.128.45.117),服务节点IP和端口信息作为值存储到Etcd上。这些Key都是带租约的Key,需要我们的服务自己去定期续租,一旦服务节点本身宕掉,比如node2上的服务宕掉,无法完成续租后,那么它对应的Key:/service/a/114.128.45.117 就会过期,客户端也就无法再从Etcd上获取到这个服务节点的信息了。

与此同时客户端也会利用EtcdWatch功能监听以/servive/a为前缀的所有Key的变化,如果有新增或者删除节点Key的事件发生Etcd都会通过WatchChan发送给客户端,WatchChan在编程语言上的实现就是GoChannel

服务注册

关于Etcd的服务注册,官方提供的软件包里并没有提供统一的注册函数供调用。那么我们在新增服务节点后怎么把节点的信息存储到Etcd上并通知给命名解析器呢?在Etcd源码包的naming/grpc.go里可以发现提供了一个Update方法,这个Update既能执行添加也能执行删除操作:

func (gr *GRPCResolver) Update(ctx context.Context, target string, nm naming.Update, opts ...etcd.OpOption) (err error) {switch nm.Op {case naming.Add:var v []byteif v, err = json.Marshal(nm); err != nil {return status.Error(codes.InvalidArgument, err.Error())}_, err = gr.Client.KV.Put(ctx, target+"/"+nm.Addr, string(v), opts...)case naming.Delete:_, err = gr.Client.Delete(ctx, target+"/"+nm.Addr, opts...)default:return status.Error(codes.InvalidArgument, "naming: bad naming op")}return err
}

服务在启动完成后可以通过Update方法把自己的服务地址和端口Put到自定义的target为前缀的key里,针对上面图示里的例子,变量target就应该是我们定义的服务名/service/a。一般在具体实践里都是自己根据系统的需求封装Update方法完成服务注册,以及服务节点Key在Etcd上的定期续租,这块每个公司的实践都不一样,我就不放具体的代码了,一般续租都是通过Etcd租约里的KeepAlive方法实现的(Lease.KeepAlive)。

服务发现

在注册完新节点、或者是原来的节点停掉后,客户端是怎么知道的呢?这块就需要命名解析器Resolver来帮助实现了,Resolver的作用可以理解为从一个字符串映射到一组IP端口等信息。

gRPC对Resolver的接口定义如下:

type Resolver interface {// Resolve creates a Watcher for target.Resolve(target string) (Watcher, error)
}

命名解析器的Resolve方法会返回一个Watcher,这个Watcher可以监听命名解析器发来的target(类似上面例子里说的与服务名相对应的Key)对应的后端服务器地址信息变化,通知Balancer对自己维护的地址进行动态地增删。

Watcher接口的定义如下:

//源码地址 https://github.com/grpc/grpc-go/blob/v1.2.x/naming/naming.go
type Watcher interface {Next() ([]*Update, error)// Close closes the Watcher.Close()
}

Etcd为这两个接口都提供了实现:

// 源码地址:https://github.com/etcd-io/etcd/blob/release-3.3/clientv3/naming/grpc.go// GRPCResolver 实现了grpc的naming.Resolver接口
type GRPCResolver struct {// Client is an initialized etcd client.Client *etcd.Client
}func (gr *GRPCResolver) Resolve(target string) (naming.Watcher, error) {ctx, cancel := context.WithCancel(context.Background())w := &gRPCWatcher{c: gr.Client, target: target + "/", ctx: ctx, cancel: cancel}return w, nil
}// 实现了grpc的naming.Watcher接口
type gRPCWatcher struct {c      *etcd.Clienttarget stringctx    context.Contextcancel context.CancelFuncwch    etcd.WatchChanerr    error
}func (gw *gRPCWatcher) Next() ([]*naming.Update, error) {if gw.wch == nil {// first Next() returns all addressesreturn gw.firstNext()}// process new events on target/*wr, ok := <-gw.wchif !ok {...updates := make([]*naming.Update, 0, len(wr.Events))for _, e := range wr.Events {var jupdate naming.Updatevar err errorswitch e.Type {case etcd.EventTypePut:err = json.Unmarshal(e.Kv.Value, &jupdate)jupdate.Op = naming.Addcase etcd.EventTypeDelete:err = json.Unmarshal(e.PrevKv.Value, &jupdate)jupdate.Op = naming.Deletedefault:continue}if err == nil {updates = append(updates, &jupdate)}}return updates, nil
}func (gw *gRPCWatcher) firstNext() ([]*naming.Update, error) {// 获取前缀为gw.target的所有Key的值,放到现有数组里resp, err := gw.c.Get(gw.ctx, gw.target, etcd.WithPrefix(), etcd.WithSerializable())if gw.err = err; err != nil {return nil, err}updates := make([]*naming.Update, 0, len(resp.Kvs))for _, kv := range resp.Kvs {var jupdate naming.Updateif err := json.Unmarshal(kv.Value, &jupdate); err != nil {continue}updates = append(updates, &jupdate)}opts := []etcd.OpOption{etcd.WithRev(resp.Header.Revision + 1), etcd.WithPrefix(), etcd.WithPrevKV()}// watch 监听这些Key的变化,包括前缀相同的新Key的加入gw.wch = gw.c.Watch(gw.ctx, gw.target, opts...)return updates, nil
}func (gw *gRPCWatcher) Close() { gw.cancel() }

这部分GRPCResolvergRPCWatcher类型的每个方法的功能和起到的作用都和RoundRobin这个gRPC Balancer结合地比较紧密,我准备放到下面和负载均衡的源码实现一起说明。

负载均衡

首先我们来看一下gRPC对负载均衡的接口定义:

type Balancer interface {Start(target string, config BalancerConfig) errorUp(addr Address) (down func(error))Get(ctx context.Context, opts BalancerGetOptions) (addr Address, put func(), err error)Notify() <-chan []Address// Close shuts down the balancer.Close() error
}

在gRPC 客户端与服务端之间建立连接时调用的Dail方法里可以用WithBalancer方法在DiaplOption里指定负载均衡组件:

client, err := etcd.Client()...resolver := &naming.GRPCResolver{Client: client}b := grpc.RoundRobin(resolver)opt0 := grpc.WithBalancer(b)grpc.Dial(target, opt0 , opt1, ...) // 后面省略了

上面的例子使用了gRPC自带的Balancer实现RoundRobin,RoundRobin除了实现了Balancer接口外自己内置了Resolver用来从名字获取其后绑定的IP信息以及服务的更新事件(增加删除服务节点这些事件) 。上面的例子里给RoundRobin指定了Etcd提供的name.GRPCResolver做为它的命名解析器,这个命名解析器就是上一节说的Etcd软件包里提供的gRPCnaming.Resolver接口实现。

RoundRobin

下面我们研究一下gRPC包里提供的RoundRobin代码实现,主要关注负载均衡和利用Resolver进行服务发现及节点更新这两个功能的代码实现原理

RoundRobin结构体定义如下:

// 源码在:https://github.com/grpc/grpc-go/blob/v1.2.x/balancer.go
type roundRobin struct {r      naming.Resolverw      naming.Watcheraddrs  []*addrInfo // 客户端可以尝试连接的所有地址mu     sync.MutexaddrCh chan []Address // 用于通知gRPC内部的,客户端可连接地址的信道next   int            // index of the next address to return for Get()waitCh chan struct{}  // the channel to block when there is no connected address availabledone   bool           // The Balancer is closed.
}
  • r是命名解析器,可以定义自己的命名解析器,如Etcd命名解析器。如果r为nil,那么Dial中参数target将直接作为可请求地址添加到addrs中。
  • w是命名解析器Resolve方法返回的watcher,该watcher可以监听命名解析器发来的地址信息变化,通知roundRobin对addrs中的地址进行动态的增删。
  • addrs是从命名解析器获取地址信息数组,数组中每个地址不仅有地址信息,还有gRPC与该地址是否已经创建了ready状态的连接的标记。
  • addrCh是地址数组的Channel,该Channel会在每次命名解析器发来地址信息变化后,将所有地址更新通知到gRPC内部的lbWatcher,lbWatcher是统一管理地址连接状态的协程,负责新地址的连接与被删除地址的关闭操作。
  • next是roundRobin的Index,即轮询调度遍历到addrs数组中的哪个位置了。
  • waitCh是当addrs中地址为空时,grpc调用Get()方法希望获取到一个到target的连接,如果设置了gRPC的failfast为false,那么Get()方法会阻塞在此Channel上,直到有ready的连接。

启动RoundRobin

启动RoundRobin就是实现Balancer接口的Start方法,该方法是由一开始通过grpc.WithBalancer把负载均衡器指定给的BalancerWrapperBuilder在创建BalancerWrapper时触发的:

func (bwb *balancerWrapperBuilder) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {// 这里触发Balancer的Start方法bwb.b.Start(opts.Target.Endpoint, BalancerConfig{DialCreds: opts.DialCreds,Dialer:    opts.Dialer,})_, pickfirst := bwb.b.(*pickFirst)bw := &balancerWrapper{......}cc.UpdateBalancerState(connectivity.Idle, bw)go bw.lbWatcher() // 监听Balancer 通知过来的地址变化return bw
}

Start方法其主要功能就是通过RoundRobin的命名解析器的Resolve方法拿到监听命名解析器后端变化的Watcher。与此同时还会新建一个addrChan用于向gRPC内部的lbWatcher推送Watcher监听到的地址变化。

func (rr *roundRobin) Start(target string, config BalancerConfig) error {rr.mu.Lock()defer rr.mu.Unlock()if rr.done {return ErrClientConnClosing}if rr.r == nil {// 如果没有解析器,那么直接将target加入addrs地址数组rr.addrs = append(rr.addrs, &addrInfo{addr: Address{Addr: target}})return nil}// Resolve接口会返回一个watcher,watcher可以监听解析器的地址变化w, err := rr.r.Resolve(target)if err != nil {return err}rr.w = w// 创建一个channel,当watcher监听到地址变化时,通知grpc内部lbWatcher去连接该地址rr.addrCh = make(chan []Address, 1)// go 创建新协程监听watcher,监听地址变化。go func() {for {if err := rr.watchAddrUpdates(); err != nil {return}}}()return nil
}

创建完addrCh后在Start方法最后会开启一个goroutine,这个goroutine会不停地循环调用watchAddrUpdates查询是否有命名解析器的Watcher传递过来的更新。

监听服务端地址的更新

watchAddrUpdates方法里就是通过上面Start方法里创建的Resolver Watcher的Next方法来监听Etcd上后端服务节点的更新,这个Watcher的实现就是上面服务发现章节里说的Etcd软件包里提供的gRPCWatcher类型,它的Next方法里会去通过监听Etcd上由服务名组成的Key的变化,然后在这里把这些信息传递给上面Start方法里创建好的addrChan通道。

func (rr *roundRobin) watchAddrUpdates() error {// watcher的next方法会阻塞,直至有地址变化信息过来,updates即为变化信息updates, err := rr.w.Next()if err != nil {return err}// 对于addrs地址数组的操作,显然是要加锁的,因为有多个goroutine在同时操作rr.mu.Lock()defer rr.mu.Unlock()for _, update := range updates {addr := Address{Addr:     update.Addr,Metadata: update.Metadata,}switch update.Op {case naming.Add://对于新增类型的地址,注意这里不会重复添加。var exist boolfor _, v := range rr.addrs {if addr == v.addr {exist = truebreak}}if exist {continue}rr.addrs = append(rr.addrs, &addrInfo{addr: addr})case naming.Delete://对于删除的地址,直接在addrs中删除就行了for i, v := range rr.addrs {if addr == v.addr {copy(rr.addrs[i:], rr.addrs[i+1:])rr.addrs = rr.addrs[:len(rr.addrs)-1]break}}default:grpclog.Errorln("Unknown update.Op ", update.Op)}}// 这里复制了整个addrs地址数组,然后丢到addrCh channel中通知grpc内部lbWatcher,// lbWatcher会关闭删除的地址,连接新增的地址。// 连接ready后会有专门的goroutine调用Up方法修改addrs中地址的状态。open := make([]Address, len(rr.addrs))for i, v := range rr.addrs {open[i] = v.addr}if rr.done {return ErrClientConnClosing}select {case <-rr.addrCh:default:}rr.addrCh <- openreturn nil
}

建立连接

Up方法是gRPC内部负载均衡的watcher调用的,该watcher会读全局的连接状态队列,改变RoundRobin维护的连接列表的里连接的状态 (会有单独的goroutine向目标服务发起连接尝试,尝试成功后才会把连接对象的连接状态改为connected),如果是已连接状态的连接 ,会调用Up方法来改变addrs地址数组中该地址的状态为已连接

func (rr *roundRobin) Up(addr Address) func(error) {rr.mu.Lock()defer rr.mu.Unlock()var cnt int//将地址数组中的addr置为已连接状态,这样这个地址就可以被client使用了。for _, a := range rr.addrs {if a.addr == addr {if a.connected {return nil}a.connected = true}if a.connected {cnt++}}// 当有一个可用地址时,之前可能是0个,可能要很多client阻塞在获取连接地址上,这里通知所有的client有可用连接啦。// 为什么只等于1时通知?因为可用地址数量>1时,client是不会阻塞的。if cnt == 1 && rr.waitCh != nil {close(rr.waitCh)rr.waitCh = nil}//返回禁用该地址的方法return func(err error) {rr.down(addr, err)}
}

关闭连接

关闭连接使用的是Down方法,这个方法就简单, 直接找到addr置为不可用就行了。

func (rr *roundRobin) down(addr Address, err error) {rr.mu.Lock()defer rr.mu.Unlock()for _, a := range rr.addrs {if addr == a.addr {a.connected = falsebreak}}
}

客户端获取连接

客户端在调用gRPC具体MethodInvoke方法里,会去RoundRobin的连接池addrs里获取连接,如果addrs为空,或者addrs里的地址都不可用,Get()方法会返回错误。但是如果设置了failfast = falseGet()方法会阻塞在waitCh这个通道上,直至Up方法给到通知,然后轮询调度可用的地址。

func (rr *roundRobin) Get(ctx context.Context, opts BalancerGetOptions) (addr Address, put func(), err error) {var ch chan struct{}rr.mu.Lock()if rr.done {rr.mu.Unlock()err = ErrClientConnClosingreturn}if len(rr.addrs) > 0 {// addrs的长度可能变化,如果next值超出了,就置为0,从头开始调度。if rr.next >= len(rr.addrs) {rr.next = 0}next := rr.next//遍历整个addrs数组,直到选出一个可用的地址for {a := rr.addrs[next]// next值加一,当然是循环的,到len(addrs)后,变为0next = (next + 1) % len(rr.addrs)if a.connected {addr = a.addrrr.next = nextrr.mu.Unlock()return}if next == rr.next {// 遍历完一圈了,还没找到,走下面逻辑break}}}if !opts.BlockingWait { //如果是非阻塞模式,如果没有可用地址,那么报错if len(rr.addrs) == 0 {rr.mu.Unlock()err = status.Errorf(codes.Unavailable, "there is no address available")return}// Returns the next addr on rr.addrs for failfast RPCs.addr = rr.addrs[rr.next].addrrr.next++rr.mu.Unlock()return}// Wait on rr.waitCh for non-failfast RPCs.// 如果是阻塞模式,那么需要阻塞在waitCh上,直到Up方法给通知if rr.waitCh == nil {ch = make(chan struct{})rr.waitCh = ch} else {ch = rr.waitCh}rr.mu.Unlock()for {select {case <-ctx.Done():err = ctx.Err()returncase <-ch:rr.mu.Lock()if rr.done {rr.mu.Unlock()err = ErrClientConnClosingreturn}if len(rr.addrs) > 0 {if rr.next >= len(rr.addrs) {rr.next = 0}next := rr.nextfor {a := rr.addrs[next]next = (next + 1) % len(rr.addrs)if a.connected {addr = a.addrrr.next = nextrr.mu.Unlock()return}if next == rr.next {// 遍历完一圈了,还没找到,可能刚Up的地址被down掉了,重新等待。break}}}// The newly added addr got removed by Down() again.if rr.waitCh == nil {ch = make(chan struct{})rr.waitCh = ch} else {ch = rr.waitCh}rr.mu.Unlock()}}
}

总结

整个gRPC基于Etcd实现服务注册/发现以及负载均衡的流程和关键的源码实现就梳理完了,其实源码实现的细节远比我这里列举的要复杂,这篇文章的目的也是希望能记录下一学习和实践gRPC的负载均衡和服务解析时的一些关键路径。另外需要注意的是本文里使用的是gRPC v1.2.x的代码,在1.3版本后官方包重新调整了目录和包名,与本文里列举的源码以及Balancer的使用上都会有些出入,不过原理还是大致一样的,只不过每一版都一直在此基础上演进。

看到这里了,如果喜欢我的文章可以帮我点个赞,我会每周通过技术文章分享我的所学所见和第一手实践经验,感谢你的支持。微信搜索关注公众号「网管叨bi叨」第一时间获取我的文章推送。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306706.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

堆问题(最小堆变最大堆,堆删除,中序遍历)

2-6 设最小堆&#xff08;小根堆&#xff09;的层序遍历结果为 {8, 38, 25, 58, 52, 82, 70, 60}。用线性时间复杂度的算法将该堆调整为最大堆&#xff08;大根堆&#xff09;&#xff0c;然后连续执行两次删除最大元素操作&#xff08;DeleteMax&#xff09;。则该树的中序遍历…

推荐一款.NET Core开源爬虫神器:DotnetSpider

没有爬虫就没有互联网&#xff01;爬虫的意义在于采集大批量数据&#xff0c;然后基于此进行加工/分析&#xff0c;做更有意义的事情。谷歌&#xff0c;百度&#xff0c;今日头条&#xff0c;天眼查都离不开爬虫。去开源中国和Github查询C#的爬虫项目&#xff0c;仅有几个非常简…

Excel学习使用教程

1.Excel的保存与加密 加密&#xff1a; 我设置的密码&#xff1a;517485

问题 D: 二叉树求高度

题目描述 已知一棵二叉树用邻接表结构存储&#xff0c;求这棵树的高度。例&#xff1a;如图二叉树的数据文件的数据格式如下: 输入 第一行n为二叉树的结点个树&#xff0c;n≤100&#xff1b;以下第一列数据是各结点的值&#xff0c;第二列数据是左儿子结点编号&#xff0c;第…

.Net Core in Docker - 使用阿里云Codepipeline及阿里云容器镜像服务实现持续集成(CI)...

前面已经介绍过了 .Net Core In Docker 在容器内编译并发布的内容。但是每次通过 SSH 链接到服务器敲命令&#xff0c;运行脚本也是挺麻烦的一件事。程序员是最懒的&#xff0c;能让电脑解决的问题绝不手动解决&#xff0c;如果当我们push一次代码后自动build代码&#xff0c;自…

mysql 序列_MySql中序列的应用和总结

Mysql中的序列主要用于主键&#xff0c;主键是递增的字段&#xff0c;不可重复。Mysql与Oracle不同的是&#xff0c;它不支持原生态的sequence&#xff0c;需要用表和函数的组合来实现类似序列的功能。1.首先创建序列的主表/*2.其次创建如下三个函数&#xff0c;它们的功能分别…

汉诺塔问题详细解析zufeoj

汉诺塔&#xff08;Tower of Hanoi&#xff09;&#xff0c;又称河内塔&#xff0c;是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子&#xff0c;在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重…

靠刷算法题,真的可以刷进大厂吗?

我一直不知道我在大家心目中的定位是什么&#xff0c;但我内心其实是把自己定义为一个『工具人』的。可能是因为我自己本身就是程序员&#xff0c;所以更能理解程序员的不易吧。所以&#xff0c;我尽量不写水文&#xff0c;只分享干货。就是希望大家看了能够有所收获&#xff0…

java 判断object类型_Java 类继承机制

封装、继承、多态是面向对象的三大特征&#xff0c;“继承”最主要的目的是为了实现代码的可复用性。通过父类与子类的继承关系&#xff0c;子类继承了父类的成员函数和成员变量&#xff0c;提高了代码的重复利用率。同时&#xff0c;子类也可以扩展自己特有的成员&#xff0c;…

一个情怀引发的生产事故(续)

接上一篇博文&#xff0c;用Roslyn动态编译C#语句片段&#xff0c;情怀了一把&#xff0c;但内存会飙升&#xff0c;执行速度也奇慢&#xff0c;这条路走不通&#xff0c;回归正道&#xff0c;说起脚本&#xff0c;Lua是常用的手段之一&#xff0c;那就看看NLua怎么样&#xff…

c++的unique函数

在STL中unique函数是一个去重函数&#xff0c; unique的功能是去除相邻的重复元素(只保留一个),其实它并不真正把重复的元素删除&#xff0c;是把重复的元素移到后面去了&#xff0c;然后依然保存到了原数组中&#xff0c;然后 返回去重后最后一个元素的地址&#xff0c;因为un…

用户登录查全表好还是用用户名好_外贸人/货代人不要为海运难过了:请看如何查运价和调配舱位解决缺箱!...

最近很多外贸人/货代人都被海运伤透了心&#xff0c;不仅价格上涨&#xff0c;还经常没舱位或缺柜子&#xff01;整个人的心态都不好了。其实呢运价上涨这个大环境趋势&#xff0c;我们也无法改变。但是没舱位和缺柜子是属于流动性的&#xff0c;只要不死盯一家船公司还是可以解…

BCVP开发者说第4期:Remember.Core

沉静岁月&#xff0c;淡忘流年1项目简介Remember.Core一个轻量的 Web 应用框架, 具有优雅、高效、简洁、富于表达力等优点。采用 前后端分离 设计&#xff0c;是崇尚开发效率的全栈框架简洁友好 - 统一的设计规范&#xff0c;精心打磨的操作界面回应你的期待。易扩展 - 一套完整…

c++十进制转二进制_二进制与十进制如何互相转换?

正整数的十进制转换二进制将一个十进制数除以二&#xff0c;得到的商再除以二&#xff0c;依此类推直到商等于一或零时为止&#xff0c;倒取除得的余数&#xff0c;即换算为二进制数的结果。只需记住要点&#xff1a;除二取余&#xff0c;倒序排列。由于计算机内部表示数的字节…

matlab eval函数_matlab自动给变量命名

在某些特定场景中&#xff0c;我们需要在一个循环中生成一系列的数据&#xff0c;并把这些数据保存到特定的变量中&#xff0c;这个时候我们就需要实现自动给变量命名&#xff0c;同时赋给变量数值。下面提供2种方法。方法1通过eval函数实现&#xff0c;举个例子clear%%%%%%%%%…

c++的STL中的map(哈希表)与unordered_map

map: unordered_map: map&#xff1a; map内部实现了一个红黑树&#xff0c;该结构具有自动排序的功能&#xff0c;因此map内部的所有元素都是有序的 unordered_map:unordered_map内部实现了一个哈希表&#xff0c;因此其元素的排列顺序是杂乱的&#xff0c;无序的 Map是STL的…

IdentityServer4系列 | 简化模式

一、前言从上一篇关于资源密码凭证模式中&#xff0c;通过使用client_id和client_secret以及用户名密码通过应用Client(客户端)直接获取&#xff0c;从而请求获取受保护的资源&#xff0c;但是这种方式存在client可能存了用户密码这不安全性问题&#xff0c;所以需要做到client…

char截取字符串_字符串的排列(滑动窗口)

题目&#xff1a;给定两个字符串 s1 和 s2&#xff0c;写一个函数来判断 s2 是否包含 s1 的排列。换句话说&#xff0c;第一个字符串的排列之一是第二个字符串的子串。示例1&#xff1a;输入: s1 "ab" s2 "eidbaooo"输出: True解释: s2 包含 s1 的排列之…

c++中的全排列函数next_permutation()

全排列函数next_permutation() prev_permutation函数&#xff08;按降序排序&#xff09; 计算序列全排列的函数&#xff1a;next_permutation&#xff08;start,end&#xff09;&#xff0c;此函数求的是当前排列的下一个排列&#xff0c;这里的“下一个”&#xff0c;我们可…

学习搭建 Consul 服务发现与服务网格-有丰富的示例和图片

第一部分&#xff1a;Consul 基础1&#xff0c;Consul 介绍官网文档描述&#xff1a;Consul 是一个网络工具&#xff0c;提供功能齐全的服务网格和服务发现。它可以做什么&#xff1a;自动化网络配置&#xff0c;发现服务并启用跨任何云或运行时的安全连接。那么&#xff0c;我…