前言

熟悉这个博客的朋友都知道,我一般都不会写语言类的说明书,但这次由于网络资料参差不齐,中间走了许多弯路,特此进行一次记录

故事背景

公司的工业机器人需要做车端行为树的汇总,系统组配合那边给出来一个 Grpc 接口(C++写),我们这边调用函数,车端进行充电等行为时候会触发行为树操作,而我们负责监听即可,本来是一个非常简单的需求。

项目准备

在这里推荐写代码还是使用 Linux 或者 Mac 更好,Windows 的配置实在是太折磨人了。

我们先来看看最终的,也就是实际跑起来的项目结构:

含有注释的都是重点文件

➜  tree
.
├── compatibility.go
├── config.go
├── debug
│   └── main.go # Server
├── go.mod
├── go.sum
├── grpc.go # 主要的接受监听部分代码,也可以看成 Client
├── grpc_test.go # 测试代码
├── main.go
├── mongo.go
├── xx.api.rfi # grpc 包名,要和调用方保持一致
│   ├── behavior_tree_engine_grpc.pb.go # 主要实现的方法
│   └── behavior_tree_engine.pb.go
├── mqtt.go
├── proto
│   └── behavior_tree_engine.proto # ProtoBuf 定义文件
└── README.md

3 directories, 14 files

在开始编写代码之前,我们先要安装对应的配套工具

  • protoc: 主要的生成代码工具
  • protoc-gen-go
  • protoc-gen-go-grpc

这里可以借用一下项目 README.md 的安装教程

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
$ apt install protoc

$ protoc --version
libprotoc 3.21.5

$  protoc-gen-go --version
protoc-gen-go v1.27.1

$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0

尽量保持大版本的一致,因为工具改动实在是太频繁了,这也是我从别人的博文不能一下子找到正确案例的重要因素

PROTO 文件

这个文件可以看成是伪代码,上面定义了函数的原型,例如我们这边的文件内容为 (供参考)

syntax = "proto3";

option go_package = "xx.api.rfi"; 

import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";


/// 行为树执行请求
message BehaviorTreeExecuteRequest {
  /// 行为树执行id
  string tree_id = 1;
  /// 行为树原始定义
  string definitions = 2;
  /// 相关上下文
  string context = 3;
  /// 预设的黑板数据
  google.protobuf.Struct black_board = 4;
}

/// 行为树逻辑结构图
message BehaviorTreeTopologicalDiagram {
  /// 行为树id
  string tree_id = 1;
  /// 行为树原始定义
  string definitions = 2;
  /// 上下文
  string context = 3;
  /// 根节点id
  uint32 root_node_uid = 4;
  /// 行为树节点列表
  map<uint32, BehaviorTreeNode> nodes = 5;
}

/// 行为树节点
message BehaviorTreeNode {
  /// 行为树节点类型
  enum Type {
    /// 未知未定义
    UNDEFINED = 0;
    /// 活动节点
    ACTION = 1;
    /// 条件节点
    CONDITION = 2;
    /// 控制节点
    CONTROL = 3;
    /// 装饰节点
    DECORATOR = 4;
    /// 子树节点
    SUBTREE = 5;
  }

  /// 节点执行状态
  enum Status {
    /// 空闲
    IDLE = 0;
    /// 运行中
    RUNNING = 1;
    /// 已完成
    SUCCESS = 2;
    /// 已失败
    FAILURE = 3;
  }

  /// 节点id
  uint32 uid = 1;
  /// 字节点id列表
  repeated uint32 children_uid = 2;
  /// 节点类型
  Type type = 3;
  /// 执行状态
  Status status = 4;
  /// 节点注册名称
  string name = 5;
  /// 节点别名
  string alain = 6;
}

/// 机器人行为树执行引擎操作服务
service RoboticsBehaviorTreeEngine {
  /// 监听执行的新行为树的更新
  rpc OnTreeTopologicalDiagramUpdated(google.protobuf.Empty) returns(stream BehaviorTreeTopologicalDiagram) {}
}

接着我们就可以根据这个文件生成代码

protoc --proto_path=proto proto/*.proto --go_out=. --go-grpc_out=require_unimplemented_servers=false:.

注意这个 require_unimplemented_servers,它的意思是取消向前兼容的方案,本身这个初衷是非常好的,但问题是它会在 UnimplementedGreetServiceServer 也就是服务端引入一个叫 mustEmbededUnimplementedServiceServer 的私有函数,我们或许可以通过嵌入 UnsafeFooBarServiceServer 的方式引入这个私有函数(不然就要动生成的代码将其暴露),但我们这种新项目完全没有向前兼容的需求,如果不实现该接口,则无法正确进行 RegisterXXXService 的操作

这个完完全全就是新版本引入进来的坑,而如果不是误打误撞搜这个函数名,光凭 go grpc server gen 这种搜法可能还要折腾几个小时

生成后的代码与调用

生成后的代码存放在 option go_package = "xx.api.rfi"; 这个定义中的同名文件夹中,分别包含了具体实现和函数原型

这里由于篇幅所限,只简单看一下生成后代码截图,注意检查里面有没有包含 Register 字段或者你 PROTO 文件中的函数名

服务端

package main

import (
	"fmt"
	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc"
	"log"
	"net"
	pb "pointinotify/xx.api.rfi"
)

type ServerRegister struct {
}

func (s *ServerRegister) OnTreeTopologicalDiagramUpdated(e *empty.Empty, p pb.RoboticsBehaviorTreeEngine_OnTreeTopologicalDiagramUpdatedServer) error {
	err := p.Send(&pb.BehaviorTreeTopologicalDiagram{
		TreeId:      "1",
		Definitions: "1",
		Context:     "1",
		RootNodeUid: 0,
		Nodes:       nil,
	})
	if err != nil {
		log.Fatalln(err)
	}
	return nil
}

func main() {
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
		fmt.Printf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()
	pb.RegisterRoboticsBehaviorTreeEngineServer(s, &ServerRegister{})
	// 启动服务
	err = s.Serve(lis)
	if err != nil {
		fmt.Printf("failed to serve: %v", err)
		return
	}
}