Remote Procedure Call
RPC是一种进程间通信协议,它允许程序调用另一个地址空间的过程或函数。开发RPC的动机和核心问题就是如何执行另外一个地址空间上的函数和方法,就像本地调用一样。
在网络通信中,RPC相当于一种约束Request和Response的协议。目前RPC框架大致有两种不同的侧重,一种是偏向于服务治理,提供了丰富的功能,适用于大型服务的微服务拆分和管理,另一种侧重于跨语言调用,比如gRPC。smallnest.gitbooks.io/go-rpc讨论了国内外许多RPC框架,总结了RPC调用的基本过程如下
个人理解RPC就是描述client server间点对点的通信过程,它要实现stub,通信和消息解析三个部分。下面就从这三个方面记录下go标准库的RPC是怎么实现这三部分的。
- stub主要完成协议结构(Wire Protocol),它要跟序列化和反序列化配合完成消息的读取和转换
- 通信传输(Transport)可以用TCP也可以HTTP
- 序列化反序列化(Serialization)可以是protobuf也可以是json等,go-RPC的序列化用gob做的
Go-RPC-Client
官方例子中客户端示例中,client调用Call方法即可获得结果。RPC实现的要点就是如何把这个rpc call在client stub转化为发给server的请求。
1 | func main() { |
DialHTTP
在RPC Over HTTP的场景下,rpc.DialHTTP
其实就是用默认Path = /\_goRPC\_
发一条CONNECT请求给server端。如果正常相应并连接,则创建rpcClient
进行后续处理。
1 | func DialHTTPPath(network, address, path string) (*Client, error) { |
Go & Do
go-RPC包默认使用gob编解码,本节跳过编解码过程。创建rpcClient的过程开启了input goroutine,等待响应。
1 | type Client struct { |
1 | func NewClientWithCodec(codec ClientCodec) *Client { |
call方法是就是将method以及req和reply封装在数据结构Call中,最终经过client.send
发送。
1 | type Call struct { |
rpc包提供给外部调用的是call方法,调用了异步的Go,他们通过call同步,但Call没有提供超时机制,肯定有性能问题。Do是rpc包提供的默认同步调用方法。
1 | func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call { |
send & input
之前看http client的实现,其实就是要实现conn复用。http的实现方式是通过复用一个DefaultTransport,在其中维护连接池来实现。而rpc的client是通过seq序列号和pending来记录client上每个请求。
这里send需要上锁是是因为一个rpcClient可以支持并发发送请求。pending这个map是用来存目前client正在处理的call,其中key用seq来标记,seq单调递增,这个seq类似在对client端的请求编号。
最后通过codec进行请求参数的序列化,并写入socket。若返回错误将pending中的请求记录删掉,并通过call同步调用方本次call已完成。
1 | func (client *Client) send(call *Call) { |
input负责这条RPC连接上所有的响应读取。如果请求成功,则删掉pending对应的请求记录,并取出call记录反序列化响应到call.reply上,最后通知调用方call.done。
当读取响应头出错后说明发生连接关闭或EOF等错误,这时要把这个client上所有pending的请求全部call.done。
1 | func (client *Client) input() { |
Go-RPC-Server
这是go-rpc官方注释里的例子,显然这是个RPC Over HTTP的例子。rpc.HandleHTTP直接向http的ServerMux注册了默认的rpcPath=/\_goRPC\_
和rpc.DefaultServer
。
1 | func main() { |
1 | func (server *Server) HandleHTTP(rpcPath, debugPath string) { |
serveHTTP & serveConn
上一节讲了client如果是DialHTTP会先通过发送CONNECT请求来建立连接,对应的RPC server端只处理CONNECT这种请求,并通过Hijack读出conn,然后开始处理这个TCPconn。
1 | func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
serveConn包含了RPC server端所有的操作:read请求,开goroutine处理请求,返回响应。
1 | func (server *Server) ServeCodec(codec ServerCodec) { |
register & call
server端就是要根据service,method以及对应的请求参数来执行一次远程调用,并返回响应。如何注册一个服务,如何从注册服务中找到要执行的方法,go-RPC是通过反射提供的类型信息完成的。
下面Server的数据结构,serviceMap用于保存所有注册的服务。其中service保留了服务名,服务类型以及服务实例,服务实例通常是指针的值。method中保留了服务对外Export的方法,以及方法名,参数和响应类型。
1 | type Server struct { |
以arith示例为例,这里*schema.Arith是注册服务实例的类型。s.rcvr是该指针的值,即这个实例的地址。sname通过Indirect
方法获得这个指针Value的Elem
,即获得了该指针指向的实例,通过Type
获得它的类型是schema.Arith,注册名为Arith。
reflect.Indirect方法是返回的指针value的Elem,是副本还是直接取地址,是否可改变
1 | func (server *Server) register(rcvr interface{}) error { |
register的过程中检查该类型所有的method,并且通过NumIn
和In
检查入参的类型和是否Exported,其中reply的类型必须是指针。最后NumOut
和Out
是用来检查返回值是否只有一个且类型为error。
1 | var typeOfError = reflect.TypeOf((*error)(nil)).Elem() |
read request & header
处理连接时首先读取header,为了重用request数据结构,go-RPC用了一个链表。有点难理解的是Request其实只是一个头,其中有Seq和Method。从Req中读取的ServiceMethod通常是 Arith.Divide
这种形式,最后就通过server.serviceMap找到服务名和方法名。
1 | type Request struct { |
1 | func (server *Server) readRequestHeader(codec ServerCodec) (service *service, |
在header中读取到method之后,可以知道参数类型,通过reflect.New
生成对应该类型的PtrTo(typ)
,即*schema.Arith类型。Interface
方法返回的是当前argv的值,也就是&{0,0}。同理最后replyv就是创建的存储reply对象的指针。
1 | func (server *Server) readRequest(codec ServerCodec) (service *service, |
call & response
reflect中的Method的Type字段存储方法类型,Func字段直接存储方法,该方法以receiver为第一个入参。返回值通过Interface反射回来,可能是nil或error类型。
1 | func (s *service) call(server *Server, sending *sync.Mutex, mtype *methodType, |
回写响应要把客户端带的seq返回,并且写响应时需要lock的。
1 | type Response struct { |
1 | func (server *Server) sendResponse(sending *sync.Mutex, req *Request, reply interface{}, codec ServerCodec, errmsg string) { |