介绍
Protocol Buffers and gRPC是用于定义通过网络有效通信的微服务的流行技术。许多公司在Go中构建gRPC微服务,发布了他们开发的框架,本文将从gRPC入门开始,一步一步构建一个gRPC服务。
本文的源代码已经上传至Github。
背景
之前在B站看过一个gRPC教学视频,尝试跟着视频做但踩了不少的坑,因此决定自己动手从官方教程开始,完成一个gRPC项目。
开始
环境配置
首先要配置gRPC所需要的一些环境,由于本人使用Go语言进行开发,操作系统为Ubuntu20.04,因此配置gRPC-go的环境步骤很简单。
安装Go
Ubuntu下安装Go需要先下载Go的源码,本人采用的Go版本为1.18.3,源码下载地址为Go语言中文网:go1.18.3.linux-amd64.tar.gz。
下载完毕后,首先检查机器是否存在旧版本的Go,如果存在则删除,然后解压源码到/usr/local。
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.3.linux-amd64.tar.gz
添加/usr/local/go/bin到环境变量中,可以在命令行中直接执行:
export PATH=$PATH:/usr/local/go/bin
注意:在命令行中执行上述语句,只会在当前命令行环境下生效,如果关闭命令行后再执行go命令会报错,要解决这个问题,需要将这个语句添加到$HOME/.profile或/etc/profile中,并使用source命令生效
上述步骤完成后,检查Go环境是否安装成功:
go version
输出相应版本号则代表环境配置成功。
go version go1.18.3 linux/amd64
在这里,配置Go的proxy为国内代理,方便之后下载安装package时网速问题,由于安装的Go是1.13及以上的版本,因此直接执行以下命令。
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
安装Protocol buffer compiler
Ubuntu上使用apt或者apt-get安装Protocol buffer compiler,命令如下:
sudo apt install -y protobuf-compiler
检查是否安装成功:
protoc --version # Ensure compiler version is 3+
输出相应版本号则代表环境配置成功。
libprotoc 3.6.1
配置Go plugins
在配置Go plugins时,遇到了很多错误。
--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
或
protoc-gen-go-grpc: program not found or is not executable
网上的解决方法也不一定奏效,最后还是选择按照官网上的步骤安装对应版本的protoc-gen-go和protoc-gen-go-grpc。
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
注意这里都是从goole.golang.org下载的package
更新环境变量,将下面的命令添加到$HOME/.profile或/etc/profile中,source使之生效。
export PATH="$PATH:$(go env GOPATH)/bin"
到此为之,gRPC-go的环境就算配置完成了。
gRPC接口定义
.proto文件
第一步首先定义gRPC服务以及方法请求和响应类型。要定义服务,请在.proto文件中指定命名服务:
service NewService {
rpc GetHotTopNews(Request) returns (News) {}
}
然后在服务定义中定义RPC方法,指定它们的请求和响应类型。gRPC允许您定义四种服务方法:
- 一个简单的RPC,其中客户端使用存根向服务端发送请求并等待响应返回,就像正常的函数调用一样。
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- 服务端流式RPC,客户端向服务端发送请求并获取流以读回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 客户端流式RPC,其中客户端写入一系列消息并将它们发送到服务端,再次使用提供的流。一旦客户端完成了消息的写入,它会等待服务端读取所有消息并返回其响应。可以通过将stream关键字放在请求类型之前来指定客户端流式处理方法。
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 双向流式RPC,双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务端可以按照他们喜欢的任何顺序读取和写入:例如,服务端可以在写入响应之前等待接收所有客户端消息,或者它可以交替读取消息然后写入消息,或其他一些读取和写入的组合,保留每个流中消息的顺序。可以通过在请求和响应之前放置stream关键字来指定这种类型的方法。
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
我们将要实现一个获取热点新闻的gRPC接口,.proto文件包含服务方法中使用的所有请求和响应类型的协议缓冲区消息类型定义。例如,这里是Request消息类型:
message Request {
string type = 1;
int64 page = 2;
int64 size = 3;
int64 is_filter = 4;
}
以及Response定义:
message Response { repeated New news = 1; }
其中New的结构定义为:
message New {
string uniquekey = 1;
string title = 2;
string date = 3;
string category = 4;
string author_name = 5;
string url = 6;
string thumbnail_pic_s = 7;
int64 is_content = 8;
}
最后定义RPC接口:
syntax = "proto3";
```go
在这里插入代码片
option go_package = “./;protobuf”;
package protobuf;
service NewService {
rpc GetHotTopNews(Request) returns (Response) {}
}
注意这里加上了option go_package = "./;protobuf";,说明生成的pb.go的package名称。
### protoc命令
接下来,我们需要从.proto服务定义中生成gRPC客户端和服务端接口。我们使用带有特殊gRPC Go插件的protobuf compiler来执行此操作。
```bash
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative protobuf/*.proto
会在.proto文件同级目录下生成以下go文件:
- news.pb.go,其中包含用于填充、序列化和检索请求和响应消息类型的所有协议缓冲区代码
- news_grpc.pb.go,包含:1)客户端使用服务中定义的方法调用的接口类型(或存根);2)服务端要实现的接口类型,也使用服务中定义的方法。
这里我使用VS code进行开发,在编写.proto文件时推荐使用两个插件:
- vscode-proto3:用于识别.proto文件的一些语法
- clang-format:用于格式化.proto文件,需要使用sudo apt install clang-format,并且按照插件说明进行相应配置
Go服务构建
server
服务端需要实现gRPC的接口,首先定义一个结构体:
type Server struct {
protobuf.UnimplementedNewServiceServer
}
继承了生成的pb.go文件中的UnimplementedNewServiceServer,接着实现接口内的方法:
func (s *Server) GetHotTopNews(ctx context.Context, req *protobuf.Request) (*protobuf.Response, error) {
ret := srv.RequestPublishAPI()
return &protobuf.Response{
News: ret,
}, nil
}
这样,最基本的gRPC服务就能启动了。
func main() {
// register grpc service
s := grpc.NewServer()
protobuf.RegisterNewServiceServer(s, &Server{
})
// listen tcp connection
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// start grpc server
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
client
同样的,我们用go编写一个客户端来请求测试gRPC服务是否能工作。
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
)
func main() {
flag.Parse()
// Set up a connection to the server.
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := protobuf.NewNewServiceClient(conn)
// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.GetHotTopNews(ctx, &protobuf.Request{
})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
for _, v := range r.GetNews() {
fmt.Println(v)
}
}
至此,一个简单的gRPC服务就已经全部完成了,但我们获取热点新闻的接口是伪造的,因此我们加入免费的获取热点新闻API到项目中,让客户端有实际返回,API主要逻辑如下:
// NewService contains services that fetch new and convert to grpc protobuf
type NewService struct {
apiUri string
apiKey string
reqType string
page int
size int
isFilter int
}
func (s *NewService) RequestPublishAPI() []*protobuf.New {
reqUrl := fmt.Sprintf("%s?type=%s&page=%d&page_size=%d&is_filter=%d&key=%s", s.apiUri, s.reqType, s.page, s.size, s.isFilter, s.apiKey)
log.Printf("request url: %s", reqUrl)
method := "GET"
client := &http.Client{
}
req, err := http.NewRequest(method, reqUrl, nil)
if err != nil {
panic(err)
}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
var resp ApiResponse
err = json.Unmarshal(body, &resp)
if err != nil {
panic(err)
}
var ret []*protobuf.New
for _, n := range resp.Result.Data {
isContent, _ := strconv.Atoi(n.</