什么是Twirp?
Twirp是Twitch在2018年开源的RPC框架。正如同他们在发布文章中说的那样,RPC相对于普通的RESTful API更方便设计、组织和维护,让开发者更加专注于业务。但是同样的,在Go社区中重要的gRPC方案严重与HTTP/2绑定,这也成为一个制约其推广的问题:HTTP/2的复杂性其实并不必要;与Go Runtime的割裂也是另外一个问题,导致部分优化难以直接通过升级Go版本在gRPC上显现。
Twirp则选择保留了部分好的地方:使用Protobuf这个IDL约束请求/返回类型,这样可以最大化借助Protobuf带来的优势,生成客户端和服务端代码。但是Twirp选择与Go标准库集成,这样可以更好的利用Go本身升级带来的优化。这同时也保证了Twirp本身的简洁性。同时,你也可以很方便的使用cURL等传统工具,借助json请求测试,而不需要手工处理二进制数据。同样的,借助Go标准库,未来Twirp可以更好的升级成HTTP/3而不是像gRPC一样等待上游更新。当然如果你更倾向于使用gRPC相关的实践,那么connect-go可能是你的另外一个不错选择。
当然,如果说缺点,Twirp并不完美:小众的社区,缺少生态,缺少相关信息内容等等。不过这些仍旧是瑕不掩瑜。毕竟实现一个相关的功能其实并不那么复杂。
如何使用Twirp
Twirp虽然官网比较简单,甚至社区也不是很大的样子,但是基本上需求的数据基本都可以在官网上找到入口。但是这也有个问题,导致整个流程对新手并不友好,有比较高的上手门槛。接下来的内容主要是完善这部分的内容,方便新手用户使用。
安装Protobuf相关工具
由于Twirp同样使用Protobuf,我们需要使用相关工具。首先是Protobuf,接下来是一些protoc-gen工具:
brew install protobuf # Mac Only
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install github.com/twitchtv/twirp/protoc-gen-twirp@latest
可选项:Buf
Buf是一个Protobuf管理工具,帮助你实现Schema Driven Development实践。它提供了一个CLI管理工具(支持lint,生成和破坏性检查等功能)和类似注册中心机制的BSR(Buf Schema Registry),你可以在这里管理你的Schema版本和引用其他公开服务的Schema。不使用Buf并不会带来功能缺失,并且Buf提供了付费SaaS服务(测试期间免费),可以根据你的情况选择是否使用。
brew install bufbuild/buf/buf # Mac Only
可选项:Taskfile
Taskfile是我常用来替代Makefile的工具。这并不是必须的工具,你同样可以使用手工执行命令行和Makefile命令进行。事实上,使用Makefile其实可以更好的在Jenkins之类的pipeline里执行,但是对Github Action等现代pipeline而言,区别并不大。
brew install go-task/tap/go-task # Mac Only
生成项目文件
这里我们使用一个简单的Greeter程序演示使用。假设我们已经存在了一个Go的空项目,那么我们接下来需要创建对应的目录和文件。按照官方的建议,我们可以使用如下结构创建我们的项目,你可以在Github上查看完整的代码:
$ tree .
.
├── README.md
├── Taskfile.yaml
├── buf.gen-ts.yaml
├── buf.gen.yaml
├── buf.yaml
├── build
├── client
│ ├── package.json
│ ├── pnpm-lock.yaml
│ └── src
│ └── protoc-gen-twirp-es.ts
├── cmd
│ └── greeter
│ ├── main.go
│ └── main_test.go
├── go.mod
├── go.sum
├── internal
│ └── greetersvc
│ └── service.go
└── rpc
└── greeter
└── v1
└── service.proto
编写服务端
我们先看一下greeter的服务定义:
syntax = "proto3";
package rpc.greeter.v1;
option go_package = "rpc/greeter/v1";
service GreeterService {
rpc SayHello (SayHelloRequest) returns (SayHelloResponse);
}
message SayHelloRequest {
string name = 1;
}
message SayHelloResponse {
string message = 1;
}
我们可以使用task gen
生成Protobuf对应的代码:
$ task gen
task: [gen] buf generate
task: [gen] go mod tidy
go: finding module for package github.com/twitchtv/twirp
go: finding module for package google.golang.org/protobuf/proto
go: finding module for package github.com/twitchtv/twirp/ctxsetters
go: finding module for package google.golang.org/protobuf/reflect/protoreflect
go: finding module for package google.golang.org/protobuf/runtime/protoimpl
go: finding module for package google.golang.org/protobuf/encoding/protojson
go: found github.com/twitchtv/twirp in github.com/twitchtv/twirp v8.1.3+incompatible
go: found github.com/twitchtv/twirp/ctxsetters in github.com/twitchtv/twirp v8.1.3+incompatible
go: found google.golang.org/protobuf/encoding/protojson in google.golang.org/protobuf v1.28.1
go: found google.golang.org/protobuf/proto in google.golang.org/protobuf v1.28.1
go: found google.golang.org/protobuf/reflect/protoreflect in google.golang.org/protobuf v1.28.1
go: found google.golang.org/protobuf/runtime/protoimpl in google.golang.org/protobuf v1.28.1
go: downloading github.com/google/go-cmp v0.5.5
go: downloading golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
go: finding module for package github.com/pkg/errors
go: found github.com/pkg/errors in github.com/pkg/errors v0.9.1
接下来,我们就可以编辑internal/greetersvc/service.go
文件,添加服务的实现:
package greetersvc
import (
"context"
pb "github.com/ipfans/twirp-demo/rpc/greeter/v1"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, req *pb.SayHelloRequest) (*pb.SayHelloResponse, error) {
return &pb.SayHelloResponse{
Message: "Hello, " + req.Name,
}, nil
}
最后我们完善一下入口cmd/greeter/main.go
文件:
package main
import (
"net/http"
"time"
"github.com/ipfans/twirp-demo/internal/greetersvc"
greeterv1 "github.com/ipfans/twirp-demo/rpc/greeter/v1"
"github.com/twitchtv/twirp"
)
func main() {
server := &greetersvc.Server{} // implements `GreeterService` interface
twirpHandler := greeterv1.NewGreeterServiceServer(
server,
twirp.WithServerPathPrefix(""), // Default will be `twirp`
)
httpServer := &http.Server{
Addr: "127.0.0.1:8080",
Handler: twirpHandler,
ReadHeaderTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
}
err := httpServer.ListenAndServe()
if err != nil {
panic(err)
}
}
然后执行task
,启动Server看一下。
$ task
task: [gen] buf generate
task: [gen] go mod tidy
task: [lint] golangci-lint run ./...
task: [lint] buf lint
task: [build] go build -o build/greeter ./cmd/greeter
这样服务就监听在本地的8080端口上。我们也可以通过比如curlie等工具访问本地服务确定程序已经成功启动。
$ curlie POST http://localhost:8080/rpc.greeter.v1.GreeterService/SayHello -H "Content-Type: application/json" -d '{"name":"kevin"}'
HTTP/1.1 200 OK
Content-Length: 26
Content-Type: application/json
Date: Tue, 24 Jan 2023 14:15:06 GMT
{
"message": "Hello, kevin"
}
这里注意一下URL内容,我们可以看到http://localhost:8080/rpc.greeter.v1.GreeterService/SayHello
这个地址中,rpc.greeter.v1
是我们之前在Protobuf中定义的package名称,GreeterService
是Protobuf中的service名称,而SayHello
则是定义的rpc名称。因为我们在这个例子中将Prefix
设定为空,所以被跳过了,否则,完整的URL将会是http://localhost:8080/twipc/rpc.greeter.v1.GreeterService/SayHello
。在这个例子中,我们也是使用了JSON进行了数据传输和测试,这个传输数据格式会通过Content-Type
设定,并由框架自动处理。如果是需要传输Protobuf数据,则可以声明为application/protobuf
类型。
你会发现Twirp的这套协议中,URL组成对API网关会特别友好,这也是Twirp Wire Protocol
会被很多公司选择的原因。除了Twitch自身的使用外,包括SoundCloud、Github等公司也是Twirp Wire Protocol
的用户。当然,未必是Twirp本身这套框架的用户。
编写客户端
除了服务端以外,对微服务而言,常见的服务间调用非常重要的过程。这部分当然也包括Go本身和前端代码。
Go Client
这里首先介绍一下Go代码本身的实现。因为借助buf-cli
就包含了一套完整的客户端实现,我们可以通过NewGreeterServiceProtobufClient
初始化一个Protobuf的Client:
client := greeterv1.NewGreeterServiceProtobufClient("http://127.0.0.1:8080", &http.Client{
Timeout: 10 * time.Second,
}, twirp.WithClientPathPrefix(""))
resp, _ := client.SayHello(context.TODO(), &greeterv1.SayHelloRequest{Name: "Kevin"})
TypeScript Client
你可以在项目中可以看到client目录,这个目录是为了前端项目而存在的,对应的前端项目可以参考实现。如果需要在已有项目中实现,可以先安装必要的依赖:
pnpm i @bufbuild/protobuf @bufbuild/protoc-gen-es @bufbuild/protoplugin typescript tsx
我们可以使用buf生成前端TypeScript代码:
$ task gen-ts
task: [gen-ts] buf generate --template buf.gen-ts.yaml
这样就会在client/src/gen
目录下生成对应的TypeScript客户端代码。
client/src/gen
└── rpc
└── greeter
└── v1
├── service_pb.ts
└── service_twirp.ts
当然,你可以选择仅仅生成pb的代码,调用twirp的相关代码可以根据你自己的习惯自行编写。
Next
如此,一个服务完整的布局将已经构成,这样我们可以更加专注于业务本身,而不仅仅把时间投入在技术层面纠结上了。Twirp一个使用上的一个好处就在于它可以支持符合REST规范的API,也就是说可以使用和RESTful API相同的URL。这在实际的开发中有很大的便利性,因为可以使开发者在不改变原有API规范的前提下更换RPC的实现方式。
同时,Twirp也提供了一定的性能优势,在一些场景下也让开发者能够拥有良好的性能。根据某项benchmark数据,在使用Protobuf通讯时,相对JSON可以获得更好的性能数据。
总而言之,Twirp作为一个新兴的RPC实现框架,是一个非常适合纳入考虑的框架。