33-SDK设计(上):如何设计出一个优秀的GoSDK?

 

 

在实际的项目开发中,通常会提供对开发者更友好的SDK包,供客户端调用。很多大型服务在发布时都会伴随着SDK的发布,例如腾讯云很多产品都提供了SDK:

图片

 

什么是SDK?

 

对于SDK(Software Development Kit,软件开发工具包), SDK通常是指封装了Go后端服务API接口的软件包,里面通常包含了跟软件相关的库、文档、使用示例、封装好的API接口和工具。

 

通常,服务提供者会提供不同语言的SDK,比如针对Python开发者会提供Python版的SDK,针对Go开发者会提供Go版的SDK。 例如,Protocol Buffers的编译工具protoc,就可以基于Protobuf文件生成C++、Python、Java、JavaScript、PHP等语言版本的SDK。阿里云、腾讯云这些一线大厂,也可以基于API定义,生成不同编程语言的SDK。

SDK设计方法

 

如何给SDK命名?

 SDK的命名方式和SDK的目录结构。

SDK的名字目前没有统一的规范,但比较常见的命名方式是 xxx-sdk-go / xxx-sdk-python / xxx-sdk-java 。其中, xxx 可以是项目名或者组织名,例如腾讯云在GitHub上的组织名为tencentcloud,那它的SDK命名如下图所示:

图片

SDK的目录结构  

  • README.md:SDK的帮助文档,里面包含了安装、配置和使用SDK的方法。
  • examples/sample/:SDK的使用示例。
  • sdk/:SDK共享的包,里面封装了最基础的通信功能。如果是HTTP服务,基本都是基于 net/http 包进行封装。
  • api:如果 xxx-sdk-go 只是为某一个服务提供SDK,就可以把该服务的所有API接口封装代码存放在api目录下。
  • services/{iam, tms} :如果 xxx-sdk-go 中, xxx 是一个组织,那么这个SDK很可能会集成该组织中很多服务的API,就可以把某类服务的API接口封装代码存放在 services/<服务名>下,例如AWS的Go SDK。

一个典型的目录结构如下:

├── examples            # 示例代码存放目录
│   └── authz.go
├── README.md           # SDK使用文档
├── sdk                 # 公共包,封装了SDK配置、API请求、认证等代码
│   ├── client.go
│   ├── config.go
│   ├── credential.go
│   └── ...
└── services            # API封装├── common│   └── model├── iam             # iam服务的API接口│   ├── authz.go│   ├── client.go│   └── ...└── tms             # tms服务的API接口

SDK设计方法

SDK的设计方法如下图所示:

我们可以通过Config配置创建客户端Client,例如 func NewClient(config sdk.Config) (Client, error),配置中可以指定下面的信息。

  • 服务的后端地址:服务的后端地址可以通过配置文件来配置,也可以直接固化在SDK中,推荐后端服务地址可通过配置文件配置。
  • 认证信息:最常用的认证方式是通过密钥认证,也有一些是通过用户名和密码认证。
  • 其他配置:例如超时时间、重试次数、缓存时间等。

创建的Client是一个结构体或者Go interface。这里我建议你使用interface类型,这样可以将定义和具体实现解耦。Client具有一些方法,例如 CreateUser、DeleteUser等,每一个方法对应一个API接口,下面是一个Client定义:

type Client struct {client *sdk.Request
}func (c *Client) CreateUser(req *CreateUserRequest) (*CreateUserResponse, error) {// normal coderesp := &CreateUserResponse{}err := c.client.Send(req, resp)return resp, err
}

调用 client.CreateUser(req) 会执行HTTP请求,在 req 中可以指定HTTP请求的方法Method、路径Path和请求Body。 CreateUser 函数中,会调用 c.client.Send(req) 执行具体的HTTP请求。

c.client 是 *Request 类型的变量, *Request 类型的变量具有一些方法,可以根据传入的请求参数 req 和 config 配置构造出请求路径、认证头和请求Body,并调用 net/http 包完成最终的HTTP请求,最后将返回结果Unmarshal到传入的 resp 结构体中。

根据我的调研,目前有两种SDK设计方式可供参考,一种是各大公有云厂商采用的SDK设计方式,一种是Kubernetes client-go的设计方式。 

公有云厂商采用的SDK设计方式

这里,我先来简单介绍下公有云厂商采用的SDK设计模式。SDK架构如下图所示:

SDK框架分为两层,分别是API层和基础层。API层主要用来构建客户端实例,并调用客户端实例提供的方法来完成API请求,每一个方法对应一个API接口。API层最终会调用基础层提供的能力,来完成REST API请求。基础层通过依次执行构建请求参数(Builder)、签发并添加认证头(Signer)、执行HTTP请求(Request)三大步骤,来完成具体的REST API请求。

为了让你更好地理解公有云SDK的设计方式,接下来我会结合一些真实的代码,给你讲解API层和基础层的具体设计,SDK代码见medu-sdk-go。

API层:创建客户端实例

客户端在使用服务A的SDK时,首先需要根据Config配置创建一个服务A的客户端Client,Client实际上是一个struct,定义如下:

type Client struct {sdk.Client
}

在创建客户端时,需要传入认证(例如密钥、用户名/密码)、后端服务地址等配置信息。例如,可以通过NewClientWithSecret方法来构建一个带密钥对的客户端:

func NewClientWithSecret(secretID, secretKey string) (client *Client, err error) {client = &Client{}config := sdk.NewConfig().WithEndpoint(defaultEndpoint)client.Init(serviceName).WithSecret(secretID, secretKey).WithConfig(config)return
}

这里要注意,上面创建客户端时,传入的密钥对最终会在基础层中被使用,用来签发JWT Token。

Client有多个方法(Sender),例如 Authz等,每个方法代表一个API接口。Sender方法会接收AuthzRequest等结构体类型的指针作为输入参数。我们可以调用 client.Authz(req) 来执行REST API调用。可以在 client.Authz 方法中添加一些业务逻辑处理。client.Authz 代码如下:

type AuthzRequest struct {*request.BaseRequestResource *string `json:"resource"`Action *string `json:"action"`Subject *string `json:"subject"`Context *ladon.Context
}func (c *Client) Authz(req *AuthzRequest) (resp *AuthzResponse, err error) {if req == nil {req = NewAuthzRequest()}resp = NewAuthzResponse()err = c.Send(req, resp)return
}

请求结构体中的字段都是指针类型的,使用指针的好处是可以判断入参是否有被指定,如果req.Subject == nil 就说明传参中没有Subject参数,如果req.Subject != nil就说明参数中有传Subject参数。根据某个参数是否被传入,执行不同的业务逻辑,这在Go API接口开发中非常常见。

另外,因为Client通过匿名的方式继承了基础层中的Client:

type Client struct {sdk.Client
}

所以,API层创建的Client最终可以直接调用基础层中的Client提供的Send(req, resp) 方法,来执行RESTful API调用,并将结果保存在 resp 中。

为了方便和API层的Client进行区分,我下面统一将基础层中的Client称为sdk.Client

 

package mainimport ("fmt""github.com/ory/ladon""github.com/marmotedu/medu-sdk-go/sdk"iam "github.com/marmotedu/medu-sdk-go/services/iam/authz"
)func main() {client, _ := iam.NewClientWithSecret("XhbY3aCrfjdYcP1OFJRu9xcno8JzSbUIvGE2", "bfJRvlFwsoW9L30DlG87BBW0arJamSeK")req := iam.NewAuthzRequest()req.Resource = sdk.String("resources:articles:ladon-introduction")req.Action = sdk.String("delete")req.Subject = sdk.String("users:peter")ctx := ladon.Context(map[string]interface{}{"remoteIP": "192.168.0.5"})req.Context = &ctxresp, err := client.Authz(req)if err != nil {fmt.Println("err1", err)return}fmt.Printf("get response body: `%s`\n", resp.String())fmt.Printf("allowed: %v\n", resp.Allowed)
}

基础层:构建并执行HTTP请求

上面我们创建了客户端实例,并调用了它的 Send 方法来完成最终的HTTP请求.

func (c *Client) Send(req request.Request, resp response.Response) error {method := req.GetMethod()builder := GetParameterBuilder(method, c.Logger)jsonReq, _ := json.Marshal(req)encodedUrl, err := builder.BuildURL(req.GetURL(), jsonReq)if err != nil {return err}endPoint := c.Config.Endpointif endPoint == "" {endPoint = fmt.Sprintf("%s/%s", defaultEndpoint, c.ServiceName)}reqUrl := fmt.Sprintf("%s://%s/%s%s", c.Config.Scheme, endPoint, req.GetVersion(), encodedUrl)body, err := builder.BuildBody(jsonReq)if err != nil {return err}sign := func(r *http.Request) error {signer := NewSigner(c.signMethod, c.Credential, c.Logger)_ = signer.Sign(c.ServiceName, r, strings.NewReader(body))return err}rawResponse, err := c.doSend(method, reqUrl, body, req.GetHeaders(), sign)if err != nil {return err}return response.ParseFromHttpResponse(rawResponse, resp)
}

上面的代码大体上可以分为四个步骤。

第一步,Builder:构建请求参数。

 

  1. HTTP请求路径构建

在创建客户端时,我们通过NewAuthzRequest函数创建了 /v1/authz REST API接口请求结构体AuthzRequest,代码如下:

func NewAuthzRequest() (req *AuthzRequest) {    req = &AuthzRequest{    BaseRequest: &request.BaseRequest{    URL:     "/authz",    Method:  "POST",    Header:  nil,    Version: "v1",    },    }    return                                
}

可以看到,我们创建的 req 中包含了API版本(Version)、API路径(URL)和请求方法(Method)。 

endPoint := c.Config.Endpoint                                                 
if endPoint == "" {                                                          endPoint = fmt.Sprintf("%s/%s", defaultEndpoint, c.ServiceName)           
}                                                                  
reqUrl := fmt.Sprintf("%s://%s/%s%s", c.Config.Scheme, endPoint, req.GetVersion(), encodedUrl) 

上述代码中,c.Config.Scheme=http/https、endPoint=iam.api.marmotedu.com:8080、req.GetVersion()=v1和encodedUrl,我们可以认为它们等于/authz。所以,最终构建出的请求路径为http://iam.api.marmotedu.com:8080/v1/authz 。

  1. HTTP请求Body构建

在BuildBody方法中构建请求Body。BuildBody会将 req Marshal成JSON格式的string。HTTP请求会以该字符串作为Body参数。

第二步,Signer:签发并添加认证头。

访问IAM的API接口需要进行认证,所以在发送HTTP请求之前,还需要给HTTP请求添加认证Header。

medu-sdk-go 代码提供了JWT和HMAC两种认证方式,最终采用了JWT认证方式。JWT认证签发方法为Sign,代码如下:

func (v1 SignatureV1) Sign(serviceName string, r *http.Request, body io.ReadSeeker) http.Header {tokenString := auth.Sign(v1.Credentials.SecretID, v1.Credentials.SecretKey, "medu-sdk-go", serviceName+".marmotedu.com")r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokenString))return r.Header}

auth.Sign 方法根据SecretID和SecretKey签发JWT Token。

接下来,我们就可以调用doSend方法来执行HTTP请求了。调用代码如下:

rawResponse, err := c.doSend(method, reqUrl, body, req.GetHeaders(), sign)
if err != nil {                                                               return err     
} 

 。

第三步,Request:执行HTTP请求。

调用doSend方法执行HTTP请求,doSend通过调用 net/http 包提供的 http.NewRequest 方法来发送HTTP请求,执行完HTTP请求后,会返回 *http.Response 类型的Response。 

func (c *Client) doSend(method, url, data string, header map[string]string, sign SignFunc) (*http.Response, error) {client := &http.Client{Timeout: c.Config.Timeout}req, err := http.NewRequest(method, url, strings.NewReader(data))if err != nil {c.Logger.Errorf("%s", err.Error())return nil, err}c.setHeader(req, header)err = sign(req)if err != nil {return nil, err}return client.Do(req)
}

第四步,处理HTTP请求返回结果。

调用doSend方法返回 *http.Response 类型的Response后,Send方法会调用ParseFromHttpResponse函数来处理HTTP Response,ParseFromHttpResponse函数代码如下:

func ParseFromHttpResponse(rawResponse *http.Response, response Response) error {defer rawResponse.Body.Close()body, err := ioutil.ReadAll(rawResponse.Body)if err != nil {return err}if rawResponse.StatusCode != 200 {return fmt.Errorf("request fail with status: %s, with body: %s", rawResponse.Status, body)}if err := response.ParseErrorFromHTTPResponse(body); err != nil {return err}return json.Unmarshal(body, &response)
}

可以看到,在ParseFromHttpResponse函数中,会先判断HTTP Response中的StatusCode是否为200,如果不是200,则会报错。如果是200,会调用传入的resp变量提供的ParseErrorFromHTTPResponse方法,来将HTTP Response的Body Unmarshal到resp变量中。
通过以上四步,SDK调用方调用了API,并获得了API的返回结果 resp 。

下面这些公有云厂商的SDK采用了此设计模式:

  • 腾讯云SDK:tencentcloud-sdk-go。
  • AWS SDK:aws-sdk-go。
  • 阿里云SDK:alibaba-cloud-sdk-go。
  • 京东云SDK:jdcloud-sdk-go。
  • Ucloud SDK:ucloud-sdk-go。

IAM公有云方式的SDK实现为 medu-sdk-go。

此外,IAM还设计并实现了Kubernetes client-go方式的Go SDK:marmotedu-sdk-go,marmotedu-sdk-go也是IAM Go SDK所采用的SDK。下一讲中,我会具体介绍marmotedu-sdk-go的设计和实现。

总结

 

公有云厂商的SDK设计方式中,SDK按调用顺序从上到下可以分为3个模块,如下图所示:

Client构造SDK客户端,在构造客户端时,会创建请求参数 req , req 中会指定API版本、HTTP请求方法、API请求路径等信息。

Client会请求Builder和Signer来构建HTTP请求的各项参数:HTTP请求方法、HTTP请求路径、HTTP认证头、HTTP请求Body。Builder和Signer是根据 req 配置来构造这些HTTP请求参数的。

 

课后练习

  1. 思考下,如何实现可以支持多个API版本的SDK包,代码如何实现?
  2.  在你的Go开发生涯中,还有没有一些更好的SDK实现方法? 

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

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

相关文章

GBK文件批量转UTF-8,python脚本

import os import codecsdef convert_encoding(file_path):try:# 尝试以gb18030编码打开文件并读取内容with codecs.open(file_path, r, gb18030) as f:content f.read()except UnicodeDecodeError:# 如果出现解码错误&#xff0c;尝试使用utf-8编码打开文件with codecs.open(…

【数据结构与算法】二叉搜索树和平衡二叉树

二叉搜索树 左子树的结点都比当前结点小&#xff0c;右子树的结点都比当前结点大。 构造二叉搜索树&#xff1a; let arr [3, 4, 7, 5, 2]function Node(value) {this.value valuethis.left nullthis.right null }/*** 添加结点* param root 当前结点* param num 新的结…

SpringBoot 登录认证(二)Cookie与Sesstion

SpringBoot 登录认证&#xff08;一&#xff09;-CSDN博客 SpringBoot 登录认证&#xff08;二&#xff09;-CSDN博客 SpringBoot登录校验&#xff08;三&#xff09;-CSDN博客 HTTP是无状态协议 HTTP协议是无状态协议。什么又是无状态的协议&#xff1f; 所谓无状态&…

有Digicert免费证书吗

说到Digiert证书&#xff0c;DigiCert 是美国CA认证可信&#xff0c;提供了很过十年的SSL证书和SSL管理工具。与其他CA不同&#xff0c;DigiCert 完全专注于SSL的创新&#xff0c;提供完整系列的SSL证书、工具和管理平台。它是名副其实的行业单位。 “DigiCert”是这个行业中根…

【JS】打乱数组顺序,用作领域:随机播放音乐

思路 循环数组随机获取数组下标取值&#xff1a; 取当前随机下标数组取当前循环的下标数组 相互替换步骤3的数组 /*** 随机数组顺序* param {Array} arr 数组* returns Array*/ const shufArr arr > {for (let i arr.length - 1; i > 0; i--) {const j Math.floor(M…

Postman和Python Request测试多行Form-data

1、请求参数有多个&#xff0c;F12查看请求体如下&#xff1a; 查看源代码&#xff1a; ------WebKitFormBoundaryHknGXm9VkhRUXZYC Content-Disposition: form-data; name"custId"IICON004 ------WebKitFormBoundaryHknGXm9VkhRUXZYC Content-Disposition: form-da…

企业邮箱给谷歌Gmail报错550-5.7.25解决方案

企业邮箱给谷歌Gmail报错550-5.7.25解决方案 问题表现 今天接到同事报告企业邮箱发送报错的问题&#xff0c;具体问题表现如下&#xff1a; 我司内部邮箱 xxXXX.com 邮箱给国内的163和新浪和企业内部发送邮件可以成功给Hotmail发送邮件&#xff0c;成功。给Gmail发送邮件&am…

IoT数采平台1:开篇

IoT数采平台1&#xff1a;开篇IoT数采平台2&#xff1a;文档IoT数采平台3&#xff1a;功能IoT数采平台4&#xff1a;测试 【功能概述】 开箱即用; 向下接入不同设备(PLC / 采集网关 / OPC / TCP设备 / UDP设备 / HTTP接入),向上通过MQTT发布消息; 数采底层基于NET CORE,既支持P…

Unity与CocosCreator对比学习一

一、屏幕分辨率 1.在creator中设置分辨率 1&#xff09;打开对应场景&#xff1b; 2&#xff09;选中【层级管理器】中的Canvas节点&#xff1b; 3&#xff09;修改【属性检察器】中Canvas组建的属性即可&#xff1b; 2.在Unity中设置屏幕分辨率 1&#xff09;切换到【Game视…

Python学习笔记-Flask接收post请求数据并存储数据库

1.引包 from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy 2.配置连接,替换为自己的MySQL 数据库的实际用户名、密码和数据库名 app Flask(__name__) #创建应用实列 app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://ro…

麒麟Linux安装教程(超详细)

公司要进行信息国产化&#xff0c;要用国产操作系统。公司下载了麒麟Linux&#xff0c;先安装试一下。 和大多数的Linux发行版差不多&#xff0c;支持直接试用而不安装&#xff0c;肯定是要安装的&#xff0c;所有直接选择了第二项“安装银河麒麟操作系统”。 安装主界面logo …

上网行为管理系统推荐,上网行为审计软件推荐

上网行为管理是指帮助互联网用户控制和管理对互联网的使用。它涵盖了多个方面&#xff0c;包括网页访问过滤、上网隐私保护、网络应用控制、带宽流量管理、信息收发审计、用户行为分析等。 上网行为管理产品系列适用于需要实施内容审计与行为监控、行为管理的网络环境&#xf…