Go 每日一庫之 twirp

簡介

twirp是一個基於 Google Protobuf 的 RPC 框架。twirp經過在.proto文件中定義服務,而後自動生產服務器和客戶端的代碼。讓咱們能夠將更多的精力放在業務邏輯上。咦?這不就是 gRPC 嗎?不一樣的是,gRPC 本身實現了一套 HTTP 服務器和網絡傳輸層,twirp 使用標準庫net/http。另外 gRPC 只支持 HTTP/2 協議,twirp 還能夠運行在 HTTP 1.1 之上。同時 twirp 還可使用 JSON 格式交互。固然並非說 twirp 比 gRPC 好,只是多瞭解一種框架也就多了一個選擇😊html

快速使用

首先須要安裝 twirp 的代碼生成插件:git

$ go get github.com/twitchtv/twirp/protoc-gen-twirp

上面命令會在$GOPATH/bin目錄下生成可執行程序protoc-gen-twirp。個人習慣是將$GOPATH/bin放到 PATH 中,因此可在任何地方執行該命令。github

接下來安裝 protobuf 編譯器,直接到 GitHub 上https://github.com/protocolbuffers/protobuf/releases下載編譯好的二進制程序放到 PATH 目錄便可。golang

最後是 Go 語言的 protobuf 生成插件:web

$ go get github.com/golang/protobuf/protoc-gen-go

一樣地,命令protoc-gen-go會安裝到$GOPATH/bin目錄中。編程

本文代碼採用Go Modules。先建立目錄,而後初始化:json

$ mkdir twirp && cd twirp
$ go mod init github.com/darjun/go-daily-lib/twirp

接下來,咱們開始代碼編寫。先編寫.proto文件:瀏覽器

syntax = "proto3";
option go_package = "proto";

service Echo {
  rpc Say(Request) returns (Response);
}

message Request {
  string text = 1;
}

message Response {
  string text = 2;
}

咱們定義一個service實現echo功能,即發送什麼就返回什麼。切換到echo.proto所在目錄,使用protoc命令生成代碼:服務器

$ protoc --twirp_out=. --go_out=. ./echo.proto

上面命令會生成echo.pb.goecho.twirp.go兩個文件。前一個是 Go Protobuf 文件,後一個文件中包含了twirp的服務器和客戶端代碼。微信

而後咱們就能夠編寫服務器和客戶端程序了。服務器:

package main

import (
  "context"
  "net/http"

  "github.com/darjun/go-daily-lib/twirp/get-started/proto"
)

type Server struct{}

func (s *Server) Say(ctx context.Context, request *proto.Request) (*proto.Response, error) {
  return &proto.Response{Text: request.GetText()}, nil
}

func main() {
  server := &Server{}
  twirpHandler := proto.NewEchoServer(server, nil)

  http.ListenAndServe(":8080", twirpHandler)
}

使用自動生成的代碼,咱們只須要 3 步便可完成一個 RPC 服務器:

  1. 定義一個結構,能夠存儲一些狀態。讓它實現咱們定義的service接口;
  2. 建立一個該結構的對象,調用生成的New{{ServiceName}}Server方法建立net/http須要的處理器,這裏的ServiceName爲咱們的服務名;
  3. 監聽端口。

客戶端:

package main

import (
  "context"
  "fmt"
  "log"
  "net/http"

  "github.com/darjun/go-daily-lib/twirp/get-started/proto"
)

func main() {
  client := proto.NewEchoProtobufClient("http://localhost:8080", &http.Client{})

  response, err := client.Say(context.Background(), &proto.Request{Text: "Hello World"})
  if err != nil {
    log.Fatal(err)
  }
  fmt.Printf("response:%s\n", response.GetText())
}

twirp也生成了客戶端相關代碼,直接調用NewEchoProtobufClient鏈接到對應的服務器,而後調用rpc請求。

開啓兩個控制檯,分別運行服務器和客戶端程序。服務器:

$ cd server && go run main.go

客戶端:

$ cd client && go run main.go

正確返回結果:

response:Hello World

爲了便於對照,下面列出該程序的目錄結構。也能夠去個人 GitHub 上查看示例代碼:

get-started
├── client
│   └── main.go
├── proto
│   ├── echo.pb.go
│   ├── echo.proto
│   └── echo.twirp.go
└── server
    └── main.go

JSON 客戶端

除了使用 Protobuf,twirp還支持 JSON 格式的請求。使用也很是簡單,只須要在建立Client時將NewEchoProtobufClient改成NewEchoJSONClient便可:

func main() {
  client := proto.NewEchoJSONClient("http://localhost:8080", &http.Client{})

  response, err := client.Say(context.Background(), &proto.Request{Text: "Hello World"})
  if err != nil {
    log.Fatal(err)
  }
  fmt.Printf("response:%s\n", response.GetText())
}

Protobuf Client 發送的請求帶有Content-Type: application/protobufHeader,JSON Client 則設置Content-Typeapplication/json。服務器收到請求時根據Content-Type來區分請求類型:

// proto/echo.twirp.go
unc (s *echoServer) serveSay(ctx context.Context, resp http.ResponseWriter, req *http.Request) {
  header := req.Header.Get("Content-Type")
  i := strings.Index(header, ";")
  if i == -1 {
    i = len(header)
  }
  switch strings.TrimSpace(strings.ToLower(header[:i])) {
  case "application/json":
    s.serveSayJSON(ctx, resp, req)
  case "application/protobuf":
    s.serveSayProtobuf(ctx, resp, req)
  default:
    msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type"))
    twerr := badRouteError(msg, req.Method, req.URL.Path)
    s.writeError(ctx, resp, twerr)
  }
}

提供其餘 HTTP 服務

實際上,twirpHandler只是一個http的處理器,正如其餘千千萬萬的處理器同樣,沒什麼特殊的。咱們固然能夠掛載咱們本身的處理器或處理器函數(概念有不清楚的能夠參見個人《Go Web 編程》系列文章

type Server struct{}

func (s *Server) Say(ctx context.Context, request *proto.Request) (*proto.Response, error) {
  return &proto.Response{Text: request.GetText()}, nil
}

func greeting(w http.ResponseWriter, r *http.Request) {
  name := r.FormValue("name")
  if name == "" {
    name = "world"
  }

  w.Write([]byte("hi," + name))
}

func main() {
  server := &Server{}
  twirpHandler := proto.NewEchoServer(server, nil)

  mux := http.NewServeMux()
  mux.Handle(twirpHandler.PathPrefix(), twirpHandler)
  mux.HandleFunc("/greeting", greeting)

  err := http.ListenAndServe(":8080", mux)
  if err != nil {
    log.Fatal(err)
  }
}

上面程序掛載了一個簡單的/greeting請求,能夠經過瀏覽器來請求地址http://localhost:8080/greetingtwirp的請求會掛載到路徑twirp/{{ServiceName}}這個路徑下,其中ServiceName爲服務名。上面程序中的PathPrefix()會返回/twirp/Echo

客戶端:

func main() {
  client := proto.NewEchoProtobufClient("http://localhost:8080", &http.Client{})

  response, _ := client.Say(context.Background(), &proto.Request{Text: "Hello World"})
  fmt.Println("echo:", response.GetText())

  httpResp, _ := http.Get("http://localhost:8080/greeting")
  data, _ := ioutil.ReadAll(httpResp.Body)
  httpResp.Body.Close()
  fmt.Println("greeting:", string(data))

  httpResp, _ = http.Get("http://localhost:8080/greeting?name=dj")
  data, _ = ioutil.ReadAll(httpResp.Body)
  httpResp.Body.Close()
  fmt.Println("greeting:", string(data))
}

先運行服務器,而後執行客戶端程序:

$ go run main.go
echo: Hello World
greeting: hi,world
greeting: hi,dj

發送自定義的 Header

默認狀況下,twirp實現會發送一些 Header。例如上面介紹的,使用Content-Type辨別客戶端使用的協議格式。有時候咱們可能須要發送一些自定義的 Header,例如tokentwirp提供了WithHTTPRequestHeaders方法實現這個功能,該方法返回一個context.Context。發送時會將保存在該對象中的 Header 一併發送。相似地,服務器使用WithHTTPResponseHeaders發送自定義 Header。

因爲twirp封裝了net/http,致使外層拿不到原始的http.Requesthttp.Response對象,因此 Header 的讀取有點麻煩。在服務器端,NewEchoServer返回的是一個http.Handler,咱們加一層中間件讀取http.Request。看下面代碼:

type Server struct{}

func (s *Server) Say(ctx context.Context, request *proto.Request) (*proto.Response, error) {
  token := ctx.Value("token").(string)
  fmt.Println("token:", token)

  err := twirp.SetHTTPResponseHeader(ctx, "Token-Lifecycle", "60")
  if err != nil {
    return nil, twirp.InternalErrorWith(err)
  }
  return &proto.Response{Text: request.GetText()}, nil
}

func WithTwirpToken(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    token := r.Header.Get("Twirp-Token")
    ctx = context.WithValue(ctx, "token", token)
    r = r.WithContext(ctx)

    h.ServeHTTP(w, r)
  })
}

func main() {
  server := &Server{}
  twirpHandler := proto.NewEchoServer(server, nil)
  wrapped := WithTwirpToken(twirpHandler)

  http.ListenAndServe(":8080", wrapped)
}

上面程序給客戶端返回了一個名爲Token-Lifecycle的 Header。客戶端代碼:

func main() {
  client := proto.NewEchoProtobufClient("http://localhost:8080", &http.Client{})

  header := make(http.Header)
  header.Set("Twirp-Token", "test-twirp-token")

  ctx := context.Background()
  ctx, err := twirp.WithHTTPRequestHeaders(ctx, header)
  if err != nil {
    log.Fatalf("twirp error setting headers: %v", err)
  }

  response, err := client.Say(ctx, &proto.Request{Text: "Hello World"})
  if err != nil {
    log.Fatalf("call say failed: %v", err)
  }
  fmt.Printf("response:%s\n", response.GetText())
}

運行程序,服務器正確獲取客戶端傳過來的 token。

請求路由

咱們前面已經介紹過了,twirpServer實際上也就是一個http.Handler,若是咱們知道了它的掛載路徑,徹底能夠經過瀏覽器或者curl之類的工具去請求。咱們啓動get-started的服務器,而後用curl命令行工具去請求:

$ curl --request "POST" \
  --location "http://localhost:8080/twirp/Echo/Say" \
  --header "Content-Type:application/json" \
  --data '{"text":"hello world"}'\
  --verbose
{"text":"hello world"}

這在調試的時候很是有用。

總結

本文介紹了 Go 的一個基於 Protobuf 生成代碼的 RPC 框架,很是簡單,小巧,實用。twirp對許多經常使用的編程語言都提供了支持。能夠做爲 gRPC 等的備選方案考慮。

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. twirp GitHub:https://github.com/twitchtv/twirp
  2. twirp 官方文檔:https://twitchtv.github.io/twirp/docs/intro.html
  3. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索