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.go
和echo.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 服務器:
service
接口;New{{ServiceName}}Server
方法建立net/http
須要的處理器,這裏的ServiceName
爲咱們的服務名;客戶端:
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
除了使用 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/protobuf
的Header
,JSON Client 則設置Content-Type
爲application/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) } }
實際上,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/greeting
。twirp
的請求會掛載到路徑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
默認狀況下,twirp
實現會發送一些 Header。例如上面介紹的,使用Content-Type
辨別客戶端使用的協議格式。有時候咱們可能須要發送一些自定義的 Header,例如token
。twirp
提供了WithHTTPRequestHeaders
方法實現這個功能,該方法返回一個context.Context
。發送時會將保存在該對象中的 Header 一併發送。相似地,服務器使用WithHTTPResponseHeaders
發送自定義 Header。
因爲twirp
封裝了net/http
,致使外層拿不到原始的http.Request
和http.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。
咱們前面已經介紹過了,twirp
的Server
實際上也就是一個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😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~