Hackernoon logoTwirp : A better gRPC by@sakibsami

Twirp : A better gRPC

Author profile picture

@sakibsamiSakib Sami

What is gRPC : gRPC is a RPC framework based on google’s protobuf used mostly for server to server communication. We at Pathao heavily use gRPC to manage our backend for server to server communication as everything is on micro-service architecture.

So when working on gRPC found that few things on gRPC is not straightforward like running the Server, Using middleware etc.

Few days back I found another RPC framework name Twirp which is also based on protobuf but with much simplicity.

Here is a basic tutorial how you can implement your service in Golang using Twirp.

First you have to install protobuf and twirp plugin for golang.

brew install protobuf

*** Kindly google it how to install protobuf if you are not using OSX.

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

*** Above command will install protobuf compiler for golang

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

*** Above command will install twirp plugin

Lets start building the API and discuss about the flexibilities twirp provides,

Step 1 : We have to define proto files and this part is completely similar to gRPC. Check this files having proto definitions.

Step 2 : Generate .go file from proto definitions. It’s almost similar to gRPC.

protoc --proto_path=$GOPATH/src:. --twirp_out=./../../../ --go_out=./../../../ ./protobuf/pdefs/*.proto

Everything is similar other than --twirp_out=. . For gRPC we used to write here --go_out=plugins=grpc:.

Step 3 : Implementing the server. So at this stage we have to implement the server definition generated by twirp compiler which is similar to gRPC.

type Server struct {
}

func (s *Server) SayHello(ctx context.Context, req *pdefs.ReqHello) (*pdefs.ResHello, error) {
return &pdefs.ResHello{
GoodBye: &pdefs.GoodBye{
UserID: req.GetHello().GetUserID(),
Message: req.GetHello().GetMessage(),
},
}, nil
}

Step 4 : Spin up the server.

func RunServer() {
   addr := ":7007"
twirpServer := &Server{}
handler := pdefs.NewAwesomeTwirpServiceServer(twirpServer, twirp.ChainHooks(NewAuthHook()))

routes := chi.NewRouter()
routes.Use(ForwardHttpHeaders)
routes.Mount(pdefs.AwesomeTwirpServicePathPrefix, handler)

server := http.Server{
Addr: addr,
Handler: routes,
}
   server.ListenAndServe()
}

Have a close look on above code. At line 3 created an instance of server we have implemented in previous step. Then created twirp handler. If you notice that 1st parameter is twirp server and 2nd parameter is hooks. We can put single hook or multiple hook. If you are going to use multiple hook in that case you can do using ChainHooks . ChainHooks expects parameter which is list of hooks. Example of hooks,

hook := &twirp.ServerHooks{}
hook.RequestReceived = func(ctx context.Context) (context.Context, error) {

return ctx, nil
}
hook.RequestRouted = func(i context.Context) (context.Context, error) {

return i, nil
}
hook.ResponsePrepared = func(i context.Context) context.Context {

return i
}
hook.ResponseSent = func(i context.Context) {

}
hook.Error = func(i context.Context, error twirp.Error) context.Context {

return i
}

In next step created an instance of chi.Router which one we mostly use for http server. And in router mounted twirp handler ! Wondering ! Yah, that’s the magic. You can use both http and twirp together in a single router. By default twirp spin ups both rpc and http server. So if you want to call SayHello() just implement the proto client and do call it.

client := pdefs.NewAwesomeTwirpServiceProtobufClient("http://localhost:7007", &http.Client{})
resp, err := client.SayHello(ctx, &pdefs.ReqHello{
Hello: &pdefs.Hello{
UserID: "1",
Message: "Hello World",
},
})

And if the client doesn’t support proto or you want to do REST call the url is,

http://localhost:7007/twirp/pdefs.AwesomeTwirpService/SayHello

and http verb is always POST .

So no more you have to bother about whether your platform supports gRPC or not. If it doesn’t simply use RESTful API and you don’t have to do any extra work for this.

Note 1 : Twirp doesn’t send context values from client to server automatically. But we may require to authenticate every request lets say using token like RESTful services do. How to handle this ?

header := make(http.Header)
header.Set("UID", "uDRlDxQYbFVXarBvmTncBoWKcZKqrZTY")
header.Set("AppKey", "1")
ctx := context.Background()
ctx, err := twirp.WithHTTPRequestHeaders(ctx, header)

At client side have to add values in context as header value and pass the context with request.

At server side have to do a hack which is. Have to implement a middleware in router which will parse headers and then will add them in context to forward to twirp handlers.

func ForwardHttpHeaders(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for k, v := range r.Header {
ctx = context.WithValue(ctx, k, v)
}
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

And in twirp middleware / handler,

appValues := ctx.Value("Appkey")

Much simpler isn’t it ? ;)

Checkout the complete source code of sample project : https://github.com/s4kibs4mi/awesome-twirp

Twirp Homepage : https://twitchtv.github.io/twirp/

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!