前言
熟悉这个博客的朋友都知道,我一般都不会写语言类的说明书,但这次由于网络资料参差不齐,中间走了许多弯路,特此进行一次记录
故事背景
公司的工业机器人需要做车端行为树的汇总,系统组配合那边给出来一个 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
}
}