协议约定
net/rpc的注释说这个package已经不支持新功能了 The net/rpc package is frozen and is not
accepting new features.
RPC方法的格式
func (t *T) MethodName(argType T1, replyType *T2) error
- 方法本身是可导出的(能被其他package访问)
- 方法所属的类型是可导出的(这个规定个人感觉有点模糊,好像没什么用)
- 方法有且仅有两个参数,都是可导出类型或者内置类型
- 方法的第二个参数必须是一个指针(作为响应值)
- 方法有且仅有一个error类型的返回值
两种底层的rpc调用方式
Go:异步调用
Call:同步调用,本质上调用了Go方法然后等待调用完成信号
服务端
从服务端使用方式入手
注册服务TestNetRpc,提供了TestNetRpc.Hello方法,启动9909端口监听客户端请求
type TestNetRpc struct {}
func (t *TestNetRpc) Hello(request string, response *string) error {
*response = "hello, " + request
return nil
}
func main() {
rpc.Register(new(TestNetRpc)) // 注册服务
lis, err := net.Listen("tcp",":9909")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(lis.Addr())
rpc.Accept(lis) // 监听端口
}
服务注册
服务注册需要传递结构体实例,将结构体注册为服务,并筛选出结构体满足约定条件的可被远程调用的方法
定义服务端结构体
- 服务名称到具体服务信息的映射
type Server struct { serviceMap sync.Map // map[string]*service }
表示服务的结构体
- 服务名
- 注册为服务的结构体实例:reflect.Value可以表示任何实例
- 注册为服务的结构体类型 - 获取类型包含的方法:reflect.Type可以表示任何类型,且包含该类型的元信息
- 可导出的方法名到方法类型的映射:表示该服务提供了哪些方法
type service struct { name string // name of service rcvr reflect.Value // receiver of methods for the service typ reflect.Type // type of the receiver method map[string]*methodType // registered methods }
将实例注册为服务
- 根据实例获取结构体名称作为服务名,如果自定义了服务名则采用自定义的名称,并检查结构体是否可导出。从源码看出,如果用户自定义了服务名,那么即使该服务名不满足条件也不会使用结构体名作为服务名。而且自定义了符合要求的服务名的话,其实注册为服务的结构体本身就算不可导出也能注册成功,即要注册
type serviceA struct
为服务,需要指定符合要求的服务名,否则直接注册的话会失败,因为serviceA
不可导出。这样看来,其实net/rpc
只是要求服务名的首个字符为大写字母,没有其他限制。func (server *Server) register(rcvr interface{}, name string, useName bool) error { s := new(service) s.typ = reflect.TypeOf(rcvr) // 获取类型 s.rcvr = reflect.ValueOf(rcvr) // 获取实例本身 // Name():当Type为指针时Name()返回空字符串,所以要先通过Indirect取指针的值 sname := reflect.Indirect(s.rcvr).Type().Name() if useName { // useName==true则默认使用参数中的name sname = name } if sname == "" { s := "rpc.Register: no service name for type " + s.typ.String() log.Print(s) return errors.New(s) } // IsExported:判断字符串首字符是否为大写字母 if !token.IsExported(sname) && !useName { s := "rpc.Register: type " + sname + " is not exported" log.Print(s) return errors.New(s) } s.name = sname
- 查询结构体可被RPC调用的方法,存储这些方法。
注册服务时,如果注册的结构体的可导出方法第一个参数是指向实例本身的指针,即这种格式:
func (t *T) MethodName(argType T1, replyType *T2) error
,那么要求注册服务时也要传递实例指针。// 注册可以被RPC调用的方法 s.method = suitableMethods(s.typ, true) if len(s.method) == 0 { str := "" // To help the user, see if a pointer receiver would work. // reflect.PtrTo(s.typ)获取s.typ的指针类型 提醒用户修改注册服务的代码 method := suitableMethods(reflect.PtrTo(s.typ), false) if len(method) != 0 { // 提醒使用方注册服务时传递结构体实例指针 str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" } else { str = "rpc.Register: type " + sname + " has no exported methods of suitable type" } log.Print(str) return errors.New(str) } // serviceMap是一个sync.Map if _, dup := server.serviceMap.LoadOrStore(sname, s); dup { return errors.New("rpc: service already defined: " + sname) } return nil }
方法注册
suitableMethods方法。方法注册指的是根据结构体类型获取出结构体定义的方法,将可被远程调用的方法存储在服务对应的service结构体中。
- 定义如何描述一个方法(通过强大的反射功能)
- 方法本身的反射类型
- 参数的类型
- 响应的类型
type methodType struct { sync.Mutex // protects counters method reflect.Method ArgType reflect.Type ReplyType reflect.Type numCalls uint // 记录调用次数 非必要 }
- 根据注册为服务的结构体类型获取对应的可导出方法。
- reflect.Type.NumMethod()返回的是结构体包含的可导出的方法的数量
- reflect.Type.Method(index)通过index获取可导出的方法
- 根据约定的方法的格式筛选可被RPC调用的方法
- 有且仅有三个参数,第一个参数为调用方法的实例本身,第二个参数为调用该方法所需的参数,第三个参数为调用该方法得到的响应信息,第三个参数必须为指针
- 有且仅有一个返回值,且为error类型
// suitableMethods returns suitable Rpc methods of typ, it will report // error using log if reportErr is true. func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType { methods := make(map[string]*methodType) for m := 0; m < typ.NumMethod(); m++ { method := typ.Method(m) mtype := method.Type mname := method.Name // 通过PkgPath判断方法是否可导出 // 这一步我认为没必要,因为typ.Method获取到的都是可导出的方法 if method.PkgPath != "" { continue } // 一个方法必须有且仅有三个参数:方法所属结构体的实例本身、rpc参数、rpc响应值 if mtype.NumIn() != 3 { // NumIn()获取参数个数 if reportErr { log.Printf("rpc.Register: method %q has %d input parameters; needs exactly three\n", mname, mtype.NumIn()) } continue } // 下标从0开始,第一个参数是实例本身 // 对于Object.Method(req, resp),第一个参数是Object argType := mtype.In(1) if !isExportedOrBuiltinType(argType) {// rpc参数 必须为可导出或者内置类型 if reportErr { log.Printf("rpc.Register: argument type of method %q is not exported: %q\n", mname, argType) } continue } // Second arg must be a pointer. replyType := mtype.In(2) // resp必须为指针 if replyType.Kind() != reflect.Ptr { if reportErr { log.Printf("rpc.Register: reply type of method %q is not a pointer: %q\n", mname, replyType) } continue } // Reply type must be exported. if !isExportedOrBuiltinType(replyType) { if reportErr { log.Printf("rpc.Register: reply type of method %q is not exported: %q\n", mname, replyType) } continue } // Method needs one out. if mtype.NumOut() != 1 { // NumOut()获取返回值个数 if reportErr { log.Printf("rpc.Register: method %q has %d output parameters; needs exactly one\n", mname, mtype.NumOut()) } continue } // The return type of the method must be error. // 返回值必须为error if returnType := mtype.Out(0); returnType != typeOfError { if reportErr { log.Printf("rpc.Register: return type of method %q is %q, must be error\n", mname, returnType) } continue } methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType} } return methods }
typeOfError
是通过反射构造的error类型,用(*error)(nil)
表示,因为typeOfError
要和returnType做比较,而returnType是一个单纯的类型,不是通过reflect.TypeOf(某个实例)
得到的,所以typeOfError
也应该是一个单纯的类型
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
isExportedOrBuiltinType
方法中的t.PkgPath() == ""
用于判断类型t是否为go的内置类型,若不为内置类型则t.PkgPath()
等于定义该类型的package名
// Is this type exported or a builtin?
func isExportedOrBuiltinType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
// PkgPath will be non-empty even for an exported type,
// so we need to check the type name as well.
return token.IsExported(t.Name()) || t.PkgPath() == ""
}
监听请求
监听请求之后就是处理信息,所以需要先了解信息的格式以及信息的编解码方式
消息格式
net/rpc将消息分为头部和主体两部分,头部包括请求的序列号、服务名等基础信息;主体则为调用的RPC方法需要传递的参数
-
请求头Request
type Request struct { ServiceMethod string // format: "Service.Method" Seq uint64 // sequence number chosen by client next *Request // for free list in Server }
通过Seq唯一标识一个请求,Seq由客户端生成并指定,next指针用于构造请求链表以实现对象复用。
-
响应消息Response
- 客户端会通过Seq知道是对哪个请求的响应
type Response struct { ServiceMethod string // echoes that of the Request Seq uint64 // echoes that of the request Error string // error, if any. next *Response // for free list in Server }
-
由于每个请求和响应都需要定义Request/Response对象,为了减少内存的分配,net/rpc在服务端结构体中使用了freeReq/freeResp链表实现了对象池。当需要Request对象时,从 freeReq链表中获取,当使用完毕后,再放回链表中。
type Server struct { serviceMap sync.Map // map[string]*service reqLock sync.Mutex // protects freeReq freeReq *Request respLock sync.Mutex // protects freeResp freeResp *Response }
复用的过程如下:
func (server *Server) getRequest() *Request {
server.reqLock.Lock()
req := server.freeReq
if req == nil {
req = new(Request)
} else {
server.freeReq = req.next
*req = Request{}
}
server.reqLock.Unlock()
return req
}
func (server *Server) freeRequest(req *Request) {
server.reqLock.Lock()
req.next = server.freeReq
server.freeReq = req
server.reqLock.Unlock()
}
编解码方式
net/rpc默认采用的编解码方式是gob,位于package “encoding/gob”,是go专用的编码方式,因此无法跨语言使用。
-
定义一个编解码器应有的结构
- 读取/写入的io流(此处为socket连接)
- 编码器:将要编码的数据写入缓冲区,等待推送给要写入的socket连接
- 解码器:将socket连接中的数据读取到指定的对象中
- 缓冲区(编码器需要)
type gobServerCodec struct { rwc io.ReadWriteCloser dec *gob.Decoder enc *gob.Encoder encBuf *bufio.Writer closed bool } // 初始化操作 buf := bufio.NewWriter(conn) // socket连接 conn->io.ReadWriteCloser gobServerCodec{ rwc: conn, dec: gob.NewDecoder(conn), enc: gob.NewEncoder(buf), encBuf: buf, }
-
一个编解码器应该有:
- 编码方法:
gob.Encoder.Encode(数据)
方法,将数据写入到gob.Encoder
初始化时指定的缓冲区,再调用缓冲区的Flush()
方法将缓冲区的内容写入到初始化缓冲区时指定的io流(即调用Flush()之后数据才会写入服务端与客户端的socket连接)。gob.Encoder.Encode(数据)
编码的数据是有边界的,即依次执行gob.Encoder.Encode(数据1)
、gob.Encoder.Encode(数据2)
之后通过Decode
方法解码时第一次只能取到数据1,再执行一次才能取到数据2。
// 依次编码响应头和响应内容 func (c *gobServerCodec) WriteResponse(r *Response, body interface{}) (err error) { if err = c.enc.Encode(r); err != nil { if c.encBuf.Flush() == nil { // Gob couldn't encode the header. Should not happen, so if it does, // shut down the connection to signal that the connection is broken. log.Println("rpc: gob error encoding response:", err) c.Close() } return } if err = c.enc.Encode(body); err != nil { if c.encBuf.Flush() == nil { // Was a gob problem encoding the body but the header has been written. // Shut down the connection to signal that the connection is broken. log.Println("rpc: gob error encoding body:", err) c.Close() } return } return c.encBuf.Flush() }
- 解码方法:收到信息后解码查看信息内容。
gob.Decoder.Decode(承载体)
方法,将gob.Decoder
初始化时指定的io流(即服务端与客户端的socket连接)中的数据解码到承载体中。注意此处的承载体需要是已分配了内存空间的指针。当底层socket连接没有数据时会阻塞直到有数据。
// 读取请求头信息,将c.dec中的数据写入到r func (c *gobServerCodec) ReadRequestHeader(r *Request) error { return c.dec.Decode(r) } func (c *gobServerCodec) ReadRequestBody(body interface{}) error { return c.dec.Decode(body) } func (c *gobServerCodec) Close() error { if c.closed { // Only call c.rwc.Close once; otherwise the semantics are undefined. return nil } c.closed = true return c.rwc.Close() }
- 编码方法:
监听socket连接
-
服务端根据传入的net.Listener监听客户端的连接请求,有请求时建立连接然后异步处理
func Accept(lis net.Listener) { DefaultServer.Accept(lis) } func (server *Server) Accept(lis net.Listener) { for { conn, err := lis.Accept() if err != nil { log.Print("rpc.Serve: accept:", err.Error()) return } go server.ServeConn(conn) } }
-
server.ServeConn
方法定义了数据的编解码方式,然后根据指定的方式处理连接中的数据。使用与客户端的socket连接初始化编解码器。dec: gob.NewDecoder(conn)
使得解码时从连接中获取数据;enc: gob.NewEncoder(buf)
编码器需要缓冲区,且该缓冲区的底层io流应该为与客户端的连接。定义好数据的编解码方式后调用server.ServeCodec
处理请求func (server *Server) ServeConn(conn io.ReadWriteCloser) { buf := bufio.NewWriter(conn) srv := &gobServerCodec{ // gob编码方式为go语言专用的编码方式 无法跨语言 rwc: conn, dec: gob.NewDecoder(conn), enc: gob.NewEncoder(buf), encBuf: buf, } server.ServeCodec(srv) }
处理请求
整体流程
通过server.ServeCodec方法处理请求,主要分为解析请求信息和根据请求信息调用RPC方法两步。建立的连接是长连接,所以轮询读取连接中的数据并异步处理
1. sending := new(sync.Mutex):因为对于同一个连接的不同请求,响应的发送是异步的,所以需要互斥锁来避免对同一个连接的写入冲突
2. wg := new(sync.WaitGroup)用于不再接收请求时等待正在处理的请求处理完成再关闭连接
3. 若无法正确解析请求头部信息,server.readRequest(codec)将会返回keepReading==false,这种情况net/rpc直接选择关闭服务端与客户端的socket连接。
func (server *Server) ServeCodec(codec ServerCodec) {
sending := new(sync.Mutex)
wg := new(sync.WaitGroup)
for {
// 解析请求信息
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
if err != nil {
if debugLog && err != io.EOF {
log.Println("rpc:", err)
}
// 如果无法正确解析请求头部信息则keepReading==false 退出循环 关闭连接
if !keepReading {
break
}
// send a response if we actually managed to read a header.
// 发送解析请求信息出错的响应信息 invalidRequest = struct{}{}
if req != nil {
server.sendResponse(sending, req, invalidRequest, codec, err.Error())
server.freeRequest(req)
}
continue
}
wg.Add(1)
// 调用对应的rpc函数
go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
}
// We've seen that there are no more requests.
// Wait for responses to be sent before closing codec.
wg.Wait()
codec.Close()
}
解析请求头部信息
server.readRequestHeader
方法解析请求头部信息
1. 请求的头部信息包含了ServiceMethod字段,格式为服务名称.方法名称,根据ServiceMethod获取本次请求要调用的RPC方法
2. keepReading字段为true时表示只要能获取到请求头部信息,即使解析出错或者其他步骤出错,也能够继续处理该连接的其他请求。
3. 如果无法正确解析请求头部信息,会导致keepReading==false
,这将使得服务端直接关闭与客户端的socket连接。
func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) {
// Grab the request header.
req = server.getRequest() // 复用Request
err = codec.ReadRequestHeader(req) // 读取请求头部信息
if err != nil {
req = nil
if err == io.EOF || err == io.ErrUnexpectedEOF {
return
}
err = errors.New("rpc: server cannot decode request: " + err.Error())
return
}
keepReading = true
// 解析获取服务名和要调用的方法
dot := strings.LastIndex(req.ServiceMethod, ".")
if dot < 0 {
err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
return
}
serviceName := req.ServiceMethod[:dot]
methodName := req.ServiceMethod[dot+1:]
// Look up the request.
svci, ok := server.serviceMap.Load(serviceName)
if !ok {
err = errors.New("rpc: can't find service " + req.ServiceMethod)
return
}
svc = svci.(*service)
mtype = svc.method[methodName]
if mtype == nil {
err = errors.New("rpc: can't find method " + req.ServiceMethod)
}
return
}
解析请求信息
readRequest
方法
-
解析头部信息:若出错且选择跳过这个请求(keepReading==true)等待处理下一个请求,这时需要取出连接中的本次请求的消息主体避免影响对读取下一次请求时出错(需要与客户端约定每次传递消息都是完整的消息:头部+主体)
func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err error) { // 解析请求头部信息 service, mtype, req, keepReading, err = server.readRequestHeader(codec) if err != nil { if !keepReading { return } codec.ReadRequestBody(nil) return }
-
解析请求主体信息(即调用RPC方法所需的参数):上面“编解码方式”中提到,解码时传递的承载体需要是已分配内存的指针。所以我们需要通过要调用的方法对应的参数类型来构造参数实例指针(通过
reflect.New
,相当于平时写代码new一个指针),然后再通过ReadRequestBody
解码信息。argIsValue := false if mtype.ArgType.Kind() == reflect.Ptr { // 参数类型是指针类型 那需要调用Elem()获取具体的类型 argv = reflect.New(mtype.ArgType.Elem()) } else { argv = reflect.New(mtype.ArgType) argIsValue = true } if err = codec.ReadRequestBody(argv.Interface()); err != nil { return } if argIsValue { // 若实际的参数不是指针,则在获取具体内容后需要取指针内容 argv = argv.Elem() }
-
构造响应值实例:
- 响应类型为指针,所以需要.Elem()获取具体类型
- 类似平时写代码时使用slice和map需要通过make或者new申请内存一样,当响应值为slice或者map时要调用反射的
MakeSlice
或者MakeMap
方法来申请内存(创建实例)
replyv = reflect.New(mtype.ReplyType.Elem()) switch mtype.ReplyType.Elem().Kind() { case reflect.Map: replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem())) case reflect.Slice: replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0)) } return }
调用rpc方法
解析完请求后,通过service.call调用对应的RPC方法。
-
反射的Method类型的Func字段记录了调用这个方法所需的信息,包括方法地址等。通过Func.Call([]reflect.Value{…})执行函数
-
调用rpc方法时需要传递三个参数:调用的方法所属的结构体实例本身、方法参数、方法响应值
-
returnValues
中的对象是reflect.Value
类型,需要先转为interface{}类型再转为确切的类型func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) { if wg != nil { defer wg.Done() } mtype.Lock() mtype.numCalls++ // 统计方法的调用次数 对实际使用没有影响 mtype.Unlock() function := mtype.method.Func // Invoke the method, providing a new value for the reply. returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv}) // The return value for the method is an error. errInter := returnValues[0].Interface() errmsg := "" if errInter != nil { errmsg = errInter.(error).Error() } server.sendResponse(sending, req, replyv.Interface(), codec, errmsg) server.freeRequest(req) }
发送响应消息
调用完成后发送响应给客户端,server.sendResponse方法
-
通过sending互斥锁来防止异步处理请求时对同一个连接写入响应消息造成冲突
-
发送响应消息后释放Response对象以便复用
func (server *Server) sendResponse(sending *sync.Mutex, req *Request, reply interface{}, codec ServerCodec, errmsg string) { resp := server.getResponse() // 编辑响应头部信息 resp.ServiceMethod = req.ServiceMethod if errmsg != "" { resp.Error = errmsg reply = invalidRequest } resp.Seq = req.Seq sending.Lock() err := codec.WriteResponse(resp, reply) if debugLog && err != nil { log.Println("rpc: writing response:", err) } sending.Unlock() server.freeResponse(resp) }
支持HTTP协议
基于HTTP协议实现的RPC可以在监听一个端口的情况下使用不同的url支持不同服务。
Golang中开启HTTP服务
golang中只要实现接口http.Handler就可以处理HTTP请求,http.Handler接口仅有一个方法ServeHTTP(ResponseWriter, *Request)
,定义了处理HTTP请求的逻辑。
再通过http.Handle(pattern string, handler Handler)
方法注册http.Handler到指定的pattern(即URL)。
最后通过http.Serve(l net.Listener, handler Handler)
方法指定监听的端口开启HTTP服务即可。
开启HTTP服务后若有客户端访问ip:port+pattern则会执行注册在pattern上的handler的ServeHTTP方法。
与客户端建立连接
net/rpc定义的消息格式和HTTP协议的消息格式不兼容,所以需要转换协议,即先通过HTTP协议建立连接,此后的数据传输采用net/rpc定义的消息格式进行传输。
-
HTTP协议的CONNECT方法
CONNECT方法一般用于代理服务。比如客户端需要通过代理服务器与目的站点进行HTTPS通信,由于目的站点的地址和端口都被加密所以代理服务器不知道如何转发HTTPS请求。为此,客户端需要先发送一个CONNECT请求给代理服务器告知目的站点的地址和端口,代理服务器收到后与目的站点建立连接,此后通过该连接透传客户端与目的站点之间的数据包。这可以看作是协议转换。 -
net/rpc的服务端与客户端约定,通过CONNECT请求建立连接。
- 拒绝非CONNECT方法的HTTP请求
- Hijack()的作用:通过Hijack()接管HTTP的socket连接,Golang内置的HTTP库和 HTTPServer库将不会管理这个socket连接的生命周期,而是由Hijack()的调用方接管,这样调用方可以直接通过该socket和客户端进行通信且可以自定义消息格式。
- 拿到socket连接之后,构造CONNECT请求的成功响应,告知客户端已经成功建立连接,此后通过该socket进行通信。
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { if req.Method != "CONNECT" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusMethodNotAllowed) io.WriteString(w, "405 must CONNECT\n") return } // 接管socket连接 conn, _, err := w.(http.Hijacker).Hijack() if err != nil { log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) return } // 告知客户端成功建立连接 io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") // 监听该socket 逻辑同之前“监听socket连接”部分 server.ServeConn(conn) }
客户端
从客户端使用方式入手
延续“从服务端使用方式入手”
client, err := rpc.Dial("tcp", ":9909")
if err != nil {
t.Fatal("dialing:", err)
}
var reply string
err = client.Call("TestNetRpc.Hello", "hello", &reply)
if err != nil {
t.Fatal(err)
}
可以看到首先通过rpc.Dial
获取一个与服务端建立好连接的客户端,再调用client.Call调用指定的RPC方法。
与服务端建立连接
-
rpc.Dial
方法:根据传入的传输层协议名称以及目的地址建立连接,并初始化一个rpc客户端func Dial(network, address string) (*Client, error) { conn, err := net.Dial(network, address) // 建立socket连接 if err != nil { return nil, err } return NewClient(conn), nil }
-
NewClient
方法:编码方式默认使用gob,其中gobClientCodec
指定了客户端的编解码方式,与之前介绍服务端时的初始化方式以及定义的方法一样,只不过客户端是写请求读响应,而服务端是读请求写响应。func NewClient(conn io.ReadWriteCloser) *Client { encBuf := bufio.NewWriter(conn) client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf} return NewClientWithCodec(client) }
-
NewClientWithCodec
方法:根据消息编解码方式创建客户端,抽象出这个方法是为了扩展支持其他消息编解码方式。可以发现rpc客户端的结构体包含了:codec消息编解码方式以及pending记录请求序列号到请求内容的映射,请求的内容由Call结构体表示。pending表示待处理的请求- 参数类型ClientCodec是接口类型,任何实现了ClientCodec接口的结构体都可以作为客户端的编解码方式。
- 成功创建客户端的同时也异步调用了client.input()用于处理rpc服务端的响应消息
func NewClientWithCodec(codec ClientCodec) *Client { client := &Client{ codec: codec, pending: make(map[uint64]*Call), } go client.input() return client } type ClientCodec interface { WriteRequest(*Request, interface{}) error ReadResponseHeader(*Response) error ReadResponseBody(interface{}) error Close() error }
如何表示一个请求
请求内容至少需要包括:
- 请求的服务以及方法名
- 请求参数
- 记录响应对象的指针
- 请求出错时返回的错误信息
// Call represents an active RPC.
type Call struct {
ServiceMethod string // The name of the service and method to call.
Args interface{} // The argument to the function (*struct).
Reply interface{} // The reply from the function (*struct).
Error error // After completion, the error status.
Done chan *Call // Receives *Call when Go is complete.
}
其中Done
作为一个channel来传递调用请求成功或者失败的信号
调用RPC方法
通过Call
方法调用指定的RPC方法,属于同步调用,本质上调用了Go方法(异步调用)然后等待接收调用结束信号,信号由Done传递
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
return call.Error
}
Go
方法,比Call
方法多了一个参数done chan *Call
,用于传递调用结束信号。done需要带缓冲区,防止阻塞(下面会说call.done()时会说到)
- 构造请求信息:new(Call)
- 调用client.send发送请求
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
call := new(Call)
call.ServiceMethod = serviceMethod
call.Args = args
call.Reply = reply
if done == nil {
done = make(chan *Call, 10) // 这里我觉得缓冲区为1就够了 10应该是随便写的
} else {
// If caller passes done != nil, it must arrange that
// done has enough buffer for the number of simultaneous
// RPCs that will be using that channel. If the channel
// is totally unbuffered, it's best not to run at all.
if cap(done) == 0 {
log.Panic("rpc: done channel is unbuffered")
}
}
call.Done = done
client.send(call)
return call
}
client.send
方法发送请求
client.reqMutex
互斥锁用于防止对同一个连接同时写入时造成冲突- 检查客户端是否(主动-closing/被动-shutdown)关闭,是的话表示客户端不可用,直接记录错误并退出
- Client维护一个递增的序列号唯一标识一个请求,并将请求记录在pending字段中,表示待处理的请求
- 因为维护客户端是否关闭的两个属性以及
client.pending
在其他地方可能被修改,所以使用client.mutex
互斥锁防止竞争以及并发读写map - 客户端发送请求时也需要构造请求头,不同于服务端,客户端没有采用链表的方式的复用Request,我理解是因为服务端需要同时与多个客户端交互因此当请求量多时构造的Request也多,因此可以通过复用来节省内存,而客户端只连接一个服务端所以不考虑复用的情况
- 构造完请求头后将请求头与消息主体一起通过
client.codec.WriteRequest
发送给服务端。WriteRequest
类似服务端的WriteResponse
,依次编码消息头部和消息主体,然后一起发送出去。 - 发送请求失败时需要移除
client.pending
对应的请求并发送结束请求的信号
func (client *Client) send(call *Call) {
client.reqMutex.Lock()
defer client.reqMutex.Unlock()
// Register this call.
client.mutex.Lock()
if client.shutdown || client.closing {
client.mutex.Unlock()
call.Error = ErrShutdown
call.done()
return
}
seq := client.seq
client.seq++
client.pending[seq] = call
client.mutex.Unlock()
// Encode and send the request.
client.request.Seq = seq
client.request.ServiceMethod = call.ServiceMethod
err := client.codec.WriteRequest(&client.request, call.Args)
if err != nil {
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.Error = err
call.done()
}
}
call.done()
发送结束请求的信号。这里我认为可以直接执行call.Done <- call
,源码这样写是为了测试。为了避免done()阻塞,所以call.Done
需要带缓冲区。另外,异步调用的Go方法允许用户自定义call.Done的值,所以可以通过传同一个call.Done给多处调用Go的代码来控制并发嗲用RPC方法的数量。比如设置done = make(chan *Call, 10)
传递给11处调用Go的代码,那么当前10处调用了done()
的call.Done
中的数据还未被取走,则第11处调用done()的地方就会阻塞。
func (call *Call) done() {
select {
case call.Done <- call:
// ok
default:
// We don't want to block here. It is the caller's responsibility to make
// sure the channel has enough buffer space. See comment in Go().
if debugLog {
log.Println("rpc: discarding Call reply due to insufficient Done chan capacity")
}
}
}
处理服务端的响应消息
在创建客户端时会开启一个goroutine异步监听并处理服务端的响应,通过client.input()
方法。
- 从与服务端的socket连接中轮询读取响应消息,包括消息头和消息主体。读取出错则关闭连接。
- 首先读取响应消息的消息头,根据其中的序列号Seq获取客户端记录的待处理的请求。
func (client *Client) input() {
var err error
var response Response
for err == nil {
response = Response{}
err = client.codec.ReadResponseHeader(&response)
if err != nil {
break
}
seq := response.Seq
client.mutex.Lock()
call := client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
- 分类讨论根据序列号获取到的请求信息(可能会特殊情况)
- call == nil:在发送请求时对请求头和消息主体进行编码后需要调用Flush方法数据才会真正发送给服务端。可能存在这两种情况:
- 请求A写入请求头后写入消息主体时失败,那么客户端会从pending中移除请求A,但是请求A的头部信息仍存在于socket连接的缓冲区中(未发送);随后客户端发送请求B,那么服务端收到的数据将会是“请求A头部信息-请求B头部信息-请求B消息主体”。这种情况下服务端首先解析“请求A头部信息”,认为该请求是请求A,然后将“请求B头部信息”解析为请求A的消息主体,出错,返回错误信息给客户端。所以在call == nil逻辑中需要把该错误信息读取出来。
- 请求A在调用Flush方法的时候失败,那么客户端会从pending中移除请求A;随后客户端发送请求B,那么服务端收到的数据将会是“请求A头部信息-请求A消息主体-请求B头部信息-请求B消息主体”。这种情况下服务端会正常处理连接中请求A的相关信息然后返回,所以在call == nil逻辑中需要把服务端对请求A的响应消息读取出来才能不影响客户端读取请求B的响应信息。
- response.Error != “”:RPC方法内部出错,需要将响应消息读取出来但是不需要得到具体的消息内容。call.Error = ServerError(response.Error)设置请求的返回值err,ServerError是string的别名。
- 其余情况则是正常处理服务端的响应
- call == nil:在发送请求时对请求头和消息主体进行编码后需要调用Flush方法数据才会真正发送给服务端。可能存在这两种情况:
switch {
case call == nil:
// We've got no pending call. That usually means that
// WriteRequest partially failed, and call was already
// removed; response is a server telling us about an
// error reading request body. We should still attempt
// to read error body, but there's no one to give it to.
err = client.codec.ReadResponseBody(nil)
if err != nil {
err = errors.New("reading error body: " + err.Error())
}
case response.Error != "":
// We've got an error response. Give this to the request;
// any subsequent requests will get the ReadResponseBody
// error if there is one.
call.Error = ServerError(response.Error)
err = client.codec.ReadResponseBody(nil)
if err != nil {
err = errors.New("reading error body: " + err.Error())
}
call.done()
default:
err = client.codec.ReadResponseBody(call.Reply)
if err != nil {
call.Error = errors.New("reading body " + err.Error())
}
call.done()
}
}
- 处理响应的过程中出错,则退出循环,关闭与服务端的socket连接。
- 处理服务端响应是启动一个goroutine进行轮询,为了防止在向服务端发送请求时该goroutine因出错而要关闭连接,因此采用client.reqMutex。
- 使用client.mutex是该逻辑涉及对map的读取,对client一些属性的写入,防止写入/读取冲突
- client.shutdown = true表示客户端异常退出,因此需要处理client.pending中待处理的call,防止一些RPC调用在Call方法处阻塞等待(
<-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
)
// Terminate pending calls.
client.reqMutex.Lock()
client.mutex.Lock()
client.shutdown = true
closing := client.closing
if err == io.EOF {
if closing {
err = ErrShutdown
} else {
err = io.ErrUnexpectedEOF
}
}
for _, call := range client.pending {
call.Error = err
call.done()
}
client.mutex.Unlock()
client.reqMutex.Unlock()
if debugLog && err != io.EOF && !closing {
log.Println("rpc: client protocol error:", err)
}
}
支持HTTP协议
通过HTTP协议与服务端建立连接,入口函数为DialHTTP(network, address string)
。DefaultRPCPath
是与服务端约定好的URL。
// DialHTTP connects to an HTTP RPC server at the specified network address
// listening on the default HTTP RPC path.
func DialHTTP(network, address string) (*Client, error) {
return DialHTTPPath(network, address, DefaultRPCPath)
}
- 构造HTTP/1.0的请求,报文格式为“方法 url 协议版本\n\n报文主体”,发送CONNECT请求时不需要报文主体。
http.ReadResponse
根据传入的Request获取conn中对应的Reponse。- 若响应不符合预期则说明无法正确建立与服务端的连接,因此关闭通过
net.Dial
建立的socket连接。
// DialHTTPPath connects to an HTTP RPC server
// at the specified network address and path.
func DialHTTPPath(network, address, path string) (*Client, error) {
var err error
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")
// Require successful HTTP response
// before switching to RPC protocol.
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
if err == nil && resp.Status == connected {
return NewClient(conn), nil
}
if err == nil {
err = errors.New("unexpected HTTP response: " + resp.Status)
}
conn.Close()
return nil, &net.OpError{
Op: "dial-http",
Net: network + " " + address,
Addr: nil,
Err: err,
}
}