go语言中probuf与客户端的交互

一、环境配置

  • 1、下载对应版本的安装包

  • 2、使用命令go env查看自己的go path地址并且在go path的目录下创建一个bin文件夹

  • 3、将第一点下载下来的东西解压出来将bin下面的protoc拷贝到go path下的bin文件夹下

  • 4、查看安装是否成功

    # 查看版本
    protoc --version
    # 查看常见命令
    protoc
    

    目前只支持这几种语言

    go语言中probuf与客户端的交互

  • 5、配置支持go语言,依赖包的地址

    go get -u github.com/golang/protobuf/proto
    go get -u github.com/golang/protobuf/protoc-gen-go
    

二、测试probufgo代码

  • 1、书写简单的proto文件

    //  helloWorld.proto
    
    // 版本,直接固定写3就好了
    syntax = "proto3";
    // 转换为go的包名,前面的.表示在当前目录下输出
    option go_package = ".;profile";
    
    message HelloWorld {
      string name = 1;
      int64 age = 2;
      double fee = 3;
      bool isLogin = 4;
    }
    
  • 2、运行命令将proto文件转go文件

    # protoc -I=.[当前目录] --go_out=plugins=grpc:.[当前目录] 文件名
    protoc -I=. --go_out=plugins=grpc:. helloWorld.proto
    

三、测试

  • 1、直接输出

    package main
    
    import (
    	"fmt"
    	"go_demo/01.protobuf/proto"
    )
    
    func main() {
    	hello := proto.HelloWorld{
    		Name:    "张三",
    		Age:     14,
    		Fee:     20.9,
    		IsLogin: false,
    	}
    	fmt.Println(&hello)
    }
    

四、其它的数据类型

  • 1、复合数据类型,其实就是数据的嵌套

    ....
    message Location {
      double latitude = 1;
      double longitude = 2;
    }
    message HelloWorld {
      string name = 1;
      // 注意这个地方与位置无关,只与后面的数字有关系
      Location location = 5;
      int64 age = 2;
      double fee = 3;
      bool isLogin = 4;
    }
    
  • 2、列表数据类型

    message Location {
      double latitude = 1;
      double longitude = 2;
    }
    message HelloWorld {
      string name = 1;
      Location location = 5;
      int64 age = 2;
      double fee = 3;
      bool isLogin = 4;
      repeated Location locationList = 6;
    }
    
  • 3、枚举类型

    // 定义枚举类型
    enum HelloStatus {
      NO_PAY = 0;
      PAY = 1;
    }
    message HelloWorld {
      string name = 1;
      Location location = 5;
      int64 age = 2;
      double fee = 3;
      bool isLogin = 4;
      repeated Location locationList = 6;
      HelloStatus status = 7;
    }
    
  • 4、赋值操作

    func main() {
    	var hello = proto.HelloWorld{
    		Name:    "张三",
    		Age:     14,
    		Fee:     20.9,
    		IsLogin: false,
    		Location: &proto.Location{
    			Latitude:  30,
    			Longitude: 100,
    		},
    		LocationList: []*proto.Location{
    			{
    				Latitude:  100,
    				Longitude: 10,
    			},
    		},
        Status: profile.HelloStatus_NO_PAY,
    	}
    	fmt.Println(&hello)
    }
    

五、其它的输出

  • 1、使用proto.Marshal输出二进制文件

    var hell1 profile.HelloWord
    err = proto.Unmarshal(b, &hell1)
    fmt.Println(&hello)
    
  • 2、输出json

    var hell1 profile.HelloWord
    b, err = json.Marshal(&hell1)
    fmt.Printf("%s\n", b)
    

六、实现客户端和服务器端数据交互

  • 1、定义一个proto文件

    // 版本,直接固定写3就好了
    syntax = "proto3";
    // 转换为go的包名,前面的.表示在当前目录下输出
    option go_package = ".;profile";
    
    message Location {
      double latitude = 1;
      double longitude = 2;
    }
    message HelloWorld {
      string name = 1;
      Location location = 5;
      int64 age = 2;
      double fee = 3;
      bool isLogin = 4;
      repeated Location locationList = 6;
    }
    
    message GetHelloRequest {
      string id = 1;
    }
    
    message GetHelloResponse {
      string id = 1;
      HelloWorld helloWorld = 2;
    }
    // 这个服务是必须定义的
    service HelloService {
      rpc GetHello(GetHelloRequest) returns (GetHelloResponse);
    }
    
  • 2、运行命令生成go的文件,并且查看自己定义的service

    func (c *helloServiceClient) GetHello(ctx context.Context, in *GetHelloRequest, opts ...grpc.CallOption) (*GetHelloResponse, error) {
    	out := new(GetHelloResponse)
    	err := c.cc.Invoke(ctx, "/HelloService/GetHello", in, out, opts...)
    	if err != nil {
    		return nil, err
    	}
    	return out, nil
    }
    
  • 3、定义服务器端的方法

    type Service struct {
    }
    
    func (*Service) GetHello(ctx context.Context, req *profile.GetHelloRequest) (*profile.GetHelloResponse, error) {
    	return &profile.GetHelloResponse{
    		Id: req.Id,
    		HelloWorld: &profile.HelloWorld{
    			Name:    "张三",
    			Age:     20,
    			Fee:     100,
    			IsLogin: true,
    			Location: &profile.Location{
    				Longitude: 100,
    				Latitude:  10,
    			},
    			LocationList: []*profile.Location{
    				{
    					Longitude: 10,
    					Latitude:  100,
    				},
    			},
    		},
    	}, nil
    }
    
  • 4、启动服务器端的方法

    func main() {
    	listen, err := net.Listen("tcp", ":9000")
    	if err != nil {
    		log.Fatalf("服务器启动失败:%v", err)
    	}
    	// 启动服务
    	service :=grpc.NewServer()
      // Service是上面自定义的
    	profile.RegisterHelloServiceServer(service, &Service{})
    	log.Fatal(service.Serve(listen))
    }
    
  • 5、客户端的启动

    package main
    
    import (
    	"context"
    	"fmt"
    	"go_demo/01.protobuf/profile"
    	"google.golang.org/grpc"
    	"log"
    )
    
    func main() {
    	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
    	if err != nil {
    		log.Fatalf("连接服务器端错误: %v", err)
    	}
    
    	client := profile.NewHelloServiceClient(conn)
    	response, err := client.GetHello(context.Background(), &profile.GetHelloRequest{
    		Id: "1001",
    	})
    	if err != nil {
    		log.Fatalf("连接服务错误:%v", err)
    	}
    	fmt.Println(response)
    }
    

七、将protobuf转换为http请求

  • 1、改造之前的proto文件

    syntax = "proto3";
    // 添加这个
    package collect;
    option go_package = ".;profile";
    
  • 2、在同proto目录下创建一个yaml文件

    # helloWorld.yaml文件
    type: google.api.Service
    config_version: 3
    
    http:
      rules:
        - selector: collect.HelloService.GetHello
          get: /hello/{id}
    
  • 3、安装依赖包

    go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
    
  • 4、修改运行命令

    # 和之前的命令是一样的
    protoc -I=. --go_out=plugins=grpc,paths=source_relative:. helloWorld.proto
    # 会生成一个helloWorld.pb.gw.go的文件
    protoc -I=. --grpc-gateway_out=paths=source_relative,grpc_api_configuration=helloWorld.yaml:. helloWorld.proto
    
  • 5、由于命令太多,不方便记住,在目录下创建一个gen.sh的文件,将两个命令放进去,需要运行的时候执行

    sh ./gen.sh
    
  • 6、grpc的服务层和上面的一样的

  • 7、定义http客户端

    package main
    
    import (
    	"context"
    	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    	"go_demo/01.protobuf/profile"
    	"google.golang.org/grpc"
    	"log"
    	"net/http"
    )
    
    func main() {
    	c := context.Background()
    	c, cancel := context.WithCancel(c)
    	defer cancel()
    
    	mux := runtime.NewServeMux()
    	err := profile.RegisterHelloServiceHandlerFromEndpoint(
    		c,
    		mux,
    		"localhost:9000", // grpc服务器端地址
    		[]grpc.DialOption{grpc.WithInsecure()},
    	)
    	if err != nil {
    		log.Fatalf("连接grpc错误:%v", err)
    	}
    	err = http.ListenAndServe(":8080", mux)
    	if err != nil {
    		log.Fatalf("监听错误:%v", err)
    	}
    }
    
  • 8、直接在浏览器上输入http://localhost:8080/hello/123

    go语言中probuf与客户端的交互

八、处理后端返回前端的数据结构

上面面临着两个问题

  • 1、年龄字段我们本来定义的是整数,返回给前端的是字符类型
  • 2、状态字段我们应该返回枚举类型的值,不应该是字符串的

解决办法

  • 1、年龄字段通过修改数据类型将之前的int64改为现在的int32数据类型

  • 2、状态枚举类型通过http客户端的方式来修改

    ...
    var mux = runtime.NewServeMux(
      runtime.WithMarshalerOption(
        runtime.MIMEWildcard,
        &runtime.JSONPb{
          MarshalOptions: protojson.MarshalOptions{
            UseEnumNumbers: true, // 枚举字段的值使用数字
            UseProtoNames:  true,
          },
          UnmarshalOptions: protojson.UnmarshalOptions{
            DiscardUnknown: true, // 忽略 client 发送的不存在的 poroto 字段
          },
        },
      ),
    )
    ...
    
上一篇:gRPC之metadata入门案例【附完整源码】


下一篇:使用Golang搭建gRPC服务提供给.NetCore客户端调用