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

34-SDK设计(下):IAM项目GoSDK设计和实现

 比如 Kubernetes的 client-go SDK设计方式。IAM项目参考client-go,也实现了client-go风格的SDK:marmotedu-sdk-go

 ,client-go风格的SDK具有以下优点:

  • 大量使用了Go interface特性,将接口的定义和实现解耦,可以支持多种实现方式。
  • 接口调用层级跟资源的层级相匹配,调用方式更加友好。
  • 多版本共存。

 

marmotedu-sdk-go设计

 

这里,我们先来看一个使用SDK调用iam-authz-server /v1/authz 接口的示例,代码保存在 marmotedu-sdk-go/examples/authz_clientset/main.go文件中:

package mainimport ("context""flag""fmt""path/filepath""github.com/ory/ladon"metav1 "github.com/marmotedu/component-base/pkg/meta/v1""github.com/marmotedu/component-base/pkg/util/homedir""github.com/marmotedu/marmotedu-sdk-go/marmotedu""github.com/marmotedu/marmotedu-sdk-go/tools/clientcmd"
)func main() {var iamconfig *stringif home := homedir.HomeDir(); home != "" {iamconfig = flag.String("iamconfig",filepath.Join(home, ".iam", "config"),"(optional) absolute path to the iamconfig file",)} else {iamconfig = flag.String("iamconfig", "", "absolute path to the iamconfig file")}flag.Parse()// use the current context in iamconfigconfig, err := clientcmd.BuildConfigFromFlags("", *iamconfig)if err != nil {panic(err.Error())}// create the clientsetclientset, err := marmotedu.NewForConfig(config)if err != nil {panic(err.Error())}request := &ladon.Request{Resource: "resources:articles:ladon-introduction",Action:   "delete",Subject:  "users:peter",Context: ladon.Context{"remoteIP": "192.168.0.5",},}// Authorize the requestfmt.Println("Authorize request...")ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})if err != nil {panic(err.Error())}fmt.Printf("Authorize response: %s.\n", ret.ToString())
}

在上面的代码示例中,包含了下面的操作。

  • 首先,调用 BuildConfigFromFlags 函数,创建出SDK的配置实例config;
  • 接着,调用 marmotedu.NewForConfig(config) 创建了IAM项目的客户端 clientset ;
  • 最后,调用以下代码请求 /v1/authz 接口执行资源授权请求:
ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})    
if err != nil {           panic(err.Error())    
}    fmt.Printf("Authorize response: %s.\n", ret.ToString())

调用格式为项目 客户端.应用客户端.服务客户端.资源名.接口 。

 

marmotedu-sdk-go客户端设计

 

Go项目的组织方式是有层级的:Project -> Application -> Service。marmotedu-sdk-go很好地体现了这种层级关系,使得SDK的调用更加易懂、易用。marmotedu-sdk-go的层级关系如下图所示:

marmotedu-sdk-go定义了3类接口,分别代表了项目、应用和服务级别的API接口:

// 项目级别的接口
type Interface interface {Iam() iam.IamInterfaceTms() tms.TmsInterface
}// 应用级别的接口
type IamInterface interface {APIV1() apiv1.APIV1InterfaceAuthzV1() authzv1.AuthzV1Interface
}// 服务级别的接口
type APIV1Interface interface {RESTClient() rest.InterfaceSecretsGetterUsersGetterPoliciesGetter
}// 资源级别的客户端
type SecretsGetter interface {Secrets() SecretInterface
}// 资源的接口定义
type SecretInterface interface {Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)Delete(ctx context.Context, name string, opts metav1.DeleteOptions) errorDeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) errorGet(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error)List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error)SecretExpansion
}

Interface 代表了项目级别的接口,里面包含了 Iam 和 Tms 两个应用; IamInterface 代表了应用级别的接口,里面包含了api(iam-apiserver)和authz(iam-authz-server)两个服务级别的接口。 

marmotedu-sdk-go通过 XxxV1 这种命名方式来支持不同版本的API接口,好处是可以在程序中同时调用同一个API接口的不同版本,例如:

clientset.Iam().AuthzV1().Authz().Authorize() 、clientset.Iam().AuthzV2().Authz().Authorize() 分别调用了 /v1/authz 和 /v2/authz 两个版本的API接口。

上述关系也可以从目录结构中反映出来,marmotedu-sdk-go目录设计如下(只列出了一些重要的文件):

├── examples                        # 存放SDK的使用示例
├── Makefile                        # 管理SDK源码,静态代码检查、代码格式化、测试、添加版权信息等
├── marmotedu
│   ├── clientset.go                # clientset实现,clientset中包含多个应用,多个服务的API接口
│   ├── fake                        # clientset的fake实现,主要用于单元测试
│   └── service                     # 按应用进行分类,存放应用中各服务API接口的具体实现
│       ├── iam                     # iam应用的API接口实现,包含多个服务
│       │   ├── apiserver           # iam应用中,apiserver服务的API接口,包含多个版本
│       │   │   └── v1              # apiserver v1版本API接口
│       │   ├── authz               # iam应用中,authz服务的API接口
│       │   │   └── v1              # authz服务v1版本接口
│       │   └── iam_client.go       # iam应用的客户端,包含了apiserver和authz 2个服务的客户端
│       └── tms                     # tms应用的API接口实现
├── pkg                             # 存放一些共享包,可对外暴露
├── rest                            # HTTP请求的底层实现
├── third_party                     # 存放修改过的第三方包,例如:gorequest
└── tools└── clientcmd                   # 一些函数用来帮助创建rest.Config配置

每种类型的客户端,都可以通过以下相似的方式来创建:

config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := xxxx.NewForConfig(config)

/root/.iam/config 为配置文件,里面包含了服务的地址和认证信息。BuildConfigFromFlags 函数加载配置文件,创建并返回 rest.Config 类型的配置变量,并通过 xxxx.NewForConfig 函数创建需要的客户端。xxxx 是所在层级的client包,例如 iam、tms。

marmotedu-sdk-go客户端定义了3类接口,这可以带来两个好处。

第一,API接口调用格式规范,层次清晰,可以使API接口调用更加清晰易记。

第二,可以根据需要,自行选择客户端类型,调用灵活。举个例子,在A服务中需要同时用到iam-apiserver 和 iam-authz-server提供的接口,就可以创建应用级别的客户端IamClient,然后通过 iamclient.APIV1() 和 iamclient.AuthzV1() ,来切换调用不同服务的API接口。

接下来,我们来看看如何创建三个不同级别的客户端。

项目级别客户端创建

Interface 对应的客户端实现为Clientset,所在的包为 marmotedu-sdk-go/marmotedu,Clientset客户端的创建方式为:

config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := marmotedu.NewForConfig(config)

调用方式为 clientset.应用.服务.资源名.接口 ,例如:

rsp, err := clientset.Iam().AuthzV1().Authz().Authorize()

参考示例为 marmotedu-sdk-go/examples/authz_clientset/main.go。

应用级别客户端创建

IamInterface 对应的客户端实现为IamClient,所在的包为 marmotedu-sdk-go/marmotedu/service/iam,IamClient客户端的创建方式为:

config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
iamclient,, err := iam.NewForConfig(config)

调用方式为 iamclient.服务.资源名.接口 ,例如:

rsp, err := iamclient.AuthzV1().Authz().Authorize()

参考示例为 marmotedu-sdk-go/examples/authz_iam/main.go。

服务级别客户端创建

AuthzV1Interface 对应的客户端实现为AuthzV1Client,所在的包为 marmotedu-sdk-go/marmotedu/service/iam/authz/v1,AuthzV1Client客户端的创建方式为:

config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
client, err := v1.NewForConfig(config)

调用方式为 client.资源名.接口 ,例如:

rsp, err := client.Authz().Authorize()

参考示例为 marmotedu-sdk-go/examples/authz/main.go。

上面我介绍了marmotedu-sdk-go的客户端创建方法,接下来我们再来看下,这些客户端具体是如何执行REST API请求的。

marmotedu-sdk-go的实现

marmotedu-sdk-go的实现和medu-sdk-go一样,也是采用分层结构,分为API层和基础层。如下图所示:

RESTClient是整个SDK的核心,RESTClient向下通过调用Request模块,来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用gorequest包提供的方法,完成HTTP的POST、PUT、GET、DELETE等请求,获取HTTP返回结果,并解析到指定的结构体中。RESTClient向上提供 Post() 、 Put() 、 Get() 、 Delete() 等方法来供客户端完成HTTP请求。

marmotedu-sdk-go提供了两类客户端,分别是RESTClient客户端和基于RESTClient封装的客户端。

  • RESTClient:Raw类型的客户端,可以通过指定HTTP的请求方法、请求路径、请求参数等信息,直接发送HTTP请求,例如 client.Get().AbsPath("/version").Do().Into() 。
  • 基于RESTClient封装的客户端:例如AuthzV1Client、APIV1Client等,执行特定REST资源、特定API接口的请求,方便开发者调用。

接下来,我们具体看下如何创建RESTClient客户端,以及Request模块的实现。

RESTClient客户端实现

我通过下面两个步骤,实现了RESTClient客户端。

第一步,创建rest.Config类型的变量。

BuildConfigFromFlags函数通过加载yaml格式的配置文件,来创建 rest.Config 类型的变量,加载的yaml格式配置文件内容为:

apiVersion: v1
user:#token: # JWT Tokenusername: admin # iam 用户名password: Admin@2020 # iam 密码#secret-id: # 密钥 ID#secret-key: # 密钥 Keyclient-certificate: /home/colin/.iam/cert/admin.pem # 用于 TLS 的客户端证书文件路径client-key: /home/colin/.iam/cert/admin-key.pem # 用于 TLS 的客户端 key 文件路径#client-certificate-data:#client-key-data:server:address: https://127.0.0.1:8443 # iam api-server 地址timeout: 10s # 请求 api-server 超时时间#max-retries: # 最大重试次数,默认为 0#retry-interval: # 重试间隔,默认为 1s#tls-server-name: # TLS 服务器名称#insecure-skip-tls-verify: # 设置为 true 表示跳过 TLS 安全验证模式,将使得 HTTPS 连接不安全certificate-authority: /home/colin/.iam/cert/ca.pem # 用于 CA 授权的 cert 文件路径#certificate-authority-data:

在配置文件中,我们可以指定服务的地址、用户名/密码、密钥、TLS证书、超时时间、重试次数等信息。

创建方法如下:

config, err := clientcmd.BuildConfigFromFlags("", *iamconfig)    
if err != nil {                                                  panic(err.Error())    
}  

这里的代码中,*iamconfig 是yaml格式的配置文件路径。BuildConfigFromFlags 函数中,调用LoadFromFile函数来解析yaml配置文件。LoadFromFile最终是通过 yaml.Unmarshal 的方式来解析yaml格式的配置文件的。

第二步,根据rest.Config类型的变量,创建RESTClient客户端。

通过RESTClientFor函数来创建RESTClient客户端:

func RESTClientFor(config *Config) (*RESTClient, error) {...baseURL, versionedAPIPath, err := defaultServerURLFor(config)if err != nil {return nil, err}// Get the TLS options for this client configtlsConfig, err := TLSConfigFor(config)if err != nil {return nil, err}// Only retry when get a server side error.client := gorequest.New().TLSClientConfig(tlsConfig).Timeout(config.Timeout).Retry(config.MaxRetries, config.RetryInterval, http.StatusInternalServerError)// NOTICE: must set DoNotClearSuperAgent to true, or the client will clean header befor http.Doclient.DoNotClearSuperAgent = true...clientContent := ClientContentConfig{Username:           config.Username,Password:           config.Password,SecretID:           config.SecretID,SecretKey:          config.SecretKey,...}return NewRESTClient(baseURL, versionedAPIPath, clientContent, client)
}

RESTClientFor函数调用defaultServerURLFor(config)生成基本的HTTP请求路径:baseURL=http://127.0.0.1:8080,versionedAPIPath=/v1。然后,通过TLSConfigFor函数生成TLS配置,并调用 gorequest.New() 创建gorequest客户端,将客户端配置信息保存在变量中。最后,调用NewRESTClient函数创建RESTClient客户端。

RESTClient客户端提供了以下方法,来供调用者完成HTTP请求:

func (c *RESTClient) APIVersion() scheme.GroupVersion
func (c *RESTClient) Delete() *Request
func (c *RESTClient) Get() *Request
func (c *RESTClient) Post() *Request
func (c *RESTClient) Put() *Request
func (c *RESTClient) Verb(verb string) *Request

可以看到,RESTClient提供了 Delete 、 Get 、 Post 、 Put 方法,分别用来执行HTTP的DELETE、GET、POST、PUT方法,提供的 Verb 方法可以灵活地指定HTTP方法。这些方法都返回了 Request 类型的变量。Request 类型的变量提供了一些方法,用来完成具体的HTTP请求,例如:

  type Response struct {Allowed bool   `json:"allowed"`Denied  bool   `json:"denied,omitempty"`Reason  string `json:"reason,omitempty"`Error   string `json:"error,omitempty"`
}func (c *authz) Authorize(ctx context.Context, request *ladon.Request, opts metav1.AuthorizeOptions) (result *Response, err error) {result = &Response{}                                         err = c.client.Post().Resource("authz").VersionedParams(opts).Body(request).Do(ctx).Into(result)return
}

上面的代码中, c.client 是RESTClient客户端,通过调用RESTClient客户端的 Post 方法,返回了 *Request 类型的变量。

*Request 类型的变量提供了 Resource 和 VersionedParams 方法,来构建请求HTTP URL中的路径 /v1/authz ;通过 Body 方法,指定了HTTP请求的Body。

到这里,我们分别构建了HTTP请求需要的参数:HTTP Method、请求URL、请求Body。所以,之后就可以调用 Do 方法来执行HTTP请求,并将返回结果通过 Into 方法保存在传入的result变量中。

Request模块实现

RESTClient客户端的方法会返回Request类型的变量,Request类型的变量提供了一系列的方法用来构建HTTP请求参数,并执行HTTP请求。

所以,Request模块可以理解为最底层的通信层,我们来看下Request模块具体是如何完成HTTP请求的。

我们先来看下Request结构体的定义:

type RESTClient struct {           // base is the root URL for all invocations of the client    base *url.URL    // group stand for the client group, eg: iam.api, iam.authz                       group string                                                                          // versionedAPIPath is a path segment connecting the base URL to the resource root    versionedAPIPath string                                      // content describes how a RESTClient encodes and decodes responses.    content ClientContentConfig    Client  *gorequest.SuperAgent    
}type Request struct {c *RESTClienttimeout time.Duration// generic components accessible via method settersverb       stringpathPrefix stringsubpath    stringparams     url.Valuesheaders    http.Header// structural elements of the request that are part of the IAM API conventions// namespace    string// namespaceSet boolresource     stringresourceName stringsubresource  string// outputerr  errorbody interface{}
}  

再来看下Request结构体提供的方法:

func (r *Request) AbsPath(segments ...string) *Request
func (r *Request) Body(obj interface{}) *Request
func (r *Request) Do(ctx context.Context) Result
func (r *Request) Name(resourceName string) *Request
func (r *Request) Param(paramName, s string) *Request
func (r *Request) Prefix(segments ...string) *Request
func (r *Request) RequestURI(uri string) *Request
func (r *Request) Resource(resource string) *Request
func (r *Request) SetHeader(key string, values ...string) *Request
func (r *Request) SubResource(subresources ...string) *Request
func (r *Request) Suffix(segments ...string) *Request
func (r *Request) Timeout(d time.Duration) *Request
func (r *Request) URL() *url.URL
func (r *Request) Verb(verb string) *Request
func (r *Request) VersionedParams(v interface{}) *Request

通过Request结构体的定义和使用方法,我们不难猜测出:Request模块通过 Name 、 Resource 、 Body 、 SetHeader 等方法来设置Request结构体中的各个字段。这些字段最终用来构建出一个HTTP请求,并通过 Do 方法来执行HTTP请求。

那么,如何构建并执行一个HTTP请求呢?我们可以通过以下5步,来构建并执行HTTP请求:

  1. 构建HTTP URL;
  2. 构建HTTP Method;
  3. 构建HTTP Body;
  4. 执行HTTP请求;
  5. 保存HTTP返回结果。

接下来,我们就来具体看下Request模块是如何构建这些请求参数,并发送HTTP请求的。

第一步,构建HTTP URL。

首先,通过defaultServerURLFor函数返回了http://iam.api.marmotedu.com:8080 和 /v1 ,并将二者分别保存在了Request类型结构体变量中 c 字段的 base 字段和 versionedAPIPath 字段中。

通过 Do 方法执行HTTP时,会调用r.URL()方法来构建请求URL。 r.URL 方法中,通过以下代码段构建了HTTP请求URL:

func (r *Request) URL() *url.URL {p := r.pathPrefixif len(r.resource) != 0 {p = path.Join(p, strings.ToLower(r.resource))}if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {p = path.Join(p, r.resourceName, r.subresource, r.subpath)}finalURL := &url.URL{}if r.c.base != nil {*finalURL = *r.c.bas}finalURL.Path = p...    
}

p := r.pathPrefix 和 r.c.base ,是通过 defaultServerURLFor 调用返回的 v1 和 http://iam.api.marmotedu.com:8080 来构建的。

resourceName 通过 func (r *Request) Resource(resource string) *Request 来指定,例如 authz 。

所以,最终我们构建的请求URL为 http://iam.api.marmotedu.com:8080/v1/authz 。

第二步,构建HTTP Method。

HTTP Method通过RESTClient提供的 Post 、Delete 、Get 等方法来设置,例如:

func (c *RESTClient) Post() *Request {                                                                                 return c.Verb("POST")                                                                                              
}func (c *RESTClient) Verb(verb string) *Request {                                                                      return NewRequest(c).Verb(verb)                                                                                    
}

NewRequest(c).Verb(verb) 最终设置了Request结构体的 verb 字段,供 Do 方法使用。

第三步,构建HTTP Body。

HTTP Body通过Request结构体提供的Body方法来指定:

func (r *Request) Body(obj interface{}) *Request {                    if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {              r.SetHeader("Content-Type", r.c.content.ContentType)                }                                                                                                                  r.body = obj                                                                                                       return r                                                                                                           
} 

第四步,执行HTTP请求。

通过Request结构体提供的Do方法来执行具体的HTTP请求,代码如下:

func (r *Request) Do(ctx context.Context) Result {client := r.c.Clientclient.Header = r.headersif r.timeout > 0 {var cancel context.CancelFuncctx, cancel = context.WithTimeout(ctx, r.timeout)defer cancel()}client.WithContext(ctx)resp, body, errs := client.CustomMethod(r.verb, r.URL().String()).Send(r.body).EndBytes()if err := combineErr(resp, body, errs); err != nil {return Result{response: &resp,err:      err,body:     body,}}decoder, err := r.c.content.Negotiator.Decoder()if err != nil {return Result{response: &resp,err:      err,body:     body,decoder:  decoder,}}return Result{response: &resp,body:     body,decoder:  decoder,}
}

在Do方法中,使用了Request结构体变量中各个字段的值,通过 client.CustomMethod 来执行HTTP请求。 client 是 *gorequest.SuperAgent 类型的客户端。

第五步,保存HTTP返回结果。

通过Request结构体的 Into 方法来保存HTTP返回结果:

func (r Result) Into(v interface{}) error {if r.err != nil {                                          return r.Error()}                                                                                 if r.decoder == nil {                                                                    return fmt.Errorf("serializer doesn't exist")}                            if err := r.decoder.Decode(r.body, &v); err != nil {return err                                                                    }                                                                                        return nil                                                                      
}

r.body 是在Do方法中,执行完HTTP请求后设置的,它的值为HTTP请求返回的Body。

请求认证

接下来,我再来介绍下marmotedu-sdk-go另外一个比较核心的功能:请求认证。

marmotedu-sdk-go支持两种认证方式:

  • Basic认证:通过给请求添加 Authorization: Basic xxxx 来实现。
  • Bearer认证:通过给请求添加 Authorization: Bearer xxxx 来实现。这种方式又支持直接指定JWT Token,或者通过指定密钥对由SDK自动生成JWT Token。

Basic认证和Bearer认证,我在 25讲介绍过,你可以返回查看下。

认证头是RESTClient客户端发送HTTP请求时指定的,具体实现位于NewRequest函数中:

switch {case c.content.HasTokenAuth():r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.content.BearerToken))case c.content.HasKeyAuth():tokenString := auth.Sign(c.content.SecretID, c.content.SecretKey, "marmotedu-sdk-go", c.group+".marmotedu.com")r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", tokenString))case c.content.HasBasicAuth():// TODO: get token and set headerr.SetHeader("Authorization", "Basic "+basicAuth(c.content.Username, c.content.Password))
}

上面的代码会根据配置信息,自动判断使用哪种认证方式。

总结

 

marmotedu-sdk-go在设计时,通过接口实现了3类客户端,分别是项目级别的客户端、应用级别的客户端和服务级别的客户端。开发人员可以根据需要,自行创建客户端类型。

marmotedu-sdk-go通过RESTClientFor,创建了RESTClient类型的客户端,RESTClient向下通过调用Request模块,来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用gorequest包提供的方法,完成HTTP的POST、PUT、GET、DELETE等请求,获取HTTP返回结果,并解析到指定的结构体中。RESTClient向上提供 Post() 、 Put() 、 Get() 、 Delete() 等方法,来供客户端完成HTTP请求。

课后练习

  1. 阅读defaultServerURLFor源码,思考下defaultServerURLFor是如何构建请求地址 http://iam.api.marmotedu.com:8080 和API版本 /v1 的。
  2. 使用gorequest包,编写一个可以执行以下HTTP请求的示例:
curl -XPOST http://example.com/v1/user -d '{"username":"colin","address":"shenzhen"}'

相关文章:

34-SDK设计(下):IAM项目GoSDK设计和实现

比如 Kubernetes的 client-go SDK设计方式。IAM项目参考client-go,也实现了client-go风格的SDK:marmotedu-sdk-go。 ,client-go风格的SDK具有以下优点: 大量使用了Go interface特性,将接口的定义和实现解耦&#xff0…...

基于Matlab的血管图像增强算法,Matlab实现

博主简介: 专注、专一于Matlab图像处理学习、交流,matlab图像代码代做/项目合作可以联系(QQ:3249726188) 个人主页:Matlab_ImagePro-CSDN博客 原则:代码均由本人编写完成,非中介,提供…...

LeetCode每日一题之专题一:双指针 ——复写零

复写零OJ链接:1089. 复写零 - 力扣(LeetCode) 题目: 解法(原地复写-双指针): 算法思路: 如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致…...

Golang基础-9

Go语言基础 介绍 基础 结构体 自定义类型 结构体定义 结构体声明 结构体初始化 字段访问与修改 匿名结构体 结构体嵌套 初始化函数定义 介绍 本文介绍Go语言中自定义类型、结构体定义、结构体声明、结构体初始化、字段访问与修改、匿名结构体、结构体嵌套、初始化…...

Vue基础知识:路由的封装抽离,路由模块的封装抽离的好处是什么?,如何快速的引入组件,基于@指代src目录,从src目录出发找组件

如果将所有的路由配置都存放在main.js中,是非常有问题的,杂且乱。所以我们要将路由模块进行抽离,这样有利于:拆分模块,利于维护。大致的做法就是将路由相关的东西放到router这个文件夹的index.js中,而将来只…...

插入排序---算法

1、算法概念 插入排序:它的工作原理是通过构建有序排序,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。 2、算法步骤 将第一待排序序列第一个元素看作一个有序序列,把第二个元素到最后一个元素当成是…...

Vue3 Vite 整合组件脚手架笔记

序号更新时间备注12024.04.03初始化整理笔记 目录 一、安装运行命令二、相关依赖内容 1、http客户端 - alova2、国际化 - I18n3、时间管理 - moment4、pdf预览 - pdfjs-dist5、doc预览 - docx-preview6、请求参数处理 - qs7、全局状态管理 - Pinia8、路由管理 - vue-router9、…...

续二叉搜索树递归玩法

文章目录 一、插入递归二、寻找递归&#xff08;非常简单&#xff0c;走流程就行&#xff09;三、插入递归&#xff08;理解起来比较麻烦&#xff09; 先赞后看&#xff0c;养成习惯&#xff01;&#xff01;&#xff01;^ _ ^<3 ❤️ ❤️ ❤️ 码字不易&#xff0c;大家的…...

DDD 的四层领域模型是怎样的?包含哪些基础概念?

DDD的四层领域模型如下所示&#xff1a; 展现层&#xff1a;这一层负责向用户显示信息和解释用户命令&#xff0c;完成前端界面逻辑。并将用户请求传递给应用层。应用层&#xff1a;这一层是很薄的一层&#xff0c;负责协调领域层中的领域对象&#xff0c;组成具体应用场景。应…...

AI 在医疗保健领域的应用:技术、趋势和前景

人工智能&#xff08;AI&#xff09;在医疗保健领域的应用已经成为引人瞩目的发展方向&#xff0c;其在医学影像分析、疾病诊断和个性化治疗等方面展现出了巨大潜力。本文将深入探讨这些技术应用和未来的发展趋势。 医学影像分析 医学影像分析是AI在医疗领域中应用最广泛的领…...

SVG XML 格式定义图形入门介绍

SVG SVG means Scalable Vector Graphics. SVG 使用 XML 格式定义图形SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失SVG 是万维网联盟的标准 Hello World Use SVG in html and you can see: Link to the SVG file You can use <a> tag to link to the svg…...

MYSQL数据库的故障排除与优化

目录 一.MySQL单实例故障排查 故障现象1 故障现象 2 故障现象 3 故障现象 4 故障现象 5 故障现象 6 故障现象 7 故障现象 8 二.主从环境常见故障 1.故障一 2. 故障二 3. 故障三 三. 优化 1.SQL优化 2. 架构优化 3.硬件方面 1.1 关于CPU 1.2 关于内存 1.3 关…...

C++从入门到精通——入门知识

1. C关键字(C98) C总计63个关键字&#xff0c;C语言32个关键字 2. 命名空间 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称都将存在于全局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的就是对标识符的名…...

一些题目学习

1.打开文件添加helloworld public class Saier {public static void main(String[] args){String path"C:\\Users\\sjg\\Desktop\\abc.txt";String text"hello world";try {File file new File(path);FileWriter fileWriter new FileWriter(file,true);…...

Linux上管理文件系统

Linux上管理文件系统 机械硬盘 机械硬盘由多块盘片组成&#xff0c;它们都绕着主轴旋转。每块盘片上下方都有读写磁头悬浮在盘片上下方&#xff0c;它们与盘片的距离极小。在每次读写数据时盘片旋转&#xff0c;读写磁头被磁臂控制着不断的移动来读取其中的数据。 所有的盘片…...

【Linux】寿司线程池{单例模式之懒汉模式下的线程池}

文章目录 回顾单例模式0.多线程下的单例模式的意义1.什么是单例模式1.0设计模式1.1C单例模式的介绍及原理1.2拷贝构造和赋值重载的处理1.3if (nullptr ptr)&#xff0c;nullptr放在比较运算符的前面?1.4实现单例模式的方式 2.实现懒汉方式的单例模式2.1单线程的单例模式2.2多…...

Docker资源管理和分配指南

什么是cgroup&#xff1f; cgroups其名称源自控制组群&#xff08;control groups&#xff09;的简写&#xff0c;是Linux内核的一个功能&#xff0c;用来限制、控制与分离一个进程组&#xff08;如CPU、内存、磁盘输入输出等&#xff09;。 什么是Docker资源限制&#xff1f;…...

为什么索引的底层结构是B+树

B树 1.数据库与数据交互的单位是page,而B树的每个节点都是一个page,访问一个节点&#xff0c;就相当于进行了一次I/O操作。所以访问的节点越少&#xff0c;查找效率越大。而B树是矮胖的&#xff0c;查找深度也不会太大。 2.B树中的节点是有序存储的&#xff0c;对于范围查询、排…...

NLP学习路线指南总结

当然可以&#xff0c;以下是一份较为详细的NLP学习路线指南&#xff0c;帮助你逐步掌握自然语言处理的核心技术和应用。 一、基础知识与技能 语言学基础&#xff1a; 语言学基本概念&#xff1a;语音、语法、语义等。语言的层次与分类&#xff1a;语音学、音系学、句法学、语…...

试过了,ChatGPT确实不用注册就可以使用了!

看到官网说不用登录也可以直接使用ChatGPT 我们来试一下 直接打开官网 默认是直接进入了chatgpt3.5的聊天界面 之前是默认进的登录页面 聊一下试试 直接回复了&#xff0c;目前属于未登录状态&#xff0c;挺好&#xff01; 来试下ChatGPT4 跳转到了登录页面 目前来看gpt4还…...

CANoe自带的TCP/IP协议栈中TCP的keep alive机制是如何工作的

TCP keep alive机制我们已经讲过太多次,车内很多控制器的TCP keep alive机制相信很多开发和测试的人也配置或者测试过。我们今天想知道CANoe软件自带的TCP/IP协议栈中TCP keep alive机制是如何工作的。 首先大家需要知道TCP keep alive的参数有哪些?其实就三个参数:CP_KEEP…...

【C++练级之路】【Lv.18】哈希表(哈希映射,光速查找的魔法)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、哈希1.1 哈希概念1.2 哈希函数1.3 哈希冲突 二、闭散列2.1 数据类型2.2 成员变量2.3 默认成员函数2.…...

「PHP系列」If...Else语句/switch语句

文章目录 一、If...Else语句1. 基本语法2. 带有 elseif 的语法3. 示例示例 1&#xff1a;基本 if...else 结构示例 2&#xff1a;使用 elseif示例 3&#xff1a;嵌套 if...else 结构 4. 注意事项 二、switch语句1. 基本语法2. 示例示例 1&#xff1a;基本 switch 结构示例 2&am…...

Ubuntu部署BOA服务器

BOA服务器概述 BOA是一款非常小巧的Web服务器&#xff0c;源代码开放、性能优秀、支持CGI通用网关接口技术&#xff0c;特别适合用在嵌入式系统中。 BOA服务器主要功能是在互联嵌入式设备之间进行信息交互&#xff0c;达到通用网络对嵌入式设备进行监控&#xff0c;并将反馈信…...

安卓Glide加载失败时点击按钮重新加载图片

需求 假设此时已经用load指定一个url: String&#xff0c;又用into指定了一个img: ImageView开始加载&#xff0c;但是网络突然中断&#xff0c;导致图片加载失败。在这种情况下&#xff0c;想要通过点击一个Button重新加载。 Glide.with(context).load(url).placeholder(loa…...

linux下python服务定时(自)启动

AI应用开发相关目录 本专栏包括AI应用开发相关内容分享&#xff0c;包括不限于AI算法部署实施细节、AI应用后端分析服务相关概念及开发技巧、AI应用后端应用服务相关概念及开发技巧、AI应用前端实现路径及开发技巧 适用于具备一定算法及Python使用基础的人群 AI应用开发流程概…...

awk命令进阶操作(二)

awk模块 awk模块awk的BEGIN模块和END模块BEGIN模块BEGIN 常见错误END模块END模块 常见错误 案例计算1~100的累加和统计系统中有多少用户的shell类型是/bin/bash awk模块 awk的BEGIN模块和END模块 格式 awk BEGIN{}{}END{} 文件名BEGIN模块 用于定义一个动作&#xff0c;用{…...

【洛谷 P8695】[蓝桥杯 2019 国 AC] 轨道炮 题解(映射+模拟+暴力枚举+桶排序)

[蓝桥杯 2019 国 AC] 轨道炮 题目描述 小明在玩一款战争游戏。地图上一共有 N N N 个敌方单位&#xff0c;可以看作 2D 平面上的点。其中第 i i i 个单位在 0 0 0 时刻的位置是 ( X i , Y i ) (X_i, Y_i) (Xi​,Yi​)&#xff0c;方向是 D i D_i Di​ (上下左右之一, 用…...

高阶DS---AVL树详解(每步配图)

目录 前言&#xff1a; AVL树的概念: AVL树节点的定义&#xff1a; AVL树的插入&#xff08;重点&#xff09; AVL树的旋转&#xff1a; &#xff08;1&#xff09;新节点插入较高左子树的左侧---右单旋 &#xff08;2&#xff09;新节点插入较高右子树的右侧---左单旋 …...

c++前言

目录 1. 什么是 C 2. C 发展史 3. C 的重要性 4. 如何学习 C 5. 关于本门课程 1. 什么是C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的 程序&#xff0c;需要高度的抽象和建模时&#xff0c; C 语言则不合适…...

linux服务器下如何新建网站/厦门seo排名优化公司

文章目录一、使用场合二、磁环的作用1. 防止大功率设备影响到数据线2. 防止数据线影响到设备接收灵敏度三、磁环EMC应用一、使用场合 产品使用形态上常见的是&#xff0c;两种电子产品通过数据线相连进行数据交互或者主从控制。尤其是在通信产品中&#xff0c;为了扩展通信产品…...

梅花手表网站/百度网盘官网入口

1. 运算符优先级与括号 #define Cube(a) a*a*a 无法解决 Cube(11) ⇒ 11*11*11 ⇒ 4&#xff0c;期待的应当是 8&#xff0c;故将其改造为 #define Cube(a) (a)*(a)*(a) 如此&#xff0c;自身运算的优先级是能解决了&#xff0c;和其他表达式结合时便又存在先运算和后运算的算…...

网站字号/网络广告一般是怎么收费

本节书摘来自异步社区《树莓派开发实战&#xff08;第2版&#xff09;》一书中的第2章&#xff0c;第2.4节&#xff0c;作者[英]Simon Monk&#xff08;蒙克&#xff09;&#xff0c;韩波 译&#xff0c;更多章节内容可以访问云栖社区“异步社区”公众号查看。 2.4 为树莓派配…...

跟有流量的网站做友情链接/今日头条号官网

说明&#xff1a; &#xff08;1&#xff09;在本专栏中&#xff0c;我们会通过开发【Spring Cloud开发课程查询项目】来介绍Spring Cloud&#xff1b;其中&#xff0c;具体的项目业务内容不是重点&#xff0c;重点是Spring Cloud的开发和各组件的使用套路&#xff1b; 一&…...

wordpress自动更新表格/怎么弄一个网站平台

param参数ajax param()方法 语法作用&#xff1a;param() 方法创建数组或对象的序列化表示。该序列化值可在进行 AJAX 请求时在 URL 查询字符串中使用。语法&#xff1a;jQuery.param(object,traditional)参数&#xff1a;参数描述object要进行序列化的数组或对象。traditional…...

郑州汉狮做网站网络公司/100大看免费行情的软件

2019独角兽企业重金招聘Python工程师标准>>> https://open.weixin.qq.com/cgi-bin/showdocument?actiondir_list&tresource/res_list&verify1&idopen1419317332&token&langzh_CN 分享后实现这个显示效果 需要申独申请权限才可以发送成功&#…...