Go Daily library twirp

Posted by om.bitsian on Wed, 10 Jun 2020 19:04:20 +0200

brief introduction

twirp Is a RPC framework based on Google Protobuf.Trp automatically produces server and client code by defining the service in the.proto file.Let's focus more on business logic.Huh?Isn't this gRPC?Unlike gRPC, which implements its own set of HTTP servers and network transport layers, twirp uses the standard library net/http.In addition, gRPC only supports the HTTP/2 protocol, and twirp can also run over HTTP 1.1.Trp can also interact using JSON format.Not that twirp is better than gRPC, but knowing more about one framework gives you one more choice(viii)

Quick use

First you need to install the code generation plug-in for twirp:

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

The above command generates the executable protoc-gen-twirp in the $GOPATH/bin directory.My habit is to put $GOPATH/bin in the PATH, so I can execute it anywhere.

Next, install the protobuf compiler, directly onto GitHub https://github.com/protocolbuffers/protobuf/releases Download the compiled binaries and place them in the PATH directory.

Finally, the protobuf generation plug-in for the Go language:

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

Similarly, the command protoc-gen-go is installed in the $GOPATH/bin directory.

The code in this article uses Go Modules.First create the directory, then initialize:

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

Next, let's start coding.Write the.proto file first:

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

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

message Request {
  string text = 1;
}

message Response {
  string text = 2;
}

We define a service that implements echo, that is, what is sent and what is returned.switch toEcho.protoIn your directory, use the protoc command to generate code:

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

The above command will generateEcho.pb.goandEcho.twirp.goTwo files.The first is the Go Protobuf file, and the second contains the server and client code for twirp.

Then we can write server and client programs.The server:

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)
}

With automatically generated code, we can complete an RPC server in only three steps:

  1. Define a structure that can store some states.Let it implement the service interface we defined;
  2. Create an object of this structure and call the generated New{{ServiceName}}Server method to create the processor required by net/http, where ServiceName is our service name;
  3. Listen port.

Client:

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())
}

Trp also generates client-side code that directly calls the NewEchoProtobufClient to connect to the corresponding server and then calls the rpc request.

Open two consoles to run server and client programs.The server:

$ cd server && go run main.go

Client:

$ cd client && go run main.go

Return results correctly:

response:Hello World

For ease of comparison, the catalog structure of the program is listed below.You can also check out the sample code on my GitHub:

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

JSON Client

In addition to using Protobuf, twirp supports requests in JSON format.It is also very simple to use, simply change the NewEchoProtobufClient to NewEchoJSONClient when creating the Client:

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())
}

The request sent by the Protobuf Client has a Header with Content-Type: application/protobuf, and the JSON Client sets Content-Type to application/json.When the server receives a request, it distinguishes the type of request based on 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)
  }
}

Provide additional HTTP services

In fact, twirpHandler is just an http processor, just like millions of other processors, nothing special.Of course we can mount our own processor or processor functions (see my own if the concept is unclear) Go Web Programming Series Articles:

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)
  }
}

The program above mounts a simple / greeting request that allows you to request addresses through a browserHttp://localhost: 8080/greeting.The request for twirp is mounted under the path twirp/{{ServiceName}} where ServiceName is the service name.The PatPrefix () in the above program returns / twirp/Echo.

Client:

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))
}

Run the server first, then execute the client program:

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

Send a custom Header

By default, the twirp implementation sends some headers.For example, as described above, use Content-Type to identify the protocol format used by clients.Sometimes we may need to send some custom headers, such as token.Twirp provides the WithHTTPRequestHeaders method to do this, which returns aContext.Context.Headers saved in this object are sent together when sent.Similarly, the server uses WithHTTPResponseHeaders to send custom headers.

Because twirp encapsulates net/http, the outer layer cannot get the originalHttp.RequestandHttp.ResponseObject, so Header is a bit cumbersome to read.On the server side, NewEchoServer returns aHttp.Handler, we add a layer of middleware to readHttp.Request.Look at the following code:

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)
}

The above program returns a Header named Token-Lifecycle to the client.Client Code:

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())
}

Run the program and the server gets the token from the client correctly.

Request Routing

As we've already mentioned, Trp's Server is actually just oneHttp.HandlerIf we know its mount path, we can request it through a browser or a tool like curl.We start the get-started server and use the curl command line tool to request:

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

This is useful when debugging.

summary

This paper introduces a RPC framework for generating code based on Protobuf, which is very simple, compact and practical.twirp supports many common programming languages.It can be considered as an alternative to gRPC, etc.

If you find a fun and useful GoLanguage Library, you are welcome to submit your issue on GitHub, the GoDaily Library_

Reference resources

  1. twirp GitHub: https://github.com/twitchtv/twirp
  2. Official twirp documentation: https://twitchtv.github.io/twirp/docs/intro.html
  3. Go Daily Library of GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to my WeChat Public Number GoUpUp to learn and progress together.

Topics: Go github JSON curl Programming