一、go-plugin 简介
1.1 go-plugin 是什么?
我们知道 Go 语言缺乏动态加载代码的机制,Go 程序通常是独立的二进制文件,因此难以实现类似于 C++ 的插件系统。即使 go 的最新标准引入了 go plugin 机制,但是由于限制性条件比较多导致在生产环境中不是很好用,比如插件的编写环境和插件的使用环境要保持一致,如 gopath、go sdk 版本等。
HashiCorp 公司开源的 go-plugin 库解决了上述问题,允许应用程序通过本地网络(本机)的 gRPC 调用插件,规避了 Go 无法动态加载代码的缺点。go-plugin 是一个通过 RPC 实现的 Go 插件系统,并在 Packer、Terraform, Nomad、Vault 等由 HashiCorp 主导的项目中均有应用。
顺便说一句,Vault 开源代码,我这几天看了下,代码写的很不错,感兴趣的小伙伴可以看看 vault 是怎么使用 go-plugin,很值得借鉴,后续会针对 vault 的源代码的插件部分进行剖析。
1.2 特性
go-plugin 的特性包括:
- 插件是 Go 接口的实现: 这让插件的编写、使用非常自然。对于插件编写者来说,他只需要实现一个 Go 接口即可;对于插件的用户来说,就像在同一个进程中使用和调用函数即可。go-plugin 会处理好本地调用转换为 gRPC 调用的所有细节
- 跨语言支持:插件可以被任何主流语言编写(和使用),该库支持通过 gRPC 提供服务插件,而基于 gRPC 的插件是允许被任何语言编写的。
- 支持复杂的参数、返回值: go-plugin 可以处理接口、io.Reader/Writer 等复杂类型,我们为您提供了一个库(MuxBroker),用于在客户端 / 服务器之间创建新连接,以服务于附加接口或传输原始数据。
- 双向通信: 为了支持复杂参数,宿主进程能够将接口实现发送给插件,插件也能够回调到宿主进程(这点还需要看官网的双向通信的例子好好理解下)
- 内置日志系统: 任何使用 log 标准库的的插件,都会自动将日志信息传回宿主机进程。宿主进程会镜像日志输出,并在这些日志前面加上插件二进制文件的路径。这会使插件的调试变简单。如果宿主机使用 hclog,日志数据将被结构化。如果插件同样使用 hclog,插件的日志会发往宿主机并被结构化。
- 协议版本化: 支持一个简单的协议版本化,可增加版本号使之前插件无效。当接口签名变化、协议版本改变等情况时,协议版本话是很有用的。当协议版本不兼容时,会发送错误消息给终端用户。
- 标准输出 / 错误同步: 插件以子进程的方式运行,这些插件可以自由的使用标准输出 / 错误,并且输出会被镜像回到宿主进程。
- TTY Preservation: 插件子进程可以链接到宿主进程的 stdin 标准输入文件描述符,允许以 TTY 方式运行的软件。
- 插件运行状态中,宿主进程升级: 插件可以 “reattached”,所以可以在插件运行状态中升级宿主机进程。NewClient 函数使用 ReattachConfig 选项来确定是否 Reattach 以及如何 Reattach。
- 加密通信: gRPC 信道可以加密
1.3 架构优势
- 插件不影响宿主机进程:插件崩溃了,不会导致宿主进程崩溃
- 插件容易编写:仅仅写个 go 应用程序并执行 go build。或者使用其他语言来编写 gRPC 服务 ,加上少量的模板来支持 go-plugin。
- 易于安装:只需要将插件放到宿主进程能够访问的目录即可,剩下的事情由宿主进程来处理。
- 完整性校验:支持对插件的二进制文件进行 Checksum
- 插件是相对安全的:插件只能访问传递给它的接口和参数,而不是进程的整个内存空间。另外,go-plugin 可以基于 TLS 和插件进行通信。
1.4 适用场景
go-plugin 目前仅设计为在本地[可靠]网络上工作,不支持 go-plugin 在真实网络,并可能会导致未知的行为。
即不能将 go-plugin 用于在两台服务器之间的远程过程调用,这点和传统的 RPC 有很大区别,望谨记。
二、核心数据结构
2.1 Plugin 接口
Plugin 是一个接口,是插件进程和宿主进程进行通信的桥梁。
不管是插件编写者还是插件使用者,都需要实现 plugin.Plugin 接口,只是各自的实现不同。
`type Plugin interface {
// Server should return the RPC server compatible struct to serve
// the methods that the Client calls over net/rpc.
Server(*MuxBroker) (interface{}, error)
// Client returns an interface implementation for the plugin you’re
// serving that communicates to the server end of the plugin.
Client(_MuxBroker, _rpc.Client) (interface{}, error)
}
`
Server 接口: Server 接口应返回与 RPC server 兼容的结构以提供方法,客户端可以通过 net/rpc 来调用此方法。
Client 接口: Client 接口返回你提供服务的插件的接口实现,该接口实现将与该插件的服务器端进行通信。
2.2 GRPCPlugin 接口
`type GRPCPlugin interface {
// 由于 gRPC 插件以单例方式服务,因此该方法仅调用一次
GRPCServer(_GRPCBroker, _grpc.Server) error
// 插件进程退出时,context 会被 go-plugin 关闭
GRPCClient(context.Context, _GRPCBroker, _grpc.ClientConn) (interface{}, error)
}
`
GRPCPlugin 的接口实现,在 grpc 的例子中我们再详细解释。
2.3 plugin.client 接口
这个接口负责管理一个插件进程的完整生命周期,包括创建插件进程、连接到插件进程、分配接口实现、处理杀死进程。
对于每个插件,宿主机进程需要创建一个 plugin.Client 实例。
`type Client struct {
// 插件客户端配置
config _ClientConfig
// 插件进程是否已经退出
exited bool
l sync.Mutex
// 插件进程的 RPC 监听地址
address net.Addr
// 插件进程对象
process _os.Process
// 协议客户端,宿主进程需要调用其 Dispense 方法来获得业务接口的 Stub
client ClientProtocol
// 通信协议
protocol Protocol
logger hclog.Logger
doneCtx context.Context
ctxCancel context.CancelFunc
negotiatedVersion int
// 用于管理 插件管理协程的生命周期
clientWaitGroup sync.WaitGroup
stderrWaitGroup sync.WaitGroup
// 测试用,标记进程是否被强杀
processKilled bool
}
`
2.4 ClientConfig 和 ServeConfig 对比
ClientConfig 包含了初始化一个插件客户端所需的配置信息,一旦初始化,则不可更改。
ServeConfig 包含了初始化一个插件服务器端所需的配置信息,一旦初始化,则不可更改。
枚举这两个结构体,对其字段进行对比和类比分析
`type ClientConfig struct {
// 握手信息,用于宿主、插件的匹配。如果不匹配,插件会拒绝连接
HandshakeConfig
// 可以消费的插件列表
Plugins PluginSet
// 版本化的插件列表,用于支持在客户端、服务器之间协商兼容版本
VersionedPlugins map[int]PluginSet
// 启动插件进程使用的命令行,不能和 Reattach 联用
Cmd *exec.Cmd
// 连接到既有插件进程的必要信息,不能和 Cmd 联用
Reattach *ReattachConfig
// 用于在启动插件时校验二进制文件的完整性
SecureConfig *SecureConfig
// 基于 TLS 进行 RPC 通信时需要的信息
TLSConfig *tls.Config
// 标识客户端是否应该被 plugin 包自动管理
// 如果为 true,则调用 CleanupClients 自动清理
// 否则用户需要负责杀掉插件客户端,默认 false
Managed bool
// 和子进程通信使用的端口范围,
MinPort, MaxPort uint
// 启动插件的超时
StartTimeout time.Duration
……
}
`
`
type ServeConfig struct {
// 和客户端匹配的握手配置,其信息必须和客户端匹配,否则会拒绝连接
HandshakeConfig
// 调用此函数得到 tls.Config
TLSProvider func() (*tls.Config, error)
// 可以提供服务的插件集
Plugins PluginSet
// 版本化的插件列表,用于支持在客户端、服务器之间协商兼容版本
VersionedPlugins map[int]PluginSet
// 如果通过 gRPC 提供服务,则此字段不能为空
// 调用此函数创建一个 gRPC 服务器对象
// 公司场景采用 grpc 通信,所以涉及到 grpc 的要重点看
GRPCServer func([]grpc.ServerOption) *grpc.Server
Logger hclog.Logger
}
`
ClientConfig 和 ServeConfig 中都需要填写 HandshakeConfig,这里要声明两点
对于 rpc 通信的 client 和 server 来说,其 ClientConfig 和 ServeConfig 中的配置要保持完全一致,否则会导致连接失败,这块在调试的时候,耗费了我一些时间和精力。
2.5 PluginSet 结构体
插件进程在启动时设置 Plugins,即 ServeConfig 中设置 Plugins 时,会指明其实现者;
宿主机进程在启动时也设置 Plugins,即 ClientConfig 中设置 Plugins 时,不需要指明其实现者。
`// 插件进程的插件集
var pluginMap = map[string]plugin.Plugin{
“greeter”: &example.GreeterPlugin{Impl: greeter},
}
// 宿主机进程的插件集
var pluginMap = map[string]plugin.Plugin{
“greeter”: &example.GreeterPlugin{},
}
`
如上图所示,其 ServeConfig 中插件业务接口实现者是 greeter
三、架构设计图
四、从 example 入手学习
4.1 basic 例子剖析
4.1.1 业务接口定义
// Greeter is the interface that we're exposing as a plugin. type Greeter interface {Greet() string }
暴露插件需要实现的接口,接口的实现是在插件进程中。
4.1.2 宿主机进程剖析
宿主机进程的代码如下:
`func main() {
// 创建 hclog.Logger 类型的日志对象
logger := hclog.New(&hclog.LoggerOptions{
Name: “plugin”,
Output: os.Stdout,
Level: hclog.Debug,
})
// 两种方式选其一
// 以 exec.Command 方式启动插件进程,并创建宿主机进程和插件进程的连接
// 或者使用 Reattach 连接到现有进程
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
// 创建新进程,或使用 Reattach 连接到现有进程中
Cmd: exec.Command(“./plugin/greeter”),
Logger: logger,
})
// 关闭 client,释放相关资源,终止插件子程序的运行
defer client.Kill()
// 返回协议客户端,如 rpc 客户端或 grpc 客户端,用于后续通信
rpcClient, err := client.Client()
if err != nil {
log.Fatal(err)
}
// 根据指定插件名称分配新实例
raw, err := rpcClient.Dispense(“greeter”)
if err != nil {
log.Fatal(err)
}
// 像调用普通函数一样调用接口函数就 ok,很方便是不是?
greeter := raw.(example.Greeter)
fmt.Println(greeter.Greet())
}
var pluginMap = map[string]plugin.Plugin{
// 插件名称到插件对象的映射关系
“greeter”: &example.GreeterPlugin{},
}
`
其流程一共拆解为 5 步:
第一步、plugin.NewClient 创建宿主机进程和插件进程之间的连接
plugin.NewClient 创建 plugin.Client,可以简单理解为宿主机进程和插件进程之间连接。其参数 pluginMap 表示可被消费的插件列表。
第二步、调用 client.Client(),返回当前连接的协议客户端(即 rpcClient)
协议支持 net/rpc 或 gRPC,所以协议客户端可能是 gRPC 客户端,也可能是标准 net/rpc 客户端。
第三步、调用 rpcClient.Dispense,根据指定插件名称分配一个新实例
由于此函数很关键,下面通过走读源代码来梳理下流程:
`func (c *RPCClient) Dispense(name string) (interface{}, error) {
//1、查找插件类型是否支持
p, ok := c.plugins[name]
if !ok {
return nil, fmt.Errorf(“unknown plugin type: %s”, name)
}
var id uint32
if err := c.control.Call(
“Dispenser.Dispense”, name, &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
//2、非常重要,Dispense 函数会回调 Plugin 的 Client 接口实现
return p.Client(c.broker, rpc.NewClient(conn))
}
`
在 Dispense 方法中会调用自己实现的插件的 Client 方法。
前面 2.1 章节提过,每个新插件都会实现 plugin.Plugin 接口(grpc 插件实现的是 GRPCPlugin 接口),即 Server 和 Client 接口
下面是 basic 例子中的 GreeterPlugin 插件实现
`type GreeterPlugin struct {
// 内嵌业务接口
// 插件进程会设置其为实现业务接口的对象
// 宿主进程则置空
Impl Greeter
}
// 此方法由插件进程延迟的调用
func (p _GreeterPlugin) Server(_plugin.MuxBroker) (interface{}, error) {
return &GreeterRPCServer{Impl: p.Impl}, nil
}
// 此方法由宿主进程调用
func (GreeterPlugin) Client(b _plugin.MuxBroker, c _rpc.Client) (interface{}, error) {
return &GreeterRPC{client: c}, nil
}
`
Server 方法必须返回一个这种插件类型的 RPC server 服务器,我们构造了 GreeterRPCServer。
Client 方法必须返回一个接口的实现,并且能够通过 RPC client 客户端通信,我们返回了 GreeterRPC
这里出个思考题
1、Server 方法为什么需要返回 GreeterRPCServer 的指针?
2、Client 方法为什么需要返回 GreeterRPC 的指针?
感兴趣的小伙伴可在评论区留言哈,看看你是否理解到本质呢?
综上所述,Dispense 的返回值是指向 GreeterRPC 的指针。
`type GreeterRPC struct{client *rpc.Client}
func (g *GreeterRPC) Greet() string {
var resp string
err := g.client.Call(“Plugin.Greet”, new(interface{}), &resp)
if err != nil {
// You usually want your interfaces to return errors. If they don’t,
// there isn’t much other choice here.
panic(err)
}
return resp
}
`
GreeterRPC 结构体实现了业务接口的 Greet(),在方法实现的函数体 body 中,实际是用 rpc client 客户端调用 Call() 来进行远程过程调用,并将响应返回,如出错则会导致 panic。
问题 3:g.client.Call(“Plugin.Greet”, new(interface{}), &resp) 中的第一个参数 “Plugin.Greet” 可以更换吗?
第四步、转换成业务接口类型,并调用对应 api
从第三步我们知道 Dispense 的返回值 raw 是指向 GreeterRPC 的指针。而 GreeterRPC 结构体实现了业务接口 example.Greeter。所以两者之间可以进行类型转换。
greeter := raw.(example.Greeter) fmt.Println(greeter.Greet())
上述两句代码,将 raw 转换为业务接口类型 example.Greeter,然后调用之前暴露的业务接口 Greet() 函数。
第五步、关闭 client,释放资源
调用 client.Kill() 函数,来释放之前申请的系统资源,防止内存泄露。
4.1.3 插件进程剖析
`// Here is a real implementation of Greeter
// 重点:业务接口的真正实现
type GreeterHello struct {
logger hclog.Logger
}
// 之前暴露的插件业务接口,此处必须实现,供宿主机进程 RPC 调用
func (g *GreeterHello) Greet() string {
g.logger.Debug(“message from GreeterHello.Greet”)
return “Hello!”
}
// 握手配置,插件进程和宿主机进程,都需要保持一致
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: “BASIC_PLUGIN”,
MagicCookieValue: “hello”,
}
func main() {
logger := hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: os.Stderr,
JSONFormat: true,
})
greeter := &GreeterHello{
logger: logger,
}
// pluginMap is the map of plugins we can dispense.
// 插件进程必须指定 Impl,此处赋值为 greeter 对象
var pluginMap = map[string]plugin.Plugin{
“greeter”: &example.GreeterPlugin{Impl: greeter},
}
logger.Debug(“message from plugin”, “foo”, “bar”)
// 调用 plugin.Serve() 启动侦听,并提供服务
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
})
}
`
第一步、定义 GreeterHello 结构体,并实现插件暴露的业务接口 Greet()
第二步、整理插件的映射关系,并在 plugin.Serve 函数调用时,以参数形式赋值给 Plugins
如名称为 greeter 的插件,对应 & example.GreeterPlugin{Impl: greeter}
var pluginMap = map[string]plugin.Plugin{ // 插件名称到插件对象的映射关系 "greeter": &example.GreeterPlugin{Impl: greeter}, }
第三步、在 main 函数中调用 plugin.Serve(),启动监听来提供插件服务。
服务器调用 plugin.Serve 方法后,主线程会阻塞。直到客户端调用 Dispense 方法请求插件实例时,服务器端才会实例化插件(业务接口的实现):
`func (d _dispenseServer) Dispense(
name string, response _uint32) error {
// 从 PluginSet 中查找
p, ok := d.plugins[name]
if !ok {
return fmt.Errorf(“unknown plugin type: %s”, name)
}
// 调用(下面的那个函数)插件接口的方法
impl, err := p.Server(d.broker)
if err != nil {
return errors.New(err.Error())
}
// MuxBroker 基于唯一性的 ID 进行 TCP 连接的多路复用
id := d.broker.NextId()
*response = id
// 在另外一个协程中处理该请求
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf(“[ERR] go-plugin: plugin dispense error: %s: %s”, name, err)
return
}
serve(conn, “Plugin”, impl)
}()
return nil
}
func (p _GreeterPlugin) Server(_plugin.MuxBroker) (interface{}, error) {
return &GreeterRPCServer{Impl: p.Impl}, nil
}
`
4.2 grpc 例子剖析
应当尽量采用 grpc 而非 net/rpc,原因如下:
1)gRPC 支持多种语言来实现插件,而 net/rpc 是 Go 专有的,不利于程序的可扩展性
2)在 gRPC 模式下,go-plugin 插件请求通过 http2 发送,传输性能更好
3)对于 gRPC 模式来说,插件进程只会有单个插件 “实例”。对于 net/rpc 你可能需要创建多个 “实例”。
使用 gRPC 模式下的 go-plugin,其步骤如下:
xx(未完成)
xx
xx
4.2.1 proto 定义
`syntax = “proto3”;
package proto;
// 请求
message GetRequest {
string key = 1;
}
// 应答
message GetResponse {
bytes value = 1;
}
message PutRequest {
string key = 1;
bytes value = 2;
}
message Empty {}
// 定义 service 的接口 Get 和 Put
service KV {
rpc Get(GetRequest) returns (GetResponse);
rpc Put(PutRequest) returns (Empty);
}
`
执行命令:protoc -I proto/ proto/kv.proto —go_out=plugins=grpc:proto/ 生成 Go 代码。
4.2.2 业务接口
在 examples/grpc/shared/interface.go 文件中,是其定义的业务接口
// 业务接口 type KV interface {Put(key string, value []byte) error Get(key string) ([]byte, error) }
4.2.3 插件接口
gRPC 模式下,你需要实现接口 plugin.GRPCPlugin,并嵌入 plugin.Plugin 接口:
问题 4:为什么要嵌入 plugin.Plugin 插件接口呢?以前只嵌入接口的实现就行(这点暂时还没解决)
type KVGRPCPlugin struct { // 需要嵌入插件接口 plugin.Plugin // 具体实现,仅当业务接口实现基于 Go 时该字段有用 Impl KV }
plugin.GRPCPlugin 接口的规格如下,你需要实现两个方法:
`type GRPCPlugin interface {
// 此方法被插件进程调用
// 你需要向其提供的 grpc.ServergRPC 参数,注册服务的实现(服务器端存根)
// 由于 gRPC 下服务器端是单例模式,因此该方法仅调用一次
GRPCServer(_GRPCBroker, _grpc.Server) error
// 此方法被宿主进程调用
// 你需要返回一个业务接口的实现(客户端存根),此实现直接将请求转给 gRPC 客户端即可
// 传入的 context 对象会在插件进程销毁时取消
GRPCClient(context.Context, _GRPCBroker, _grpc.ClientConn) (interface{}, error)
}
`
其实现如下:
`func (p _KVGRPCPlugin) GRPCServer(broker _plugin.GRPCBroker, s *grpc.Server) error {
// 向 grpc.ServergRPC 类型参数 s,注册服务的实现
proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl})
return nil
}
func (p _KVGRPCPlugin) GRPCClient(ctx context.Context, broker _plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
// 创建 gRPC 客户端的方法是自动生成的
return &GRPCClient{client: proto.NewKVClient(c)}, nil
}
`
备注信息:
KVPlugin 是对 plugin.Plugin 接口的实现
KVGRPCPlugin 是对 GRPCPlugin 接口的实现
GRPCClient 是对 KV 接口的实现
GRPCServer 是对 KVServer 接口的实现
1、GRPCServer 接口实现
`// 实现自动生成的 KVServer 接口,具体逻辑委托给业务接口 KV 的实现
type GRPCServer struct {
// This is the real implementation
Impl KV
}
func (m _GRPCServer) Put(
ctx context.Context,
req _proto.PutRequest) (*proto.Empty, error) {
return &proto.Empty{}, m.Impl.Put(req.Key, req.Value)
}
func (m _GRPCServer) Get(
ctx context.Context,
req _proto.GetRequest) (*proto.GetResponse, error) {
v, err := m.Impl.Get(req.Key)
return &proto.GetResponse{Value: v}, err
}
`
2、GRPCClient 接口实现
在 GRPCClient 方法的实现中,你需要返回一个业务接口的实现(客户端 stub),此实现只是将请求转发给 gRPC 服务处理:
`// 业务接口 KV
type KV interface {
Put(key string, value []byte) error
Get(key string) ([]byte, error)
}
// 业务接口 KV 的实现,通过 gRPC 客户端转发请求给插件进程
type GRPCClient struct{client proto.KVClient}
func (m *GRPCClient) Put(key string, value []byte) error {
_, err := m.client.Put(context.Background(), &proto.PutRequest{
Key: key,
Value: value,
})
return err
}
func (m *GRPCClient) Get(key string) ([]byte, error) {
resp, err := m.client.Get(context.Background(), &proto.GetRequest{
Key: key,
})
if err != nil {
return nil, err
}
return resp.Value, nil
}
`
4.2.4 宿主机进程
宿主机进程使用 gRPC 方式时,只需要设置 AllowedProtocols,指明同时支持 plugin.ProtocolNetRPC 和 plugin.ProtocolGRPC 两种协议。
`func main() {
// We don’t want to see the plugin logs.
log.SetOutput(ioutil.Discard)
// We’
re a host. Start by launching the plugin process.
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: shared.Handshake,
Plugins: shared.PluginMap,
Cmd: exec.Command(“sh”, “-c”, os.Getenv(“KV_PLUGIN”)),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
})
defer client.Kill()
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
fmt.Println(“Error:”, err.Error())
os.Exit(1)
}
// Request the plugin
raw, err := rpcClient.Dispense(“kv_grpc”)
if err != nil {
fmt.Println(“Error:”, err.Error())
os.Exit(1)
}
// We should have a KV store now! This feels like a normal interface
// implementation but is in fact over an RPC connection.
kv := raw.(shared.KV)
os.Args = os.Args[1:]
switch os.Args[0] {
case “get”:
result, err := kv.Get(os.Args[1])
if err != nil {
fmt.Println(“Error:”, err.Error())
os.Exit(1)
}
fmt.Println(string(result))
case “put”:
err := kv.Put(os.Args[1], []byte(os.Args[2]))
if err != nil {
fmt.Println(“Error:”, err.Error())
os.Exit(1)
}
default:
fmt.Printf(“Please only use’get’or’put’, given: %q”, os.Args[0])
os.Exit(1)
}
os.Exit(0)
}
`
4.2.5 插件进程
只需要指定 GRPCServer,提供创建 gRPC 服务器的函数,其他的和以前没什么区别。
`// Here is a real implementation of KV that writes to a local file with
// the key name and the contents are the value of the key.
type KV struct{}
func (KV) Put(key string, value []byte) error {
value = []byte(fmt.Sprintf(“%s\n\nWritten from plugin-go-grpc”, string(value)))
return ioutil.WriteFile(“kv_”+key, value, 0644)
}
func (KV) Get(key string) ([]byte, error) {
return ioutil.ReadFile(“kv_” + key)
}
func main() {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: shared.Handshake,
Plugins: map[string]plugin.Plugin{
“kv”: &shared.KVGRPCPlugin{Impl: &KV{}},
},
// A non-nil value here enables gRPC serving for this plugin…
GRPCServer: plugin.DefaultGRPCServer,
})
}
`
五、总结
5.1 宿主机进程和插件进程在 rpc 通信中扮演的角色?
扮演客户端的角色,插件进程扮演服务器的角色,因为插件进程在主函数的末尾会调用 Serve(opts *ServeConfig) 函数。
5.2 插件编写者和插件使用者如何来使用 go-plugin 库?
一般来说,步骤如下:
1、选择插件希望暴露的接口。
2、对于每个接口,实现该接口确保其通过 net/rpc 连接或 gRPC 连接可以通信,你必须同时实现客户端和服务器。
3、创建 Plugin 接口的实现,知道如何为给定的插件类型创建 RPC client/server。
4、插件编写者,在 main 函数中调用 plugin.Serve(),启动监听来提供插件服务。
5、插件使用者,使用 plugin.Client 启动子进程,通过 rpc 请求一个接口实现。
上述步骤有不妥当的地方,请及时反馈给我。
参考链接:https://blog.gmem.cc/go-plugin-over-grpc#comment-26043
https://mp.weixin.qq.com/s/N1BWGheV8ZIpO5L90vaZyg