Featured image of post Twirp初相识

Twirp初相识

什么是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等公司也是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实现框架,是一个非常适合纳入考虑的框架。

comments powered by Disqus
Built with Hugo
主题 StackJimmy 设计