RPCフレームワークTwirpを試してみる

先日TwitchからリリースされたRPCフレームワークTwirpを試してみました。

* [Twirp: a sweet new RPC framework for Go – Twitch Blog](https://blog.twitch.tv/twirp-a-sweet-new-rpc-framework-for-go-5f2febbf35f)
* [twitchtv/twirp: A simple RPC framework with protobuf service definitions](https://github.com/twitchtv/twirp)

# Twirpとは

Twirpは[gRPC](https://grpc.io/)と同じようにProtocol Buffersの`.proto`ファイルからGoのコードを生成しますが以下のような違いがあります。

* HTTP/1.1を使用
  * gRPCはHTTP/2を使用
* JSONクライアントサポート
  * 他の言語でクライアントを書くのが比較的簡単
  * cURL等を使ってコマンドラインからリクエストを送信しデバッグすることができる

# 動作に必要なツールをインストール

`protoc`などのツールをインストールします。

[Install and Update Twirp · twitchtv/twirp Wiki](https://github.com/twitchtv/twirp/wiki/Install-and-Update-Twirp)

Go(1.7+)とProtocol Bufferをインストール。（Goは環境変数の設定が必要かもしれません。）

```
brew install go
brew install protobuf
```

[retool](https://github.com/twitchtv/retool)をインストール。
`go get`で`twirp`をインストールすることもできますが`retool`を使うとプロジェクトごとにインストールできます。

```
go get github.com/twitchtv/retool
```

## protoファイルの作成とコード生成

twirpのWikiにディレクトリ構成等のベストプラクティスがまとめてあったので、これを参考に進めてみます。

[Best Practices · twitchtv/twirp Wiki](https://github.com/twitchtv/twirp/wiki/Best-Practices)

今回は`helloworld`というサービスを作ることにします。

ディレクトリを作成。

```
mkdir -p cmd/server
mkdir -p cmd/client
mkdir -p rpc/helloworld
```

protoファイルを作成

```proto:rpc/helloworld/service.proto
syntax = "proto3";
package twirp.example.helloworld;
option go_package = "helloworld";

service HelloWorld {
  rpc Hello(HelloReq) returns (HelloResp);
}

message HelloReq {
  string subject = 1;
}

message HelloResp {
  string text = 1;
}
```

`retool`で`protoc-gen-go`と`protoc-gen-twirp`をインストール。

```sh
retool add github.com/golang/protobuf/protoc-gen-go master
retool add github.com/twitchtv/twirp/protoc-gen-twirp master
```

コード生成コマンド実行用のMakefileを作成します。`retool`でインストールしたツールを使う場合はコマンドの頭に`retool do`をつけます。

```Makefile
gen:
	retool do protoc --proto_path=. --twirp_out=. --go_out=. rpc/helloworld/service.proto
```

`make gen`コマンドを実行すると`rpc/helloworld`に`service.pg.go`と`service.twirp.go`が生成されます。

# サーバーとクライアントを実装

生成されたコードを使ってサーバーとクライアントを実装します。

```go:cmd/server/main.go
package main

import (
	"context"
	"net/http"

	pb "github.com/atsaki/twirp-example/rpc/helloworld"
)

type HelloWorldServer struct{}

func (s *HelloWorldServer) Hello(ctx context.Context, req *pb.HelloReq) (resp *pb.HelloResp, err error) {
	return &pb.HelloResp{Text: "Hello " + req.GetSubject()}, nil
}

func main() {
	twirpHandler := pb.NewHelloWorldServer(&HelloWorldServer{}, nil)
	mux := http.NewServeMux()
	mux.Handle(pb.HelloWorldPathPrefix, twirpHandler)
	http.ListenAndServe(":8080", mux)
}
```

```go:cmd/client/main.go
package main

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

	pb "github.com/atsaki/twirp-example/rpc/helloworld"
)

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

	subject := ""
	if len(os.Args) > 1 {
		subject = os.Args[1]
	}
	resp, err := client.Hello(context.Background(), &pb.HelloReq{Subject: subject})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.GetText())
}
```

サーバーを実行した状態で、

```
go run cmd/server/main.go
```

クライアントを実行すると、

```
go run cmd/client/main.go Twirp
```

以下のような文字列が出力されます。

```
Hello Twirp
```

# cURLでリクエストを送信する

上の例ではProtocol Buffersのクライアントを作成しサーバーにリクエストを送信しましたが、cURLなどでリクエストを送信し結果をJSONで取得することもできます。

リクエストは以下のURIに送信します。
`<package>`は`.proto`ファイルで`package`に指定した値になります。

```
/twirp/<package>.<service>/<method>
```

[HTTP Routing and Serialization · twitchtv/twirp Wiki](https://github.com/twitchtv/twirp/wiki/HTTP-Routing-and-Serialization)

今回の例では次のようになります。

```
curl -X POST \
     -H "Content-Type:application/json" \
     -d '{"subject": "Twirp"}' \
     http://localhost:8080/twirp/twirp.example.helloworld.HelloWorld/Hello
```

サーバーが起動している状態で実行すると結果がJSONで返ってきます。

```
{"text":"Hello Twirp"}
```