利用 git-kit 實現支援 http 和 grpc 的微服務

Mr_houzi 發表於 2022-01-24
微服務 Git

利用 git-kit 微服務框架實現一個同時支援 http 和 grpc 服務的應用。以一個最常見的文章服務為例,開始教程!

專案架子

go-kit 三層模型簡介

go-kit 是一套開源的 golang 微服務工具集合。go-kit 自上而下提供了三層模型,分別是 Transport 層、Endpoint 層、Service 層。

  • Transport 層:處理 HTTP、gRPC、Thrift 等協議相關的邏輯,主要對請求進行解碼、對響應進行編碼操作;
  • Endpoint 層:在Service的上層作為業務的中介軟體,可使用限流、熔斷、監控等能力;
  • Service 層:用來處理業務邏輯;

專案初始化

感謝FengGeSe/demo 專案提供了一個很好工程 demo,本教程基於此倉庫進行改造,教程程式碼於此git-kit-demo

來自FengGeSe/demo中的圖

FengGeSe/demo專案在 go-kit 三層模型中,又增加了 Server 和 Router 層,前者作為服務啟動,後者作為路由轉發。

利用資料模型 Model 作為“中立”資料格式,同時相容多協議的請求。

eg:

一個 http 請求到來,json 資料會轉為 model,響應時 model 會轉為 json。

一個 grpc 請求到來,protobuf 資料會轉為 model,當響應時,model 會轉為 protobuf。

專案目錄結構

.
├── README.md                   
├── cmd                    // 提供client和server的入口
│   ├── client
│   └── server
├── conf                   // 配置相關
├── endpoint               // endpoint層
│   └── article
├── errors                 // 錯誤處理
├── go.mod
├── go.sum
├── params                  // model層(在程式碼中使用params表示)
│   └── article
├── pb                     // pb層
│   └── article
├── router                 // 路由層。grpc和http註冊路由的地方
│   ├── grpc
│   └── http
├── server                 // server層,啟動服務的地方
│   ├── grpc
│   └── http
├── service                // service層,處理業務邏輯的地方
│   └── article
├── static                 // 文件,文件圖片相關
│   └── img
├── transport              // transport, 資料轉換的地方
│   ├── grpc
│   └── http
├── util                   // 工具方法
└── vendor                 // 三方依賴

下面進入開發!

Service 開發

定義介面

Service 層用來處理業務邏輯,一個 Service,由許多功能方法構成。拿一個最熟悉的文章服務來說,它將會提供增刪改查的功能。

定義介面。定義 ArticleService 介面,規定 Service 要提供的方法。

// service/article.go
package service

import (
    "context"
    "demo/params/article_param"
    "fmt"
)

type ArticleService interface {
    Create (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error)
    Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error)
}

定義資料模型

要實現一個方法,我們就要先想好它的資料模型 model,明確它的入參和出參。為了區別 orm 的 model 層,在程式碼實現中資料模型 model 姑且稱為 params,它主要來定義一個請求傳入引數傳出引數

// params/article_param/article.go
package article_param

type CreateReq struct {
    Title string `json:"title"`
    Content string `json:"content"`
    CateId int64 `json:"cate_id"`
}

type CreateResp struct {
    Id int64 `json:"id"`
}

type DetailReq struct {
    Id int64 `json:"id"`
}

type DetailResp struct {
    Id int64 `json:"id"`
    Title string `json:"title"`
    Content string `json:"content"`
    CateId int64 `json:"cate_id"`
    UserId int64 `json:"user_id"`
}

Service 具體實現

通過定義一個 articleService 結構體,實現 ArticleService 介面規劃的所有方法。並通過 NewArticleService 方法將實現的 Service 暴露出去。

package service

import (
    "context"
    "demo/params/article_param"
    "fmt"
)

// ArticleService 定義文章service介面,規定本service要提供的方法
type ArticleService interface {
    Create (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error)
    Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error)
}

// NewArticleService new service
func NewArticleService() ArticleService {
    var svc = &articleService{}
    {
        // middleware
    }
    return svc
}

// 定義文章service結構體,並實現文章service介面的所有方法
type articleService struct {}

func (s *articleService) Create    (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error) {
    fmt.Printf("req:%#v\n", req)

    // mock:insert 根據傳入引數req插庫生成Id
    id := 1
    return &article_param.CreateResp{
        Id: int64(id),
    }, nil
}

func (s *articleService) Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error) {
    ……
}

以建立文章方法為例,一個方法拿到 model 層定義的入參,然後執行具體邏輯,最終返回 model 層定義的出參。

一個 web 開發者,很容易想到一個 http 響應返回的引數應該是 json 格式,這裡不同,這裡響應的是 mdoel 層定義的 struct,它是一箇中立的資料結構,只與開發語言有關。而 json 格式與 http 服務耦合,並不是所有的服務都用 json 來傳遞資料,像 grpc 服務,一般用 protobuf 來作為資料格式。所以,Service 層實現的方法入參和出參,既不是 json 也不是 protobuf message。

至於資料轉換的問題,那是 transport 層該關心的事情(後文)。

Endpoint 開發

endpoint 層通過 Service 層暴露出來的 NewArticleService 方法進行呼叫。 endpoint 層在 Service 層之前,可以作為業務的中介軟體。它同樣不關心請求是 http 還是 grpc 。

// endpoint/article/article.go
package article

import (
    "context"
    "demo/errors"
    "demo/params/article_param"
    service "demo/service"
    "github.com/go-kit/kit/endpoint"
)

// make endpoint             service -> endpoint
func MakeCreateEndpoint(svc service.ArticleService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req, ok := request.(*article_param.CreateReq)
        if !ok {
            return nil, errors.EndpointTypeError
        }
        resp, err := svc.Create(ctx, req)
        if err != nil {
            return nil, err
        }
        return resp, nil
    }
}

// make endpoint             service -> endpoint
func MakeDetailEndpoint(svc service.ArticleService) endpoint.Endpoint {
    ……
}

HTTP + Json 開發

Transport 層

transport 層是在 endpoint 層之前,它主要作用是解析請求的資料,編碼響應的資料。在這一層,就要區分請求是 http 還是 grpc了,畢竟兩種服務請求的資料格式不同,需要分別處理。

在 transport 層要實現 decodeRequest、encodeResponse 和 http.Handler。

  • decodeRequest 解析 http 請求傳來的引數,轉為“中立”的 model 結構;
  • encodeResponse 將“中立”的 model 結構轉為 json,或新增響應頭之類的操作;
  • handler 利用 git-kit 提供的 transport/http 的融合 decodeRequest、encodeResponse ,並呼叫 endpoint 層。
// transport/http/article/create.go
package article

import (
    "context"
    endpoint "demo/endpoint/article"
    "demo/params/article_param"
    "demo/service"
    transport "demo/transport/http"
    "encoding/json"
    "fmt"
    httptransport "github.com/go-kit/kit/transport/http"
    "net/http"
)

// Server
// 1. decode request      http.request -> model.request
func decodeCreateRequest(_ context.Context, r *http.Request) (interface{}, error) {
    if err := transport.FormCheckAccess(r); err != nil {
        return nil, err
    }
    if err := r.ParseForm(); err != nil {
        fmt.Println(err)
        return nil, err
    }
    req := &article_param.CreateReq{}
    err := transport.ParseForm(r.Form, req)
    if err != nil {
        return nil, err
    }
    fmt.Printf("r.Form:%#v\n", r.Form)
    fmt.Printf("req:%#v\n", req)
    r.Body.Close()
    return req, nil
}

// 2. encode response      model.response -> http.response
func encodeCreateResponse(_ context.Context, w http.ResponseWriter, resp interface{}) error {
    w.Header().Set("Content-Type", "application/json")
    return json.NewEncoder(w).Encode(resp)
}

// make handler
func MakeCreateHandler(svc service.ArticleService) http.Handler {
    handler := httptransport.NewServer(
        endpoint.MakeCreateEndpoint(svc),
        decodeCreateRequest,
        encodeCreateResponse,
        transport.ErrorServerOption(), // 自定義錯誤處理
    )
    return handler
}

Router + Server

router 層根據 url 轉發到不同 transport 層

// router/httprouter/article.go
package httprouter

import (
    svc "demo/service"
    transport "demo/transport/http/article"
    "net/http"
)

func RegisterRouter(mux *http.ServeMux)  {
    mux.Handle("/article/create", transport.MakeCreateHandler(svc.NewArticleService()))
    mux.Handle("/article/detail", transport.MakeDetailHandler(svc.NewArticleService()))
}

server 層用來啟動 http 服務,並引入 router。

// server/http/server.go
package http

import (
    "demo/router/httprouter"
    "net"
    "net/http"
)

var mux = http.NewServeMux()

var httpServer = http.Server{Handler: mux}

// http run
func Run(addr string, errc chan error) {

    // 註冊路由
    httprouter.RegisterRouter(mux)

    lis, err := net.Listen("tcp", addr)
    if err != nil {
        errc <- err
        return
    }
    errc <- httpServer.Serve(lis)
}

最後在一個統一的指令碼中呼叫 http.Run 啟動 http 服務。

// cmd/server/sever.go
package main

import http "demo/server/http"

func main() {
    errc := make(chan error)

    go http.Run("0.0.0.0:8080", errc)
    // 等grpc服務完成後,在這裡啟動 grpc

    log.WithField("error", <-errc).Info("Exit")
}

Grpc + Protobuf 開發

編寫 protobuf

在 grpc 服務中,我們用 protobuf 來作為資料格式,所以第一步編寫 protobuf。

在 protobuf 中,定義 service 和 message。基於資料模型 model 編寫 protobuf mesage,基於 Service 層的 ArticleService interface 編寫 protobuf service。

// pb/article/article.proto
syntax = "proto3";
option go_package = ".;proto";

service ArticleService {
  rpc Create(CreateReq) returns (CreateResp);
  rpc Detail(DetailReq) returns (DetailResp);
}

message CreateReq {
  string Title = 1;
  string Content = 2;
  int64 CateId = 3;
}

message CreateResp {
  int64 Id = 1;
}

message DetailReq {
  int64 Id = 1;
}

message DetailResp {
  int64 Id = 1;
  string Title = 2;
  string Content = 3;
  int64 CateId = 4;
  int64 UserId = 5;
}

proto 檔案和資料模型 model 很像,但不要把它們混為一談。 model 是“中立的”。

proto 檔案具體表示什麼意思呢?

service 關鍵字規定了一個 grpc 的服務,它提供了兩個方法 Create 和 Detail。
message 關鍵字規定了訊息結構,它由型別 變數名 = 序號組成,最終傳遞 protobuf 是二進位制格式,只編碼值,不編碼變數名, 所以需要序號來解析對應的變數。

當客戶端呼叫 ArticleService 服務的 Create 方法時,需要傳入 CreateReq 結構的引數集合,服務端將會返回 CreateResp 結構的引數集合。

生成 pb 檔案,在pb/article/目錄下,執行如下命令:

protoc --proto_path=./ --go_out=plugins=grpc:./ ./article.proto

生成的 Go 版本的 pb 檔案,如下。

這是 article.pb.go 中的 Server 程式碼,供服務端使用。

// pb/article/article.pb.go

……

// ArticleServiceServer is the server API for ArticleService service.
type ArticleServiceServer interface {
    Create(context.Context, *CreateReq) (*CreateResp, error)
    Detail(context.Context, *DetailReq) (*DetailResp, error)
}

// UnimplementedArticleServiceServer can be embedded to have forward compatible implementations.
type UnimplementedArticleServiceServer struct {
}

func (*UnimplementedArticleServiceServer) Create(context.Context, *CreateReq) (*CreateResp, error) {
    return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
}
func (*UnimplementedArticleServiceServer) Detail(context.Context, *DetailReq) (*DetailResp, error) {
    return nil, status.Errorf(codes.Unimplemented, "method Detail not implemented")
}

func RegisterArticleServiceServer(s *grpc.Server, srv ArticleServiceServer) {
    s.RegisterService(&_ArticleService_serviceDesc, srv)
}

這是 article.pb.go 中的 Client 程式碼,pb 檔案會給到客戶端,Client 程式碼會供客戶端使用。

// pb/article/article.pb.go

……

// ArticleServiceClient is the client API for ArticleService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ArticleServiceClient interface {
    Create(ctx context.Context, in *CreateReq, opts ...grpc.CallOption) (*CreateResp, error)
    Detail(ctx context.Context, in *DetailReq, opts ...grpc.CallOption) (*DetailResp, error)
}

type articleServiceClient struct {
    cc grpc.ClientConnInterface
}

func NewArticleServiceClient(cc grpc.ClientConnInterface) ArticleServiceClient {
    return &articleServiceClient{cc}
}

func (c *articleServiceClient) Create(ctx context.Context, in *CreateReq, opts ...grpc.CallOption) (*CreateResp, error) {
    out := new(CreateResp)
    err := c.cc.Invoke(ctx, "/ArticleService/Create", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

func (c *articleServiceClient) Detail(ctx context.Context, in *DetailReq, opts ...grpc.CallOption) (*DetailResp, error) {
    out := new(DetailResp)
    err := c.cc.Invoke(ctx, "/ArticleService/Detail", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

Transport 層

接下來實現 grpc 服務的 transport 層。

在 transport 層要實現 decodeRequest、encodeResponse 和 grpc.Handler。

  • decodeRequest 解析 grpc 請求的 protobuf,轉為“中立”的 model 結構;
  • encodeResponse 將“中立”的 model 結構轉為 protobuf;
  • handler 利用 git-kit 提供的 transport/grpc 的融合 decodeRequest、encodeResponse ,並呼叫 endpoint 層。
// transport/grpc/article/create.go
package article

import (
    "context"
    "demo/params/article_param"
    pb "demo/pb/article"
    "fmt"
)

// 1. decode request          pb -> model
func decodeCreateRequest(c context.Context, grpcReq interface{}) (interface{}, error) {
    req, ok := grpcReq.(*pb.CreateReq)
    if !ok {
        fmt.Println("grpc server decode request出錯!")
        return nil, fmt.Errorf("grpc server decode request出錯!")
    }
    // 過濾資料
    request := &article_param.CreateReq{
        Title: req.Title,
        Content: req.Content,
        CateId: req.CateId,
    }
    return request, nil
}

// 2. encode response           model -> pb
func encodeCreateResponse(c context.Context, response interface{}) (interface{}, error) {
    fmt.Printf("%#v\n", response)
    resp, ok := response.(*article_param.CreateResp)
    if !ok {
        return nil, fmt.Errorf("grpc server encode response error (%T)", response)
    }

    r := &pb.CreateResp{
        Id: resp.Id,
    }

    return r, nil
}

grpc 服務的 transport 層和 http 服務的 transport 層類似,除此以外還需要一點“膠水”將 grpc 和 go-kit 融合。

// transport/grpc/article/article.go
package article

import (
    "context"
    endpoint "demo/endpoint/article"
    pb "demo/pb/article"
    "demo/service"
    grpctransport "github.com/go-kit/kit/transport/grpc"
)

// ArticleGrpcServer 1.實現了 pb.ArticleServiceServer 的所有方法,實現了”繼承“;
// 2.提供了定義了 create 和 detail 兩個 grpctransport.Handler。
type ArticleGrpcServer struct {
    createHandler grpctransport.Handler
    detailHandler grpctransport.Handler
}

// 通過 grpc 呼叫 Create 時,Create 只做資料傳遞, Create 內部又呼叫 createHandler,轉交給 go-kit 處理

func (s *ArticleGrpcServer) Create (ctx context.Context, req *pb.CreateReq) (*pb.CreateResp, error) {
    _, rsp, err := s.createHandler.ServeGRPC(ctx, req)
    if err != nil {
        return nil, err
    }
    return rsp.(*pb.CreateResp), err
}

func (s *ArticleGrpcServer) Detail (ctx context.Context, req *pb.DetailReq) (*pb.DetailResp, error) {
    _, rsp, err := s.detailHandler.ServeGRPC(ctx, req)
    if err != nil {
        return nil, err
    }
    return rsp.(*pb.DetailResp), err
}

// NewArticleGrpcServer 返回 proto 中定義的 article grpc server
func NewArticleGrpcServer(svc service.ArticleService, opts ...grpctransport.ServerOption) pb.ArticleServiceServer {

    createHandler := grpctransport.NewServer(
        endpoint.MakeCreateEndpoint(svc),
        decodeCreateRequest,
        encodeCreateResponse,
        opts...,
    )

    articleGrpServer := new(ArticleGrpcServer)
    articleGrpServer.createHandler = createHandler

    return articleGrpServer
}

ArticleGrpcServer 的作用

1.實現了 pb.ArticleServiceServer 介面的所有方法,實現了”繼承“,也可以說 ArticleGrpcServer 的例項是一個 pb.ArticleServiceServer 型別。

2.提供了定義了 create 和 detail 兩個 grpctransport.Handler。 目的是為了對接 go-kit 的模型

當通過 grpc 呼叫 Create 方法時,Create 只做資料傳遞, Create 內部又呼叫 createHandler,這樣請求就轉交給 go-kit 處理了

NewArticleGrpcServer的作用 返回 proto 中定義的 article grpc server,將 grpc 服務暴露出去,供外層的 grpc router 呼叫,它融合了多個 handler。

go-kit 的 Handler 呼叫 endpoint、decodeRequest、encodeResponse。

createHandler := grpctransport.NewServer(
    endpoint.MakeCreateEndpoint(svc),
    encodeCreateResponse,
    decodeCreateRequest,
    opts...,
)

Router + Server

在 router 層將 transport 層暴露的服務註冊進來。

// router/grpcrouter/article.go
package grpcrouter

import (
    pb "demo/pb/article"
    "demo/service"
    transport "demo/transport/grpc/article"
    "google.golang.org/grpc"
)

func RegisterRouter(grpcServer *grpc.Server) {
    pb.RegisterArticleServiceServer(grpcServer, transport.NewArticleGrpcServer(service.NewArticleService()))
}

server 層引入 router 層,啟動 grpc 服務。

// server/grpc/server.go
package grpc

import (
    "demo/router/grpcrouter"
    "net"

    grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
    "google.golang.org/grpc"
)

var opts = []grpc.ServerOption{
    grpc_middleware.WithUnaryServerChain(
        RecoveryInterceptor,
    ),
}

var grpcServer = grpc.NewServer(opts...)

func Run(addr string, errc chan error) {

    // 註冊grpcServer
    grpcrouter.RegisterRouter(grpcServer)

    lis, err := net.Listen("tcp", addr)
    if err != nil {
        errc <- err
        return
    }

    errc <- grpcServer.Serve(lis)
}

最後在一個統一的指令碼中呼叫 grpc.Run 啟動 grpc 服務和之前實現的 http 服務。

// cmd/server/sever.go
package main

import http "demo/server/http"
import grpc "demo/server/grpc"

func main() {
    errc := make(chan error)

    go http.Run("0.0.0.0:8080", errc)
    go grpc.Run("0.0.0.0:5000", errc)

    log.WithField("error", <-errc).Info("Exit")
}

執行指令碼

go run cmd/server/sever.go

完成!

參考

github.com/FengGeSe/demo

github.com/junereycasuga/gokit-grp...

github.com/win5do/go-microservice-...

個人部落格同步文章 利用 git-kit 實現支援 http 和 grpc 的微服務

本作品採用《CC 協議》,轉載必須註明作者和本文連結