golang 基于grpc的插件框架——go-plugin 使用入门
说说我对插件的理解
大家都用过vscode
,当我们想要在vscode中格式化json的时候,很简单,去插件市场安装一个json tools
就好了;想要使用eclipse
的键盘快捷方式,安装一个eclipse keymap
就可以.
由此可见,插件帮助我们扩展原有程序的功能,同时它与原有工程是解耦的,可以独立开发。
总结下:
- 插件架构的功能
- 为系统提供扩展能力
- 不侵入系统现有功能
- 插件的好处
- Host与插件的代码解耦,独立开发
- Host 只关注插件的接口,不关注实现细节
- Host动态引入插件,因而可以自由定制所需的能力,避免部署包的体积过大
- 插件可以独立升级
一些常见的插件架构设计思路
- 共享库方式。
- 优点:动态编译,发布件小
- 缺点:
a. 共享库,决定了发布件必然是动态编译构建的,跨平台能力会比较弱。比如你要开发个https下载功能,要依赖
libssl
吧,而libssl
又依赖glibc
,于是这个依赖链就产生了各种版本上的强依赖。 b. 不安全。共享库方式调用插件代码,相当于把共享库的代码附加到当前进程,其函数可以直接访问当前主系统进程的内存空间,不仅不安全,而且如果插件代码质量低,可能导致主系统直接崩溃 c. 共享库的日志输出到主系统很麻烦 d. 如果用c语言开发还好,要是换个编程语言,还要涉及到数据类型的转换,恶心。。。
- 基于轻量通信协议的方式
- 优点:能解决以上所有问题
go-plugin
接下来我要介绍下github.com/hashicorp/go-plugin
,go-plugin使用grpc
协议来完成插件与主平台的接口调用. (不熟悉GRPC
的话,后文阅读起来可能比较难懂)
UML图
请对照UML图浏览后续内容,更有助于理解
讲解
proto
我们基于一个非常简单的protobuf来实现插件与HOST的通信
//protoc -I proto/ proto/print.proto --go_out=plugins=grpc:proto/ --go_out=. --go_opt=paths=source_relative
syntax = "proto3";
option go_package = "github.com/sxy/try-go-plugin/proto";
package proto;
message Empty {}
service HelloPlugin {
// Sends a greeting
rpc Hello (Request) returns (Response) {}
}
// 对应uml中的request
message Request{
string name = 1;
}
// 对应uml的response
message Response{
string result = 1;
}
定义IHelloService 接口
Host将插件实例化后的对象识别为接口——IHelloService接口,我们简单定义一个IHelloService
type IHelloService interface {
Hello(name string) (string, error)
}
插件中会完成对IHelloService
的实现——HelloService
type PluginService struct{}
func (p PluginService) Hello(name string) (string, error) {
return "hello " + name, nil
}
(重要) 定义GRPCPlugin
插件要里通过go-plugin框架来注册一个 GRPCPlugin
实例, Host 会利用go-plugin
的框架,来导入GRPCPlugin
实例
GRPCPlugin接口时go-plugin
提供的grpc 插件标准接口,只有两个成员函数
// GRPCServer 负责注册一个grpc server,本例中就是实现proto.HelloPluginServer的struct实例.
GRPCServer(*GRPCBroker, *grpc.Server) error
// GRPCClient 要返回一个实现了IHelloService接口的struct实例, 同时利用本方法传入的grpcConnection实例来调用proto.NewHelloPluginClient生成grpc通信所需的客户端实例, 这样它就能作为IHelloService接口方法与HelloPlugin接口(GRPC生成代码)的适配层.
GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
看代码就一目了然了.
// GRPCHelloPlugin implement plugin.GRPCPlugin
type GRPCHelloPlugin struct {
plugin.Plugin
Impl IHelloService
}
// 注册HelloPluginServer
func (p GRPCHelloPlugin) GRPCServer(broker *plugin.GRPCBroker, server *grpc.Server) error {
proto.RegisterHelloPluginServer(server, GPRCHelloPluginServerWrapper{impl: p.Impl})
return nil
}
// Host去获取插件的实例时,就掉用这个方法,将HelloPluginClient 作为 GRPCHelloPluginClientWrapper 的成员并返回GRPCHelloPluginClientWrapper
// 同时GRPCHelloPluginClientWrapper 也实现了IHelloService
func (p GRPCHelloPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, conn *grpc.ClientConn) (interface{}, error) {
return GRPCHelloPluginClientWrapper{client: proto.NewHelloPluginClient(conn)}, nil
}
type GPRCHelloPluginServerWrapper struct {
impl IHelloService
proto.UnimplementedHelloPluginServer
}
func (_this GPRCHelloPluginServerWrapper) Hello(ctx context.Context, request *proto.Request) (*proto.Response, error) {
r, _ := _this.impl.Hello(request.Name)
return &proto.Response{
Result: r,
}, nil
}
// GRPCHelloPluginClientWrapper 作为server 调用插件接口的包装器,
type GRPCHelloPluginClientWrapper struct {
client proto.HelloPluginClient
}
func (_this GRPCHelloPluginClientWrapper) Hello(name string) (string, error) {
in := proto.Request{Name: name}
resp, err := _this.client.Hello(context.Background(), &in)
if err != nil {
return "", err
} else {
return resp.Result, nil
}
}
插件 main 启动grpc并注册自己的服务
func main() {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: shared.Handshake,
Plugins: map[string]plugin.Plugin{
"PrintPlugin": &shared.GRPCHelloPlugin{Impl: &PluginService{}},
},
// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
}
Host 调用插件
func main() {
log.SetOutput(os.Stdout)
pluginClientConfig := &plugin.ClientConfig{
HandshakeConfig: shared.Handshake,
// helloPlugin.exe 是我们编译插件得到的可执行文件
Cmd: exec.Command("./helloPlugin.exe"),
// Host 只使用 GRPCHelloPlugin 的 GRPCClient 方法,无需使用任何GRPCHelloPlugin内部成员
Plugins: map[string]plugin.Plugin{"main": &shared.GRPCHelloPlugin{}},
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
}
client := plugin.NewClient(pluginClientConfig)
pluginClientConfig.Reattach = client.ReattachConfig()
protocol, err := client.Client()
if err != nil {
log.Fatalln(err)
}
// 实例化,此处实例化得到的其实就是 GRPCHelloPluginClientWrapper
raw, err := protocol.Dispense("main")
if err != nil {
log.Fatalln(err)
}
// 类型断言为IHelloService接口, 也可以用反射调用函数
service := raw.(shared.IHelloService)
res, err := service.Hello("sxy")
if err != nil {
log.Fatalln(err)
}
log.Println(res)
}
最终效果
编译插件
go build -o helloPlugin.exe plugin/plugin.go
运行server
go build -o server.exe server/server.go