Grcp&Protobuf
集中整合更新旧的文章。包含 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 语言
在项目中执行下面的命令安装插件。
$ 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
项目中的依赖添加:
$ go get -u google.golang.org/grpc
传统传输定义
参考本文:跳转
流式传输
proto 定义
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
声明请求参数或者返回值是个流。以下为各种流的服务端实现和客户端实现
请求流
服务端
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 + " "
}
}
客户端
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)
}
响应流
服务端
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
}
客户端
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)
}
}
双向流
服务端
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
}
客户端
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 函数即可。
ctx := context.Background()
// 创建新的元数据,同key会被覆盖
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("k2", "v2"))
// 附加元数据,同名的会被合并,不存在的会被创建
ctx = metadata.AppendToOutgoingContext(ctx, "k1", "v1")
// 普通方法
client.SayHello(ctx, &proto.HelloRequest{Name: "张三"})
// 流式传输
client.SendStream(ctx)
接受数据
// 普通方法
// 创建用户结束服务端发送的 header 和 trailer
// 直接以 grpc.CallOption 的方式绑定到请求上
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
}
// 流式传输接受
// 在流中获取 header 和 trailer
log.Println(streamClient.Header())
log.Println(streamClient.Trailer())
服务端收发元数据
发送元数据
// 普通方法
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"))
接受元数据
// 普通方法
header, err := metadata.FromOutgoingContext(ctx)
// 流式方法
header, err := metadata.FromIncomingContext(stream.Context())
拦截器(中间件)
社区中的开源拦截器: https://github.com/grpc-ecosystem/go-grpc-middleware
拦截器分为普通拦截器,作用于普通的 rpc 请求上。流拦截器,作用于流式请求上的数据过滤。 服务端和客户端的拦截器使用都是大同小异的。
拦截器分为:一元拦截器和流拦截器
函数签名示例
// 服务端
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
,该方式遵循洋葱模型。
// 客户端使用中间件
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
func myInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 预处理
// 调用 rpc 方法
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
func myUnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// 请求处理前
// 调用 rpc 方法
resp, err = handler(ctx, req)
// 请求处理后
return resp, nil
}
流处理拦截器(客户端)
客户端的流拦截器是重写 grpc.ClientStream
接口的方法。
参数解释:
ctx
:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。desc
:是一个*StreamDesc
类型的指针,表示请求的流描述符。cc
:是一个*ClientConn
类型的指针,表示客户端的连接对象。method
:是一个字符串类型的变量,表示调用的方法名称。streamer
:是一个ClientStream
类型的对象,表示客户端发送请求的流对象。
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
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
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 的插件之前都安装过了,所以只需要安装下面的两个插件即可:
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 文件即可。
// 导入 google 的 proto
import "google/api/annotations.proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
// 这里添加 google.api.http 注释
// 其中 post 表示请求方式
// post 后面的路径就是请求的路由
// body “*” 表示全部的body都转换到 grpc 中
option (google.api.http) = {
post: "/say_hello"
body: "*"
};
}
}
重新编译 proto
因为插件是作用于 服务端
的,所以服务端的 proto 编译指令需要增加插件的参数:
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 服务
修改服务端的启动部分代码
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)
}
}()
// 创建一个 grpc 的客户端连接
conn, err := grpc.DialContext(context.Background(), "localhost:8080",
// WithBlock: 设置客户端必须连接上服务端才会继续执行之后的代码
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
fmt.Println(err)
return
}
// runtime 使用的是 github.com/grpc-ecosystem/grpc-gateway/v2/runtime gwmux := runtime.NewServeMux()
// 注册
if err := pb.RegisterGreeterHandler(context.Background(), gwmux, conn); err != nil {
fmt.Println(err.Error())
return
}
// 创建服务
gwServer := &http.Server{
Addr: ":8090",
Handler: gwmux,
}
// 启动 http fmt.Println("http server on 8090")
if err := gwServer.ListenAndServe(); err != nil {
fmt.Println(err.Error())
return
}
}
启动之后,尝试访问设置的地址验证。
Protobuf
是一种 IDL(Interface description language) 语言。
protobuf 的消息必须指定一个编号,且编号不能重复,其中 19000-19999 不能使用为保留字段。 1-15 占用一个字节,所以经常使用的字段使用 1-15。最好预留下。 编号不需要按顺序设置,也不需要从 1 开始编号。
编号一经分配强烈不建议在进行修改,字段删除后一定要设置为保留字段。
消息的定义
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
// 整数, 类型对应go中同名类型
int32 number = 1;
int64 number = 1;
uint32 number = 1;
uint64 number = 1;
// 浮点数, float 对应 go float32, double 对应 float64
float number = 1;
double number = 1;
// 字符串
string str = 1; // 对应 string utf8
// 字节
bytes data = 1; // 对应 []byte
// 字典(map)
map<string, string> data = 3;
// 切片(列表, 数组...), 使用 repeated 关键字标识后面的类型是一个数组类型
repeated string strSlice = 1; // 对应 []string
repeated int32 intSlice = 1; // 对应 []int32
repeated map<string, string> mapSlice = 3; // 对应 []map[string]string
// 嵌套消息 对应 go 结构体嵌套
message people {
string name = 1;
int32 age = 2;
}
message human {
repeated people peoples = 1;
}
服务的定义
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc Add (AddRequest) returns (AddReply) {}
}
service
:定义服务rpc
:定义函数,函数的参数和返回值必须有且必须是 protobuf 中的消息
保留字段
当我们修改了 proto 文件,比如删除了某个字段,那么我们可以重新使用该字段的编号,如果其他不知情的人使用了旧版本的 proto 文件,就可能造成数据损坏或者数据泄露。所以为了防止这种情况发生,我们需要保留该编号让其他人不能在使用。
使用 reserved
关键字来指定,下面为关键字的用法。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
oneof
定义一个消息内的字段,多个字段只有最后一个设置的字段可以生效,之前其他字段设置的值会被删除。简而言之就是多个字段只能赋值一个字段。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
枚举
- 必须有一个零值,这样我们就可以使用0作为数值默认值。
- 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
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;
}
编译指令
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
中指定的值。
// pb 目录下存在 book 目录,且内部有两个 proto 文件,price 依赖 book
// protoc --proto_path=pb --go_out=. --go_opt=paths=source_relative book/price.proto book/book.proto
import "book/book.proto"
生成到不同目录
a.proto 文件内容
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 文件内容
syntax = "proto3";
option go_package="demo/pb2;pb2";
message MsgB {
int32 a = 1;
}
文件结构如下:
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 文件夹。
# a 文件操作
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
# b 文件操作
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 写成一样即可。
序列化和反序列化
在项目中添加依赖:
go get -u github.com/golang/protobuf/proto
序列化
func main() {
pbData := new(pb文件中的结构体)
msgbyte, err := proto.Marshal(pbData)
if err != nil {
log.Println("序列化错误消息失败:" + err.Error())
return
}
}
反序列化
func main() {
pbData := new(pb文件中的结构体)
err := proto.Unmarshal(data, pbData)
if err != nil {
msg := "消息反序列化失败:" + err.Error()
fmt.Println(msg)
}
}
Hello World
编写一个 HelloWorld 示例。
完整项目结构如下:
定义服务端 protobuf
定义一个简单的 proto 协议文件
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 文件夹的父级执行。
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./server/pb/hello.proto
为了方便写成 Makefile 方便调用执行
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
实现服务端
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
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;
}
实现客户端
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()
}