使用指南
本文作为对一些自己没有深究过的内容做一个索引和复习,内容可能包含官方文档,培训课,公开课,博客,著名程序员cv&debug网站等等。
安装略
简单的proto文件定义
语法(略)
一般报这种错误的话,多半是/usr/local/include路径下缺失了必要文件 两种解决办法
- empty.proto copy的本地 例如empty.proto 编译时指定empry.proto 的-I=xxx的绝对路径
- 把这些需要的*.proto,例如empry.proto,copy到/usr/include/路径下
user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.
你可以很容易找到参考1的翻译版本。本文使用的是proto3。
简单定义
hello.proto定义如下
syntax = "proto3";
option go_package="xxxmodule/api/gen/v1;hello";
message HelloRequest {
int64 a = 1;
string b = 2;
}
message HelloResponse {
string reply = 1;
}
service AuthService {
rpc Login (HelloRequest) returns (HelloResponse);
}
现在option go_package
是必须要指定的了,不然你会收获下面的提示。
一些需要注意的地方
编译命令
先从简单的命令入手。在hello.proto的同级目录
protoc --go_out=. hello.proto
--go_out表示输出的目录,这里是输出到到当前目录下, 会输出hello.pb.go。
进阶一点,依旧是同级目录。
mkdir -p api
protoc -I=. --go_out=plugins=grpc,paths=source_relative:./api hello.proto
-I表示.proto文件的目录,--go_out=plugins=grpc参数来生成proto中的定义的Service部分的grpc代码,如果不加plugins=grpc,就只生成message数据。这里使用plugins,也就是protoc-gen-go,是因为ProtoBuf本身不支持Go语言,所以我们需要protoc-gen-go来协助我们。paths=souce_relative:指定了要输出的路径为proto文件同级路径下的api目录(这个目录必须存在),hello.proto指定proto目录下的输入文件。 但这里有一个容易犯错的地方source_relative:后面的路径是从你执行这个命令的路径开始,而不是从你指定的proto文件目录开始。 你可以试一试在上一级目录执行这个命令
protoBuf 参数 (略)
依然可以在文章1中找到
protoc-gen-go 的参数
要将额外的参数传递给插件,使用逗号分隔的参数列表,以冒号与输出目录分隔:
protoc --go_out=plugins=grpc,import_path=mypackage:. *.proto
- paths=(import | source_relative) 包含两个可选命令,没有指定proto中的go_package时就需要import。source_relative 与输入文件.proto同级的路径。
- plugins=plugin1+plugin2 指定要加载的子插件列表。这个 repo 中唯一的插件是grpc.
- Mfoo/bar.proto=quux/shme M参数,指定.proto文件编译后的包名(foo/bar.proto编译后为包名为quux/shme) 以下参数已弃用,不应使用:
- import_prefix=xxx - 添加到所有导入开头的前缀。
- import_path=foo/bar- 如果没有输入文件声明,则用作包go_package。如果它包含斜杠,则忽略最右边斜杠之前的所有内容。
mac 踩坑
user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.
一般报这种错误的话,多半是/usr/local/include路径下缺失了必要文件 两种解决办法
- empty.proto copy的本地 例如empty.proto 编译时指定empry.proto 的-I=xxx的绝对路径
- 把这些需要的*.proto,例如empry.proto,copy到/usr/include/路径下
user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.
保证protoc和golang/protobuf版本一致即可。
容易产生困惑的地方
如果到这里好巧不巧你也搜到了文章1,顺着官网的quick_start做入门。那你大概率可能会遇到到几种写法了。此时你就已经满脸黑人问号了。我到底该用哪种啊。
假设你阅读过参考文章的1,2,3。那么有大概有如下几种写法。
- 本文一开始介绍的简单例子的进阶版
protoc --go_out=paths=source_relative:./gen -I. authenticator.proto
- Go Generated Code给出的版本
protoc --go_out=plugins=grpc:./gen --go_opt=paths=source_relative authenticator.proto
- --go_opt通过flag变量 source_relative 参数控制protoc编译出的相对路径为./gen
- 新版本grpc quick_start的例子
protoc --go-grpc_out=./gen --go-grpc_opt=paths=source_relative authenticator.proto
- 以及我使用的与1类似的写法,当然这几种都是等价的。 之后包括grpc-gateway的编译,我都使用这种方式
protoc -I=$PROTO_PATH --go_out=plugins=grpc,paths=source_relative:$GO_OUT_PATH ${DOMAIN}.proto
着实心累。老老实实proto里option go_package
写上。也不需要什么M参数指定--go_opt=M${PROTO_FILE}=${GO_IMPORT_PATH}
。用法4编译就完事啦。
gRPC的四种通信模式
简单模式(Simple RPC)
这种模式最为传统,即客户端发起一次请求,服务端响应一个数据,这和大家平时熟悉的RPC没有什么大的区别,所以不再详细介绍。
服务器端流 RPC(Server Sreaming RPC)
这种模式是客户端发起一次请求,服务端返回一段连续的数据流。典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。
客户端流 RPC(Client Streaming RPC)
与服务端数据流模式相反,这次是客户端源源不断的向服务端发送数据流,而在发送结束后,由服务端返回一个响应。典型的例子是物联网终端向服务器报送数据。
双向流 RPC(Bidirectional Streaming RPC)
顾名思义,这是客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,也就是可以实现实时交互。典型的例子是聊天机器人。
代码示例
简单模式
.proto
syntax = "proto3";
option go_package = ".;hello";
service Test {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
client
package main
import (
"context"
"fmt"
hello "go-interview/grpc"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
c := hello.NewTestClient(conn)
r, err := c.SayHello(context.Background(), &hello.HelloRequest{Name: "server"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
server
package main
import (
"context"
"fmt"
hello "go-interview/grpc"
"google.golang.org/grpc"
"net"
)
type Server struct {
//hello.UnimplementedTestServer //新版本的protoc编译后如需测试需要实现Server下的所有接口,为了方便测试就可以先引入这一条。
}
func (s *Server) SayHello(ctx context.Context, request *hello.HelloRequest) (*hello.HelloReply, error) {
return &hello.HelloReply{Message: "Hello " + request.Name}, nil
}
func main() {
g := grpc.NewServer()
s := Server{}
hello.RegisterTestServer(g, &s)
lis, err := net.Listen("tcp", fmt.Sprintf(":8080"))
if err != nil {
panic("failed to listen: " + err.Error())
}
g.Serve(lis)
}
编译命令
protoc -I . xxx.proto --go_out=plugins=grpc:.
流模式
proto
syntax = "proto3";//声明proto的版本 只能 是3,才支持 grpc
//声明 包名
option go_package=".;proto";
//声明grpc服务
service Greeter {
/*
以下 分别是 服务端 推送流, 客户端 推送流 ,双向流。
*/
rpc GetStream (StreamReqData) returns (stream StreamResData){}
rpc PutStream (stream StreamReqData) returns (StreamResData){}
rpc AllStream (stream StreamReqData) returns (stream StreamResData){}
}
//stream请求结构
message StreamReqData {
string data = 1;
}
//stream返回结构
message StreamResData {
string data = 1;
}
client
package main
import (
"google.golang.org/grpc"
proto "ttt"
"context"
_ "google.golang.org/grpc/balancer/grpclb"
"log"
"time"
)
const (
ADDRESS = "localhost:50052"
)
func main(){
//通过grpc 库 建立一个连接
conn ,err := grpc.Dial(ADDRESS,grpc.WithInsecure())
if err != nil{
return
}
defer conn.Close()
//通过刚刚的连接 生成一个client对象。
c := proto.NewGreeterClient(conn)
//调用服务端推送流
reqstreamData := &proto.StreamReqData{Data:"aaa"}
res,_ := c.GetStream(context.Background(),reqstreamData)
for {
aa,err := res.Recv()
if err != nil {
log.Println(err)
break
}
log.Println(aa)
}
//客户端 推送 流
putRes, _ := c.PutStream(context.Background())
i := 1
for {
i++
putRes.Send(&proto.StreamReqData{Data:"ss"})
time.Sleep(time.Second)
if i > 10 {
break
}
}
//服务端 客户端 双向流
allStr,_ := c.AllStream(context.Background())
go func() {
for {
data,_ := allStr.Recv()
log.Println(data)
}
}()
go func() {
for {
allStr.Send(&proto.StreamReqData{Data:"ssss"})
time.Sleep(time.Second)
}
}()
select {
}
}
server
package main
import (
"fmt"
"google.golang.org/grpc"
"log"
"net"
proto "ttt"
"sync"
"time"
)
const PORT = ":50052"
type server struct {
}
//服务端 单向流
func (s *server)GetStream(req *proto.StreamReqData, res proto.Greeter_GetStreamServer) error{
i:= 0
for{
i++
res.Send(&proto.StreamResData{Data:fmt.Sprintf("%v",time.Now().Unix())})
time.Sleep(1*time.Second)
if i >10 {
break
}
}
return nil
}
//客户端 单向流
func (s *server) PutStream(cliStr proto.Greeter_PutStreamServer) error {
for {
if tem, err := cliStr.Recv(); err == nil {
log.Println(tem)
} else {
log.Println("break, err :", err)
break
}
}
return nil
}
//客户端服务端 双向流
func(s *server) AllStream(allStr proto.Greeter_AllStreamServer) error {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
for {
data, _ := allStr.Recv()
log.Println("收到客户端的消息", data.Data)
}
wg.Done()
}()
go func() {
for {
allStr.Send(&proto.StreamResData{Data:"我是服务器"})
time.Sleep(time.Second)
}
wg.Done()
}()
wg.Wait()
return nil
}
func main(){
//监听端口
lis,err := net.Listen("tcp",PORT)
if err != nil{
panic(err)
return
}
//创建一个grpc 服务器
s := grpc.NewServer()
//注册事件
proto.RegisterGreeterServer(s,&server{})
//处理链接
err = s.Serve(lis)
if err != nil {
panic(err)
}
}
需要注意的改动
我文中都是基于protoc 3.11构建的。如果是最新那么,server端定义空struct需要嵌入UnimplementedXXX
hello.UnimplementedTestServer
metadata
1. 新建metadata
type MD map[string][]string
创建的时候可以像创建普通的map类型一样使用new关键字进行创建:
//第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二种方式 key不区分大小写,会被统一转成小写。
md := metadata.Pairs(
"key1", "val1",
"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
"key2", "val2",
)
2. 发送metadata
md := metadata.Pairs("key", "val")
// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)
3. 接收metadata
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
md, ok := metadata.FromIncomingContext(ctx)
// do something with metadata
}
4. grpc中使用metadata
待补充
interceptor 拦截器
proto
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
//将 sessionid放入 放入cookie中 http协议
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
client
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"time"
"start/grpc_interceptor/proto"
)
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
return err
}
func main(){
//stream
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
// 指定客户端interceptor
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial("localhost:50051", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name:"bobby"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
server
package main
import (
"context"
"fmt"
"net"
"google.golang.org/grpc"
"start/grpc_interceptor/proto"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply,
error){
return &proto.HelloReply{
Message: "hello "+request.Name,
}, nil
}
func main(){
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// 继续处理请求
fmt.Println("接收到新请求")
res, err := handler(ctx, req)
fmt.Println("请求处理完成")
return res, err
}
var opts []grpc.ServerOption
opts = append(opts, grpc.UnaryInterceptor(interceptor))
g := grpc.NewServer(opts...)
proto.RegisterGreeterServer(g, &Server{})
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil{
panic("failed to listen:"+err.Error())
}
err = g.Serve(lis)
if err != nil{
panic("failed to start grpc:"+err.Error())
}
}
状态码
文档
snap-code
健康检查
- GRPC 作为 RPC 服务,跟普通的 RPC 服务类似,一个 health check API 来检测是否可以正常返回。类似:
ping => pong
; - 它可以做的更丰富,比如检测每个服务(rpc service)的健康状态;
- 作为 GRPC 服务,它可以重用现有的账单计费,配合等基础架构等。因为服务器可以完全控制运行状态以及检测服务的访问;
- 搭配诸如consul等,注册grpc的健康检查来检测服务运行是否正常。 .proto
syntax = "proto3";
package grpc.health.v1;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
在grpc-go中的使用
healthcheck := health.NewServer()
healthpb.RegisterHealthServer(s, healthcheck)
服务注册 & 负载均衡
主要是实现grpc的resolver 和 balance 接口。