侧边栏壁纸
博主头像
问道

问道的小花园,总能给你带来惊喜

  • 累计撰写 68 篇文章
  • 累计创建 35 个标签
  • 累计收到 4 条评论

混合云资产管理项目(七)

问道
2023-01-27 / 0 评论 / 0 点赞 / 317 阅读 / 10,582 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-01-27,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

本文对resource模块添加restful api,将接口暴露出去供前端或其他模块调用。api的暴露使用go-restful框架

restful框架

要想理解RESTful,就需要先明白REST。REST是 Roy T. Fielding 在其2000年的博士论文中提出的,是REpresentational State Transfer 词组的缩写,可以翻译为“表现层状态转移”,其实这个词组前面省略了个主语--“Resource”,加上主语后就是“资源表现层状态转移”。每个词都能看懂,连在一起不知道什么意思了有木有?

  • Resource(资源)

    所谓资源,就是互联网上的一个实体。URI(Uniform Resource Identifier)的全称就是统一资源标识符,和我们这里的资源一个意思。一个资源可以是一段文字、一张图片、一段音频、一个服务。

  • 表现层(Representation)

    "资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。比如一篇文章,可以使用XML、JSON、HTML的形式呈现出来。

  • 状态转移(State Transfer)

    访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。互联网通信协议HTTP协议,是一个无状态协议,这意味着所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。

一个 RESTful API 框架需要什么?

从应用程序开发的角度来看,RESTful API 的本质是一个 Web Application,而 RESTful API 框架就是实现这个 Web Application 所封装的一些列工具库,使开发者可以忽略底层实现的复杂度,专注以自身 Application 的逻辑设计。

一个 RESTful API 框架应该具备以下几个元素:

Resources:资源的定义,即 HTTP URI(或称之为 HTTP URL Path)的定义。RESTful API 的设计围绕着 Resource 进行建模。

Handlers:资源处理器,是资源业务逻辑处理的具体实现。

Request Routers:资源请求路由器,完成 HTTP URIs、HTTP Request Methods 和 Handlers 三者之间的映射与路由。

Request Verification Schemas:HTTP Request Body 校验器,验证请求实体的合法性。

Response View Builder:HTTP Response Body 生成器,生成合法的响应实体。

Controllers:资源表现层状态转移控制器,每个 Resource 都有着各自的 Controller,将 Resource 自身及其所拥有的 Handlers、Request Verification Schemas 以及 Response View Builder 进行封装,配合 Request Routers 完成 RESTful 请求的处理即响应。

go-restful

go-restful 是一个 Golang 第三方库,是一个轻量的 RESTful API 框架,基于 Golang Build-in 的 http/net 库。适用于构建灵活多变的 Web Application,Kubernetes 的 ApiServer 也使用了 go-restful。

Github:https://github.com/emicklei/go-restful

Doc:https://godoc.org/github.com/emicklei/go-restful

go-restful 具有以下特性:

支持可配置的请求路由,默认使用 CurlyRouter 快速路由算法,也支持 RouterJSR311。
支持在 URL path 上定义正则表达式,例如:/static/{subpath:*}。
提供 Request API 用于从 JSON、XML 读取路径参数、查询参数、头部参数,并转换为 Struct。
提供 Response API 用于将 Struct 写入到 JSON、XML 以及 Header。
支持在服务级、或路由级对请求、响应流进行过滤和拦截。
支持使用过滤器自动响应 OPTIONS 请求和 CORS(跨域)请求。
支持使用 RecoverHandler 自定义处理 HTTP 500 错误。
支持使用 ServiceErrorHandler 自定义处理路由错误产生 HTTP 404/405/406/415 等错误。
支持对请求、响应的有效负载进行编码(例如:gzip、deflate)。
支持使用 CompressorProvider 注册自定义的 gzip、deflate 的读入器和输出器。
支持使用 EntityReaderWriter 注册的自定义编码实现。
支持 Swagger UI 编写的 API 文档。
支持可配置的日志跟踪。

go-restful定义了三个重要的数据结构:

  • Router:表示一条路由,包含url、回调处理函数
  • Webservice:表示一个服务
  • Container:表示一个服务器

Route

Route 表示一条请求路由记录,即:Resource 的 URL Path(URI),从编程的角度可细分为 RootPath 和 SubPath。Route 包含了 Resource 的 URL Path、HTTP Method、Handler 三者之间的组合映射关系。go-restful 内置的 RouteSelector(请求路由分发器)根据 Route 将客户端发出的 HTTP 请求路由到相应的 Handler 进行处理。

go-restful 支持两种路由分发器:快速路由 CurlyRouter 和 RouterJSR311。实际上,CurlyRoute 也是基于 RouterJSR311 的,相比 RouterJSR11,还支持了正则表达式和动态参数,也更加轻量级,Kubernetes ApiServer 中使用的就是这种路由。

CurlyRouter 的元素包括:请求路径(URL Path),请求参数(Parameter),输入、输出类型(Writes、Reads Model),处理函数(Handler),响应内容类型(Accept)等。

WebService

一个 WebService 由若干个 Routes 组成,并且 WebService 内的 Routes 拥有同一个 RootPath、输入输出格式、基本一致的请求数据类型等等一系列的通用属性。通常的,我们会根据需要将一组相关性非常强的 API 封装成为一个 WebServiice,继而将 Web Application 所拥有的全部 APIs 划分若干个 Group。

所以,WebService 至少会有一个 Root Path,通过 ws.Path() 方法设置,例如:/user_group,作为 Group 的 “根”。Group 下属的 APIs 都是 RootRoute(RootPath)下属的 SubRoute(SubPath)。

每个 Group 就是提供一项服务的 API 集合,每个 Group 会维护一个 Version。Group 的抽象是为了能够安全隔离的对各项服务进行敏捷迭代,当我们对一项服务进行升级时,只需要通过对特定版本号的更新来升级相关的 APIs,而不会影响到整个 Web Server。视实际情况而定,可能是若干个 APIs 分为一个 Group,也有可能一个 API 就是一个 Group。

Container

Container 表示一个 Web Server(服务器),由多个 WebServices 组成,此外还包含了若干个 Filters(过滤器)、一个 http.ServeMux 多路复用器以及一个 dispatch。go-restful 如何在从 Container 开始将路由分发给各个 WebService,再由 WebService 分发给具体的 Handler 函数,这些都在 dispatch 中实现。

三者的关系如下:

  • go-restful支持多个container,一个container相当于一个http server,不同的container监控不同的地址和端口
  • 每个container可以包含多个webservice,相当于一组不同服务的分类
  • 每个webservice包含多个Router(路由),Router根据http请求的URL路由到对应的处理函数(Handler Func)

实现http的协议

protocol/http.go

// NewHTTPService 构建函数
func NewHTTPService() *HTTPService {

  r := restful.DefaultContainer
  // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
  // You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
  // Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json
  // http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/Projects/swagger-ui/dist"))))

  // Optionally, you may need to enable CORS for the UI to work.
  cors := restful.CrossOriginResourceSharing{
    AllowedHeaders: []string{"*"},
    AllowedMethods: []string{"*"},
    CookiesAllowed: false,
    Container:      r}
  r.Filter(cors.Filter)


  server := &http.Server{
    ReadHeaderTimeout: 60 * time.Second,
    ReadTimeout:       60 * time.Second,
    WriteTimeout:      60 * time.Second,
    IdleTimeout:       60 * time.Second,
    MaxHeaderBytes:    1 << 20, // 1M
    Addr:              conf.C().App.HTTP.Addr(),
    Handler:           r,
  }

  return &HTTPService{
    r:      r,
    server: server,
    l:      zap.L().Named("HTTP Service"),
    c:      conf.C(),
    mc:     rpc.C(),
  }
}

cmd/start.go

// startCmd represents the start command
var serviceCmd = &cobra.Command{
  Use:   "start",
  Short: "mycmdb API服务",
  Long:  "mycmdb API服务",
  RunE: func(cmd *cobra.Command, args []string) error {
    // 初始化全局变量
    if err := loadGlobalConfig(""); err != nil {
      return err
    }

    // 初始化全局日志配置
    if err := loadGlobalLogger(); err != nil {
      return err
    }

    // 初始化全局app
    if err := app.InitAllApp(); err != nil {
      return err
    }


    rpc.SetGlobal(mc)

    conf := conf.C()
    // 启动服务
    ch := make(chan os.Signal, 1)
    defer close(ch)
    signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT)

    // 初始化服务
    svr, err := newService(conf)
    if err != nil {
      return err
    }

    // 等待信号处理
    go svr.waitSign(ch)

    // 启动服务
    if err := svr.start(); err != nil {
      if !strings.Contains(err.Error(), "http: Server closed") {
        return err
      }
    }

    return nil
  },
}

func newService(cnf *conf.Config) (*service, error) {
  http := protocol.NewHTTPService()
  grpc := protocol.NewGRPCService()
  svr := &service{
    http: http,
    grpc: grpc,
    log:  zap.L().Named("CLI"),
  }

  return svr, nil
}

type service struct {
  http *protocol.HTTPService
  grpc *protocol.GRPCService

  log logger.Logger
}

func (s *service) start() error {
  s.log.Infof("loaded grpc app: %s", app.LoadedGrpcApp())
  s.log.Infof("loaded http app: %s", app.LoadedRESTfulApp())

  s.log.Infof("loaded internal app: %s", app.LoadedInternalApp())

  go s.grpc.Start()
  return s.http.Start()
}

// config 为全局变量, 只需要load 即可全局可用户
func loadGlobalConfig(string) error {
  //// 配置加载
  //switch configType {
  //case "file":
  //  err := conf.LoadConfigFromToml(confFile)
  //  if err != nil {
  //    return err
  //  }
  //case "env":
  //  err := conf.LoadConfigFromEnv()
  //  if err != nil {
  //    return err
  //  }
  //default:
  //  return errors.New("unknown config type")
  //}
  err := conf.LoadConfigFromToml("etc/config.toml")
  if err != nil {
    return err
  }

  return nil
}

// log 为全局变量, 只需要load 即可全局可用户, 依赖全局配置先初始化
func loadGlobalLogger() error {
  var (
    logInitMsg string
    level      zap.Level
  )
  lc := conf.C().Log
  lv, err := zap.NewLevel(lc.Level)
  if err != nil {
    logInitMsg = fmt.Sprintf("%s, use default level INFO", err)
    level = zap.InfoLevel
  } else {
    level = lv
    logInitMsg = fmt.Sprintf("log level: %s", lv)
  }
  zapConfig := zap.DefaultConfig()
  zapConfig.Level = level
  switch lc.To {
  case conf.ToStdout:
    zapConfig.ToStderr = true
    zapConfig.ToFiles = false
  case conf.ToFile:
    zapConfig.Files.Name = "api.log"
    zapConfig.Files.Path = lc.PathDir
  }
  switch lc.Format {
  case conf.JSONFormat:
    zapConfig.JSON = true
  }
  if err := zap.Configure(zapConfig); err != nil {
    return err
  }
  zap.L().Named("INIT").Info(logInitMsg)
  return nil
}

protocol/http.go

加载完之后注册服务

// 注册功能列表
func (s *HTTPService) RegistryEndpoint(ctx context.Context) {
  epss := []*endpoint.Entry{}
  wss := s.r.RegisteredWebServices()
  for i := range wss {
    eps := tools.TransferRoutesToEndpints(wss[i].Routes())
    epss = append(epss, eps...)
  }

  // mcenter的客户端
  req := endpoint.NewRegistryRequest("0.0.1", epss)
  req.ClientId = s.mc.Config().ClientID
  req.ClientSecret = s.mc.Config().ClientSecret
  resp, err := s.mc.Endpoint().RegistryEndpoint(ctx, req)
  if err != nil {
    s.l.Errorf("registry endpoint error, %s", err)
  } else {
    s.l.Debugf("registy success, %s", resp.Message)
  }
}

protocol/http.go

在start中注册

// Start 启动服务
func (s *HTTPService) Start() error {
  // 装置子服务路由
  app.LoadRESTfulApp(s.PathPrefix(), s.r)

  s.RegistryEndpoint(context.Background())

  // API Doc
  config := restfulspec.Config{
    WebServices:                   restful.RegisteredWebServices(), // you control what services are visible
    APIPath:                       "/apidocs.json",
    PostBuildSwaggerObjectHandler: swagger.Docs}
  s.r.Add(restfulspec.NewOpenAPIService(config))
  s.l.Infof("Get the API using http://%s%s", s.c.App.HTTP.Addr(), config.APIPath)

  // 启动 HTTP服务
  s.l.Infof("HTTP服务启动成功, 监听地址: %s", s.server.Addr)
  if err := s.server.ListenAndServe(); err != nil {
    if err == http.ErrServerClosed {
      s.l.Info("service is stopped")
    }
    return fmt.Errorf("start service error, %s", err.Error())
  }
  return nil
}

resource的api接口

apps/resource/api/http.go

对外的restful api只给了一个查询接口

var (
  h = &handler{}
)

type handler struct {
  service resource.Service
  log     logger.Logger
}

func (h *handler) Config() error {
  h.log = zap.L().Named(resource.AppName)
  h.service = app.GetGrpcApp(resource.AppName).(resource.Service)
  return nil
}

func (h *handler) Name() string {
  return resource.AppName
}

func (h *handler) Version() string {
  return "v1"
}

func (h *handler) Registry(ws *restful.WebService) {
  tags := []string{"资源查询"}

  ws.Route(ws.GET("/").To(h.QueryResource).
    Metadata(label.Auth, true).
    Doc("create a book").
    Metadata(restfulspec.KeyOpenAPITags, tags))
}

func init() {
  app.RegistryRESTfulApp(h)
}

apps/resource/api/resource.go

go-restful实现http api的handler

func (h *handler) QueryResource(r *restful.Request, w *restful.Response) {
  req := resource.NewQueryResourceRequestFromHTTP(r.Request)
  set, err := h.service.QueryResource(r.Request.Context(), req)
  if err != nil {
    response.Failed(w.ResponseWriter, err)
    return
  }
  response.Success(w.ResponseWriter, set)
}

apps/resource/app.go

补充QueryResource的NewQueryResourceRequestFromHTTP

func NewQueryResourceRequestFromHTTP(r *http.Request) *QueryResourceRequest {
  req := NewQueryResourceRequest()
  qs := r.URL.Query()
  req.Domain = qs.Get("domain")
  req.Namespace = qs.Get("namespace")
  req.Env = qs.Get("env")
  req.Page = request.NewPageRequestFromHTTP(r)
  return req
}

apps/api.go

注册http服务模块
import (
  // 注册所有HTTP服务模块, 暴露给框架HTTP服务器加载
  _ "gitee.com/go-course/go8/projects/devcloud/cmdb/apps/resource/api"
)

测试该查询方法

make run运行

zengz@DESKTOP-Q3MJC54 MINGW64 /f/go/project/0-shizhanxiangmu/myCMDB/mycmdb (master)
$ make run                                                                         
2023-01-27 21:30:22     INFO    [INIT]  cmd/start.go:166        log level: debug
2023-01-27 21:30:22     INFO    [CLI]   cmd/start.go:100        loaded grpc app: [host resource secret]
2023-01-27 21:30:22     INFO    [CLI]   cmd/start.go:101        loaded http app: [secret host resource]    
2023-01-27 21:30:22     INFO    [CLI]   cmd/start.go:103        loaded internal app: [host resource secret]
2023-01-27 21:30:22     INFO    [GRPC Service]  protocol/grpc.go:54     GRPC 服务监听地址: 127.0.0.1:18060
2023-01-27 21:30:24     ERROR   [HTTP Service]  protocol/http.go:137    registry endpoint error, rpc error: code = Unavailable desc = connection error: desc = "transport: Error while d
ialing dial tcp 127.0.0.1:18050: connectex: No connection could be made because the target machine actively refused it."
2023-01-27 21:30:24     INFO    [HTTP Service]  protocol/http.go:97     Get the API using http://127.0.0.1:8060/apidocs.json
2023-01-27 21:30:24     INFO    [HTTP Service]  protocol/http.go:100    HTTP服务启动成功, 监听地址: 127.0.0.1:8060

查看api文档

访问路径查看数据

http接口验证通过

0

评论区