上一篇帖子 go 微服务框架 go-micro 深度学习 (三) Registry 服务的注册和发现详 https://www.cnblogs.com/li-peng/p/9689786.html 细解释了 go-micro 是如何做服务注册和发现在, 服务端注册 server 信息, client 获取 server 的地址信息, 就可以和服务建立连接, 然后就可以进行通信了. 这篇帖子详细说一下, go-micro 的通信协议, 编码, 和具体服务方法的调用过程是如何实现的, 文中的代码还是我 GitHub 上的例子: https://github.com/lpxxn/gomicrorpc
go-micro 支持很多通信协议: http,tcp,grpc 等, 支持的编码方式也很多有 JSON,protobuf,bytes,jsonrpc 等. 也可以根据自己的需要实现通信协议和编码方式. go-micro 默认的通信协议是 http, 默认的编码方式是 protobuf, 我就以默认的方式来分解他的具体实现.
服务的启动
go-micro 在启动的时候会选择默认通信协议 http 和 protobuf 编码方式, 但他是如何路由到具体方法的? 在 go-micro 服务端启动的时候我们需要注册 Handler, 也就是我们具体实现结构体 , 如例子中注册方法时, 我们调用的 RegisterSayHandler 方法
// 注册 Handler
rpcapi.RegisterSayHandler(service.Server(), new(handler.Say))
这个方法内部的体实现主要是利用了反射的力量, 注册的对象是实现了 rpc 接口的方法, 如我们的 Say 实现了 SayHandler.go-micro 默认的 router 会利用反射把 Say 对象的信息完全提取出来, 解析出结构体内的方法及方法的参数, 保存到一个 map 内 -> map[结构体名称][方法信息集合]
具体的实现在 rpc_router.go 里 router 的 Handle(Handler) 方法, 组织完成后 map 的是下图这样, 保存了很多反射信息, 用以将来调用.
下面是这个方法的主要代码, 删除了一些, 很希望大家读一下 rpc_router.go 里面的代码, prepareMethod 方法是具体利用反射提取信息的方法.
- func (router *router) Handle(h Handler) error {
- router.mu.Lock()
- defer router.mu.Unlock()
- // ....
- rcvr := h.Handler()
- s := new(service)
- s.typ = reflect.TypeOf(rcvr)
- s.rcvr = reflect.ValueOf(rcvr)
- // check name
- // ....
- s.name = h.Name()
- s.method = make(map[string]*methodType)
- // Install the methods
- for m := 0; m <s.typ.NumMethod(); m++ {
- method := s.typ.Method(m)
- // prepareMethod 会把所有解析的信息返回来
- if mt := prepareMethod(method); mt != nil {
- s.method[method.Name] = mt
- }
- }
- // .....
- // save handler
- router.serviceMap[s.name] = s
- return nil
}
serviceMap 里保存的就是反射后的信息, 下图是我用 goland 的 debug 得到的保存信息
路由信息处理完后, 主要的工作就已经完成了, 然后注册服务并启动服务, 启动的服务是一个 http 的服务, 我们可以看一下 http_transport.go 里的代码
服务的简单流程图如下 , 选择通信协议和编码方式 -> 注册服务方法 -> 启动服务并注册服务信息
客户端调用服务方法
客户端在启动的时候也要选择默认的通信协议 http, 和 protobuf 编码. 客户端在调用 rpc 方法的时候如:
rsp, err := client.Hello(context.Background(), &model.SayParam{Msg: "hello server"})
go-micro 为我们自动生成的 rpcapi.micro.go 里我们可以看一上 Hello 的具体实现, 没有几行代码, 但内部还是做了很多工作
func (c *sayService) Hello(ctx context.Context, in *model.SayParam, opts ...client.CallOption) (*model.SayResponse, error) { req := c.c.NewRequest(c.name, "Say.Hello", in) out := new(model.SayResponse) err := c.c.Call(ctx, req, out, opts...) if err != nil { return nil, err } return out, nil
}
他的实现方式是封装 request, 然后调用服务方法. 这个 request 是非常重要的:
func newRequest(service, endpoint string, request interface{}, contentType string, reqOpts ...RequestOption) Request { var opts RequestOptions for _, o := range reqOpts { o(&opts) } // set the content-type specified if len(opts.ContentType)> 0 { contentType = opts.ContentType } return &rpcRequest{ service: service, method: endpoint, endpoint: endpoint, body: request, contentType: contentType, opts: opts, }
}
这个方法返回一个 rpcRequest 里面包含了详细的调用信息, servicec: 服务名, method 和 endpoint 目前是一样的是方法名这里是 Say.Hello,contentType 是 protobuf,body 是具体的信息, 也要要进行编码的内容, 使用 protobuf 进行编码, 然后会把这些信息放到一个 http.Request 里, 再从 Register 或者从缓存获取服务器信息, 连接服务器, 发送数据.
简单流程图如下
client: 封装参数 -> 编码数据 -> 连接服务 -> 发送数据 -> 接收返回数据, 并解码.
service: 接收数据 -> 解码数据, 找到相应的实例和方法, 利用反射调用具体方法 -> 编码返数据 -> 发送给客户端.
服务端处理请求
当服务端监接收到数据后, 从 http 的 Request 里的 Header 中读取到相应的信息: 编码方式, endpoint, 请求的数据, 由路由器进行对比和匹配找到保存的反射信息, 利用反射把请求的数据根据相应的编码方式进行解码, 再利用反射调用具体的方法, 处理完把返回数据进行编码, 组织一个 http.Response 传输给用户, 客户端接收到数据后进行解码读取数据. rpc_router.go 里的 call 方法就是具体的调用过程方法, 有时间大家可以读一下.
来源: https://juejin.im/post/5c74af7cf265da2dc45384eb