原创分享 基于 Twirp RPC 的简易 JSON Api Gateway 实现

yiplee-github · 2020年07月07日 · 最后由 unclet 回复于 2020年07月12日 · 1498 次阅读
本帖已被设为精华帖!

Twirp 是 Twichtv 2018 年开源的一套极简 RPC 框架,当时官方的介绍文章 Twirp: a sweet new RPC framework for Go。功能上比不上大名鼎鼎的 grpc,但是胜在小巧灵活,兼容 Go 原生的 http.Handler,可以与现有的任意 Router 和中间件搭配使用;同时支持 protobuf 和 json 格式传输,调试方便;另外我个人很喜欢它的 Error 实现,直观并且可扩展性还不错。

Twirp 支持 json 格式的 api 请求,前端可以直接接入使用;但是请求全部是统一的 POST 请求,url 是 /twirp/package.name/method 这种格式。就算能说服自己公司的前端接受这种接入方式,也不太好公开出去给第三方合作方使用。所以需要类似 grpc gateway 的东西,能提供 restful 风格的 api 出去。并且由于项目历史原因,希望返回的数据按照如下格式返回:

// ok
{
    "data": { // twirp 返回的数据没有 data 这层包装
        "foo":"bar"
    }
}

// error
{
    "code": 10002, // twirp 返回的 error code  'permission_denied' 这种 string
    "msg": "error msg"
}

需求明确之后剩下的实现也就清晰了。我们需要类似 httputil.ReverseProxy 那样对收到的 request 按照 twirp 的要求进行改造,然后交给实现好的 twirp rpc service 去处理。对于 body 的处理,我们自定义一个 http.ResponsWriter 对 body 进行调整之后再写入真正的 Writer。 下面我将使用 go-chi/chi 作为 Router 来实现一个简单的 twirp api gateway 的例子。这个例子的完整代码开源在 yiplee/twirp-gateway-example

/reversetwirp.go

  • 将 url path params 以及 query params 打包到 body

  • 修改 request method 为 POST

  • 修改 url path 为对应的 /twirp/package.name/method

  • 转发给 twirp rpc handler 处理请求

使用示例见 /api/books

/render/response.go 包装 Response

  • 拦截 response status

  • 如果是成功的返回,将 body 用 data 包装后写入真正的 ResponseWriter

  • 如果是失败的返回,还原 Twirp Error 然后构造新的 errorResponse 写入 ResponseWriter

Server

Server 实现了 rpc 和 rest api 的 Handler

Route RPC

使用 path 前缀匹配的方式将 rpc 请求路由到对应的实现上去

func (s Server) HandleRpc() http.Handler {
    r := chi.NewRouter()
    r.Use(resetRoutePath)

    // book service
    {
        svc := book.New(s.Books)
        r.Mount(svc.PathPrefix(), svc)
    }

    return r
}

Route Rest Api

提供 restful 风格的 api 请求,由 ReverseTwirp 实现

func (s Server) HandleApi() http.Handler {
    r := chi.NewRouter()
    r.Use(render.WrapResponse(true))

    r.Route("/books", func(r chi.Router) {
        svc := book.New(s.Books)
        rt := reversetwirp.NewSingleTwirpServerProxy(svc)

        r.Post("/", rt.Handle("Create", nil))
        r.Get("/{id}", rt.Handle("Find", nil))
    })

    return r
}

启动 Server

r := chi.NewMux()
r.Use(middleware.Recoverer)
r.Use(middleware.Logger)

books := book.New()
svr := handler.New(books)

r.Mount("/twirp", svr.HandleRpc())
r.Mount("/api", svr.HandleApi())
r.Mount("/hc", hc.Handler())

addr := fmt.Sprintf(":%d", cfg.port)
if err := http.ListenAndServe(addr, r); err != nil {
    log.Fatal(err)
}

最后,我们既提供了由 twirp code generator 生成的 rpc 客户端代码 可以被内部服务直接使用,也提供了

GET /api/books/:id

POST /api/books

这两个 rest api 供前端使用。

完整代码见 yiplee/twirp-gateway-example

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 07月07日 12:11
mahuaibo GoCN 每日新闻 (2020-07-10) 中提及了此贴 07月10日 13:42

我们也一直重度使用 twirp rpc,我之前专门写过一篇文章https://zhuanlan.zhihu.com/p/69029677

我们也是因为客户端历史包袱原因,改造了 twirp 框架,使其支持 form 表单调用。欢迎一起讨论。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册