集中整合更新旧的文章。包含 Grpc 和 Protobuf 两部分内容。
Protoc 安装配置 Grpc 和 Protobuf 的使用都需要依赖 protoc
这个二进制命令程序完成。 需要去 github 下载预编译好的二进制文件放到系统的 PATH 中。
GitHub Release 地址: https://github.com/protocolbuffers/protobuf/releases 找到自身系统的版本解压缩,并把 bin 目录设置在系统的 PATH 中。include 文件夹需要和 bin 文件夹放同一级目录中。
解决 GoLand 找不到 protoc 自带的 proto 文件,可以如图设置到 GoLand 中
图中 xxxx/.env/include
就是 protoc 下载下来之后自带的那个 include 文件夹。
Grpc
grpc 使用 protobuf 协议通讯,基于 http2。
参考官网: https://grpc.io/docs/languages/go/quickstart/
依赖配置 Go 语言 在项目中执行下面的命令安装插件。
1 2 $ 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
项目中的依赖添加:
1 $ go get -u google.golang.org/grpc
传统传输定义 参考本文:跳转
流式传输 proto 定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 service Greeter { rpc SendStream (stream SendStreamRequest) returns (SendStreamReply) {} rpc ReceiveStream (ReceiveStreamRequest) returns (stream ReceiveStreamReply) {} rpc Stream (stream StreamRequest) returns (stream StreamReply) {} } message StreamRequest { string message = 1 ; } message StreamReply { string echo = 1 ; } message ReceiveStreamRequest { string name = 1 ; } message ReceiveStreamReply { string message = 1 ; } message SendStreamRequest { string name = 1 ; } message SendStreamReply { string message = 1 ; }
使用 stream
声明请求参数或者返回值是个流。以下为各种流的服务端实现和客户端实现
请求流 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (s *server) SendStream(clientStream pb.Greeter_SendStreamServer) error { var msg string for { clientRequest, err := clientStream.Recv() if err != nil { if err == io.EOF { if err := clientStream.SendAndClose(&pb.SendStreamReply{Message: msg}); err != nil { return err } return nil } return err } msg += "你好: " + clientRequest.Name + " " } }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func SendStream (client proto.GreeterClient) { streamClient, err := client.SendStream(context.Background()) if err != nil { log.Println(err) return } names := []string {"张三" , "李四" , "王五" } for _, name := range names { if err := streamClient.Send(&proto.SendStreamRequest{Name: name}); err != nil { log.Println(err) return } log.Println("发送成功: " + name) time.Sleep(time.Second * 2 ) } resp, err := streamClient.CloseAndRecv() if err != nil { log.Println(err) return } log.Println(resp.Message) }
响应流 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (s *server) ReceiveStream(clientRequest *pb.ReceiveStreamRequest, serverRespStream pb.Greeter_ReceiveStreamServer) error { clientName := clientRequest.Name data := []string { "hello" , "你好" , } for _, word := range data { err := serverRespStream.Send(&pb.ReceiveStreamReply{Message: word + ":" + clientName}) if err != nil { return err } time.Sleep(time.Second * 2 ) } return nil }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func ReceiveStream (client proto.GreeterClient) { responseStream, err := client.ReceiveStream(context.Background(), &proto.ReceiveStreamRequest{Name: "张三" }) if err != nil { log.Println(err) return } for { resp, err := responseStream.Recv() if err != nil { if err.Error() == "EOF" { break } log.Println(err) return } log.Println(resp.Message) } }
双向流 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (s *server) Stream(stream pb.Greeter_StreamServer) error { for { clientRequest, err := stream.Recv() if err != nil { if err == io.EOF { break } return err } if err := stream.Send(&pb.StreamReply{Echo: "Echo: " + clientRequest.Message}); err != nil { return err } } return nil }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func Stream (client proto.GreeterClient) { stream, err := client.Stream(context.Background()) if err != nil { log.Println(err) return } if err := stream.Send(&proto.StreamRequest{Message: "张三" }); err != nil { log.Println(err) return } replay, err := stream.Recv() if err != nil { log.Println(err) return } log.Println(replay.Echo) if err := stream.Send(&proto.StreamRequest{Message: "李四" }); err != nil { log.Println(err) return } replay, err = stream.Recv() if err != nil { log.Println(err) return } log.Println(replay.Echo) }
元数据 Grpc 类似 HTTP 可已在 HEADER 中存放一些键值对。
键值的 key 不区分大小写,会自动全部转为小写。
无法使用 grpc- 开头的 key 名称,这是保留字段。
key 永远都是字符串,但是值可以是字符串也可以是二进制,二进制的值 key 需要以-bin 结尾。发送前会使用 base64 编码,收到后解码。
只有服务端可以发送 trailer 数据。
客户端收发元数据 发送元数据:设置属性到 ctx 上,传入调用的 grcp 函数即可。
1 2 3 4 5 6 7 8 9 10 ctx := context.Background() ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("k2" , "v2" )) ctx = metadata.AppendToOutgoingContext(ctx, "k1" , "v1" ) client.SayHello(ctx, &proto.HelloRequest{Name: "张三" }) client.SendStream(ctx)
接受数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var header, trailer metadata.MD resp, err := client.SayHello(ctx, &proto.HelloRequest{Name: "张三" }, grpc.Header(&header), grpc.Trailer(&trailer)) if err != nil { log.Println(err) return } log.Println(streamClient.Header()) log.Println(streamClient.Trailer())
服务端收发元数据 发送元数据
1 2 3 4 5 6 grpc.SendHeader(ctx, metadata.Pairs("k1" , "v1" )) grpc.SetTrailer(ctx, metadata.Pairs("k2" , "v2" )) stream.SendHeader(metadata.Pairs("k1" , "v1" )) stream.SetTrailer(metadata.Pairs("k2" , "v2" ))
接受元数据 1 2 3 4 header, err := metadata.FromOutgoingContext(ctx) header, err := metadata.FromIncomingContext(stream.Context())
拦截器(中间件) 社区中的开源拦截器: https://github.com/grpc-ecosystem/go-grpc-middleware
拦截器分为普通拦截器,作用于普通的 rpc 请求上。流拦截器,作用于流式请求上的数据过滤。 服务端和客户端的拦截器使用都是大同小异的。
拦截器分为:一元拦截器和流拦截器
函数签名示例 1 2 3 4 5 6 7 type UnaryServerInterceptor func (ctx context.Context, req interface {}, info *UnaryServerInfo, handler UnaryHandler) (resp interface {}, err error )type StreamServerInterceptor func (srv interface {}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error type UnaryClientInterceptor func (ctx context.Context, method string , req, reply interface {}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error type StreamClientInterceptor func (ctx context.Context, desc *StreamDesc, cc *ClientConn, method string , streamer Streamer, opts ...CallOption) (ClientStream, error )
拦截器使用 多个一元中间件使用 grpc.WithChainUnaryInterceptor
或者 grpc.ChainUnaryInterceptor
,该方式遵循洋葱模型。
1 2 3 4 5 6 7 8 9 10 11 conn, err := grpc.Dial("localhost:8080" , grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(myInterceptor), grpc.WithStreamInterceptor(myStreamInterceptor)) if err != nil { panic (err) } grpcServer := grpc.NewServer( grpc.UnaryInterceptor(myUnaryServerInterceptor), grpc.StreamInterceptor(myStreamServerInterceptor))
中间件函数示例 一元拦截器(客户端)
参数解释:
ctx
:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
req
:是请求的消息体,是一个 interface{}
类型的变量。该变量可以是任何类型,具体类型需要根据服务端定义的 protobuf 消息体结构而定。
info
:是一个 *UnaryServerInfo
类型的指针,包含了请求的一些元信息,如请求的方法名称、请求的元数据等。
handler
:是一个 UnaryHandler
类型的函数,是一元 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1 2 3 4 5 6 7 8 9 10 11 func myInterceptor (ctx context.Context, method string , req, reply interface {}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { if err := invoker(ctx, method, req, reply, cc, opts...); err != nil { return err } return nil }
一元拦截器(服务端)
参数解释:
ctx
:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
req
:是请求的消息体,是一个 interface{}
类型的变量。该变量可以是任何类型,具体类型需要根据服务端定义的 protobuf 消息体结构而定。
info
:是一个 *UnaryServerInfo
类型的指针,包含了请求的一些元信息,如请求的方法名称、请求的元数据等。
handler
:是一个 UnaryHandler
类型的函数,是一元 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1 2 3 4 5 6 7 8 9 func myUnaryServerInterceptor (ctx context.Context, req interface {}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface {}, err error ) { resp, err = handler(ctx, req) return resp, nil }
流处理拦截器(客户端)
客户端的流拦截器是重写 grpc.ClientStream
接口的方法。
参数解释:
ctx
:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
desc
:是一个 *StreamDesc
类型的指针,表示请求的流描述符。
cc
:是一个 *ClientConn
类型的指针,表示客户端的连接对象。
method
:是一个字符串类型的变量,表示调用的方法名称。
streamer
:是一个 ClientStream
类型的对象,表示客户端发送请求的流对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type wrapperClientStream struct { grpc.ClientStream } func newWrapperClientStream (s grpc.ClientStream) grpc.ClientStream { return &wrapperClientStream{s} } func (w *wrapperClientStream) SendMsg(m interface {}) error { log.Printf("发送消息: (Type: %T) at %v" , m, time.Now().Format(time.RFC3339)) return w.ClientStream.SendMsg(m) } func (w *wrapperClientStream) RecvMsg(m interface {}) error { log.Printf("收到消息: (Type: %T) at %v" , m, time.Now().Format(time.RFC3339)) return w.ClientStream.RecvMsg(m) } func myStreamInterceptor (ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string , streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error ) { s, err := streamer(ctx, desc, cc, method, opts...) if err != nil { return nil , err } return newWrapperClientStream(s), nil }
流处理拦截器(服务端)
服务端的流拦截器是重写 grpc.ClientStream
接口的方法。
参数解释:
srv
:是一个 interface{}
类型的变量,表示实现服务端接口的对象。
stream
:是一个 ServerStream
类型的对象,表示服务端接收到的请求的流对象。
info
:是一个 *StreamServerInfo
类型的指针,表示请求的流信息。
handler
:是一个 StreamHandler
类型的函数,是流 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type wrapperServerStream struct { grpc.ServerStream } func newWrapperClientStream (s grpc.ServerStream) grpc.ServerStream { return &wrapperServerStream{s} } func (w *wrapperServerStream) SendMsg(m interface {}) error { log.Printf("发送消息: (Type: %T) at %v" , m, time.Now().Format(time.RFC3339)) return w.ServerStream.SendMsg(m) } func (w *wrapperServerStream) RecvMsg(m interface {}) error { log.Printf("收到消息: (Type: %T) at %v" , m, time.Now().Format(time.RFC3339)) return w.ServerStream.RecvMsg(m) } func myStreamServerInterceptor (srv interface {}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if err := handler(srv, newWrapperClientStream(ss)); err != nil { return err } return nil }
Grpc Gateway 自动生成 HTTP 网关,调用 RPC 服务。 本章节内容不属于 grpc 的功能,该功能由其插件实现。这里简单介绍使用,具体参考插件文档。 插件地址: https://github.com/grpc-ecosystem/grpc-gateway
功能图如下:
可以看到插件的目的就是同时生成 http 服务端和 grpc 的客户端,做了一层请求的转换。
前置准备 使用前置要求:protoc 二进制命令的所在目录的同级存在一个 include 文件夹。且该文件夹内存在 protobuf 自带的 proto 文件和 google.api 的以下 proto 文。
google.api 的 proto 文件下载地址: https://github.com/googleapis/googleapis/tree/master/google/api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bin └── protoc include └── google ├── api │ ├── annotations.proto │ └── http.proto └── protobuf ├── any.proto ├── api.proto ├── compiler │ └── plugin.proto ├── descriptor.proto ├── duration.proto ├── empty.proto ├── field_mask.proto ├── source_context.proto ├── struct.proto ├── timestamp.proto ├── type.proto └── wrappers.proto
如果 proto 没有放到上述位置,可以在编译 proto 文件时使用 -I
参数指定搜索 proto 的路径。
根据插件文档要求安装二进制插件,其中 grpc 和 protobuf 的插件之前都安装过了,所以只需要安装下面的两个插件即可:
1 2 go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
修改 proto 文件 理论上只需要修改服务端的 proto 文件即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import "google/api/annotations.proto" ; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { post: "/say_hello" body: "*" }; } }
重新编译 proto 因为插件是作用于 服务端
的,所以服务端的 proto 编译指令需要增加插件的参数:
1 2 3 4 5 6 7 protoc --go_out=. \ --go-grpc_out=. \ --go_opt=paths=source_relative \ --go-grpc_opt=paths=source_relative \ --grpc-gateway_out=. \ --grpc-gateway_opt=paths=source_relative \ ./server/pb/*.proto
上述命令新增了 --grpc-gateway_out
和 --grpc-gateway_opt=paths
。意思和之前的 grpc 、protobuf 是相同的。
编译完成之后,会生成一个新的后缀为 .pb.gw.go
的文件。 如果新文件内引入的包有不存在的,在项目中执行 go mod tidy
补全依赖即可。
创建 HTTP 服务 修改服务端的启动部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 func main () { listen, err := net.Listen("tcp" , ":8080" ) if err != nil { panic (err) } grpcServer := grpc.NewServer( grpc.UnaryInterceptor(myUnaryServerInterceptor), grpc.StreamInterceptor(myStreamServerInterceptor)) pb.RegisterGreeterServer(grpcServer, &server{}) go func () { fmt.Println("grpc server on 8080" ) err = grpcServer.Serve(listen) if err != nil { panic (err) } }() conn, err := grpc.DialContext(context.Background(), "localhost:8080" , grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { fmt.Println(err) return } if err := pb.RegisterGreeterHandler(context.Background(), gwmux, conn); err != nil { fmt.Println(err.Error()) return } gwServer := &http.Server{ Addr: ":8090" , Handler: gwmux, } if err := gwServer.ListenAndServe(); err != nil { fmt.Println(err.Error()) return } }
启动之后,尝试访问设置的地址验证。
Protobuf 是一种 IDL(Interface description language) 语言。
protobuf 的消息必须指定一个编号,且编号不能重复,其中 19000-19999 不能使用为保留字段。 1-15 占用一个字节,所以经常使用的字段使用 1-15。最好预留下。 编号不需要按顺序设置,也不需要从 1 开始编号。
编号一经分配强烈不建议在进行修改,字段删除后一定要设置为保留字段。
消息的定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 syntax = "proto3" ; option go_package = "server/pb" ; package pb;message people { string name = 1 ; int32 age = 2 ; } message human { repeated people peoples = 1 ; }
syntax = "proto3";
:该行必须为文件的第一行且之前不允许出现空格。表示声明协议文件的语法为 proto3 版本,默认为 2 版本。
option go_package = "server/pb";
:go 语言特有,指定了编译后源码存放的位置,相对 –go_out= 和 –go-grpc_out= 来说。
package pb;
:类似命名空间。分割相同名称的消息或者服务名称
message
:定义消息
支持的数据类型 各个类型和各个语言的对应关系参考官网:Scalar Value Types
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int32 number = 1 ;int64 number = 1 ;uint32 number = 1 ;uint64 number = 1 ;float number = 1 ;double number = 1 ;string str = 1 ; bytes data = 1 ; map<string , string > data = 3 ; repeated string strSlice = 1 ; repeated int32 intSlice = 1 ; repeated map<string , string > mapSlice = 3 ; message people { string name = 1 ; int32 age = 2 ; } message human { repeated people peoples = 1 ; }
服务的定义 1 2 3 4 service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc Add (AddRequest) returns (AddReply) {} }
service
:定义服务
rpc
:定义函数,函数的参数和返回值必须有且必须是 protobuf 中的消息
保留字段 当我们修改了 proto 文件,比如删除了某个字段,那么我们可以重新使用该字段的编号,如果其他不知情的人使用了旧版本的 proto 文件,就可能造成数据损坏或者数据泄露。所以为了防止这种情况发生,我们需要保留该编号让其他人不能在使用。
使用 reserved
关键字来指定,下面为关键字的用法。
1 2 3 4 message Foo { reserved 2 , 15 , 9 to 11 ; reserved "foo" , "bar" ; }
oneof 定义一个消息内的字段,多个字段只有最后一个设置的字段可以生效,之前其他字段设置的值会被删除。简而言之就是多个字段只能赋值一个字段。
1 2 3 4 5 6 message SampleMessage { oneof test_oneof { string name = 4 ; SubMessage sub_message = 9 ; } }
枚举
必须有一个零值,这样我们就可以使用0作为数值默认值。
零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 message SearchRequest { string query = 1 ; int32 page_number = 2 ; int32 result_per_page = 3 ; enum Corpus { UNIVERSAL = 0 ; WEB = 1 ; IMAGES = 2 ; LOCAL = 3 ; NEWS = 4 ; PRODUCTS = 5 ; VIDEO = 6 ; } Corpus corpus = 4 ; }
编译指令 1 protoc --go_out=. --go_opt=paths=source_relative ./xxx/xxx.proto
--go_out=.
:指定生成的源码文件存放位置,一个点表示当前执行 protoc 命令所在的位置。
--go_opt=paths=source_relative
: 使用相对位置
-I /xxx
:指定搜索 proto 文件的文件夹路径,比如指定 google 的 proto 文件所在文件夹的位置。
导入其他 proto proto 文件支持拆分和导入
使用 import
关键字带上其他 proto 文件的路径即可。 路径基于 --proto_path
的值,为相对路径。如果没有指定该参数应当参考 go_package
中指定的值。
1 2 3 import "book/book.proto"
生成到不同目录 a.proto 文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 syntax = "proto3" ; option go_package="demo/pb;pb" ; import "b.proto" ; message MsgA { int32 a =1 ; } service TA { rpc UpdateA(MsgA) returns (MsgB) ; } service TB { rpc UpdateA(MsgA) returns (MsgB) ; }
b.proto 文件内容
1 2 3 4 5 6 7 syntax = "proto3" ; option go_package="demo/pb2;pb2" ; message MsgB { int32 a = 1 ; }
文件结构如下:
1 2 3 4 5 6 7 8 9 10 demo ├── a │ └── a.proto ├── b │ └── b.proto ├── pb │ ├── a.pb.go │ └── a_grpc.pb.go └── pb2 └── b.pb.go
目标是对两个 proto 文件生成到 go 的两个包中。a 依赖 b,以下是生成命令,执行前手动建立 pb 文件夹和 pb 2 文件夹。
1 2 3 4 5 protoc -I=./a -I=./b --go_out=./pb --go-grpc_out=./pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./a/a.proto protoc -I=./a -I=./b --go_out=./pb2 --go-grpc_out=./pb2 --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./b/b.proto
参数注释:
-I
:指定搜索 proto 文件的路径,这里写到目录即可,被编译的 proto 文件 import 路径可以直接写文件名,如果还有子文件夹则需要带上子文件夹的名称。本参数可以同时指定多个。
-go_out
:指定编译后的 protobuf 消息 pb 文件的保存位置。
--go-grpc_out
:指定编译后的 grpc 服务 pb 文件的保存位置。
--go_opt=paths=source_relative
:让 protoc 自动识别 go_package 参数分号前的配置,现在你可以吧分号前的路径,从你项目根目录开始写了。此参数针对 protobuf 消息 pb 文件生效。
--go-grpc_opt=paths=source_relative
:同上,不过此参数针对 grpc 服务的 pb 文件生效。
末尾跟上你要编译的 proto 文件路径。
如果想生成到同一个包: go_package 写成一样即可。
序列化和反序列化 在项目中添加依赖:
1 go get -u github.com/golang/protobuf/proto
序列化
1 2 3 4 5 6 7 8 func main () { pbData := new (pb文件中的结构体) msgbyte, err := proto.Marshal(pbData) if err != nil { log.Println("序列化错误消息失败:" + err.Error()) return } }
反序列化
1 2 3 4 5 6 7 8 func main () { pbData := new (pb文件中的结构体) err := proto.Unmarshal(data, pbData) if err != nil { msg := "消息反序列化失败:" + err.Error() fmt.Println(msg) } }
Hello World 编写一个 HelloWorld 示例。
完整项目结构如下:
定义服务端 protobuf 定义一个简单的 proto 协议文件,该协议文件客户端和服务端共同使用,实际开发中可以把该文件单独放在一个 git 存储库中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 syntax = "proto3" ; option go_package = "server/pb" ; package pb; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1 ; } message HelloReply { string message = 1 ; }
编译 proto 协议文件 编译 proto 文件为 go 语言的文件。
本示例 protoc 命令需要在 server 和 client 文件夹的父级执行。
1 protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./server/pb/hello.proto
为了方便写成 Makefile 方便调用执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 build-all: build-client build-server build-client: protoc --go_out=. \ --go-grpc_out=. \ --go_opt=paths=source_relative \ --go-grpc_opt=paths=source_relative \ ./client/proto/*.proto build-server: protoc --go_out=. \ --go-grpc_out=. \ --go_opt=paths=source_relative \ --go-grpc_opt=paths=source_relative \ ./server/pb/*.proto
实现服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package main import ( "context" "google.golang.org/grpc" "net" "server/pb" ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(_ context.Context, request *pb.HelloRequest) (*pb.HelloReply, error ) { return &pb.HelloReply{ Message: "Hello" + request.Name, }, nil } func main () { listen, err := net.Listen("tcp" , ":8080" ) if err != nil { panic (err) } grpcServer := grpc.NewServer() pb.RegisterGreeterServer(grpcServer, &server{}) err = grpcServer.Serve(listen) if err != nil { panic (err) } }
定义客户端 proto 实际上客户端和服务端使用的是同一份 proto 文件。这里是为了模仿两个项目简单修改了 go_package
的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 syntax = "proto3" ; option go_package = "client/proto" ; package pb; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1 ; } message HelloReply { string message = 1 ; }
实现客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package main import ( "client/proto" "context" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "log" "time" ) func main () { conn, err := grpc.Dial("localhost:8080" , grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { panic (err) } defer func () { _ = conn.Close() }() client := proto.NewGreeterClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10 ) resp, err := client.SayHello(ctx, &proto.HelloRequest{Name: "张三" }) if err != nil { log.Println(err) return } log.Println(resp.Message) cancel() }