net/rpc源码分析
1. 前言
首先介绍一下rpc的概念,首先它是一种框架,而不是一种协议, 它的初衷是让用户可以向调本地方法一样调用远端的服务,在通信过程中,客户端和服务端需要约定一种通信机制,确保发送者发送的数据能被接收者能正确的解析,这就是协议 ,
-
如grpc框架采用的是HTTP2协议,而HTTP2是采用protocolbuf来进行编解码。
-
dubbo rpc框架采用dubbo通信协议,编解码也是采用dubbo特有的编解码方式
-
sofaRpc框架采用的是bolt私有协议,由蚂蚁金服开发
golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用。
net/rpc同时支持tcp或http数据传输方式。
本文将首先展示使用net/rpc包进行rpc调用的主要方法,随后对于net/rpc包的结构和流程进行具体分析。
2. 服务端
点击查看代码
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
)
// 算数运算结构体
type Arith struct {
}
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Result int
}
// 加法运算方法
func (this *Arith) Add(req ArithRequest, res *ArithResponse) error {
res.Result = req.A + req.B
return nil
}
func main() {
rpc.Register(new(Arith)) // 注册rpc服务
rpc.HandleHTTP() // 采用http协议作为rpc载体
lis, err := net.Listen("tcp", "127.0.0.1:8006")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
http.Serve(lis, nil)
}
HandleHTTP()方法内容如下:
// rpc/server.go
var DefaultServer = NewServer()
func NewServer() *Server {
return &Server{}
}
type Server struct {
serviceMap sync.Map // map[string]*service
reqLock sync.Mutex // protects freeReq
freeReq *Request
respLock sync.Mutex // protects freeResp
freeResp *Response
}
func HandleHTTP() {
DefaultServer.HandleHTTP(DefaultRPCPath, DefaultDebugPath)
}
const (
// Defaults used by HandleHTTP
DefaultRPCPath = "/_goRPC_"
DefaultDebugPath = "/debug/rpc"
)
func (server *Server) HandleHTTP(rpcPath, debugPath string) {
http.Handle(rpcPath, server) //
http.Handle(debugPath, debugHTTP{server})
}
可以看到在HandleHTTP函数中完成了http协议的处理方法的注册,而http请求处理函数的注册方式有两种:
http.Handle("/myrpc",http.HandlerFunc(customFunc)) // 方法1
http.HandleFunc("/myrpc",customFunc) // 方法2
其中,方法2接收一个类型为func(w http.ResponseWriter,r *http.Request)类型的处理函数,而方法一需要接收一个实现下述接口的实例:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
那就看看rpc server是如何实现这个ServeHTTP方法的。
// rpc/server.go
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
}
conn, _, err := w.(http.Hijacker).Hijack() //利用Hijack()函数将HTTP的TCP连接取出
if err != nil {
log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
return
}
// 打印响应头 CONNECT /_goRPC_ HTTP/1.0
io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
server.ServeConn(conn)
}
继续看server.ServeConn是怎么处理的:
其实走到这里已经跟HTTP没有什么关系了,处理流程都是拿tcp的conn来解析数据并处理,然后返回给客户端
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
buf := bufio.NewWriter(conn)
srv := &gobServerCodec{
rwc: conn,
dec: gob.NewDecoder(conn), //gob的解码器
enc: gob.NewEncoder(buf), //gob的编码器
encBuf: buf,
}
server.ServeCodec(srv)
}
func (server *Server) ServeCodec(codec ServerCodec) {
sending := new(sync.Mutex)
wg := new(sync.WaitGroup)
for {
//解码,得到请求的service、方法、参数等信息
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
if err != nil {
if debugLog && err != io.EOF {
log.Println("rpc:", err)
}
if !keepReading {
break
}
// send a response if we actually managed to read a header.
if req != nil {
server.sendResponse(sending, req, invalidRequest, codec, err.Error())
server.freeRequest(req)
}
continue
}
wg.Add(1)
//反射执行service里对应的方法,得到结果后再编码数据并返回
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()
}
3. 服务注册
服务注册本来是需要一个独立的注册中心的,类似zk,但在net/rpc包中并不存在这样一个角色,目前的做法是在服务端注册一个服务列表功能,并在客户端包中做调用该功能的相关封装。因此客户端和服务端都要维护请求和响应相关的结构体信息。说白了就是客户端发送请求是需要按照一定的格式发给服务端,服务端会拿自己的格式来处理这些请求,处理完后也要按一定的格式返回给客户端,客户端会按期望的格式获取结果。其原理有点像GRPC的protocolbuf。
rpc.register函数传递一个带有相应方法的实例指针,根据源码注释的内容,方法必需遵循一系列格式要求:
-
the method's type is exported. 方法所属类型必须是导出类型(首字母大写)
-
the method is exported. 方法本身也必须是被导出的(首字母大写)
-
the method has two arguments, both exported (or builtin) types. 方法接收两个被导出类型或内置类型的参数
-
the method's second argument is a pointer. 方法的第二个参数必需是指针
-
the method has return type error. 方法的返回值必须是一个error类型的量
func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) }
func (server *Server) Register(rcvr interface{}) error { //接收一个空接口
return server.register(rcvr, "", false)
}func (server *Server) register(rcvr interface{}, name string, useName bool) error {
s := new(service) //新建服务实例s.typ = reflect.TypeOf(rcvr) //服务结构类型 s.rcvr = reflect.ValueOf(rcvr) //服务结构reflect.Value值 sname := reflect.Indirect(s.rcvr).Type().Name() if useName { sname = name } if sname == "" { s := "rpc.Register: no service name for type " + s.typ.String() log.Print(s) return errors.New(s) } if !token.IsExported(sname) && !useName { s := "rpc.Register: type " + sname + " is not exported" log.Print(s) return errors.New(s) } s.name = sname // Install the methods // 对传入量所包含的方法进行reflect处理,生成map[string]*methodType型的注册方法字典 s.method = suitableMethods(s.typ, true) if len(s.method) == 0 { str := "" // To help the user, see if a pointer receiver would work. 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) } //将新建立的服务实例存入Server实例的服务字典中(serviceMap sync.Map) if _, dup := server.serviceMap.LoadOrStore(sname, s); dup { return errors.New("rpc: service already defined: " + sname) } return nil
}
其中关键的service和method结构体内容如下,分别代表了一个对外服务和服务中包含的一个方法。
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 methodType struct {
sync.Mutex // protects counter方法互斥锁
method reflect.Method//方法结构
ArgType reflect.Type//方法参数类型
ReplyType reflect.Type//方法返回值类型
numCalls uint //被调用次数
}
在这一部分,其核心是使用reflect包的反射技术解析出注册服务传入的结构,包括服务信息和服务包含的方法集信息,关键代码如下:
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
if method.PkgPath != "" //方法必需是被导出的
......
if mtype.NumIn() != 3 //方法必需包含三个输入
......
argType := mtype.In(1) //第一个参数必须是被导出的或内置类型
if !isExportedOrBuiltinType(argType)
......
replyType := mtype.In(2)
if replyType.Kind() != reflect.Ptr //第二个参数必需是一个指针型
......
if !isExportedOrBuiltinType(replyType) //第二个参数必需是被导出的
......
if mtype.NumOut() != 1 //必需有一个输出
......
if returnType := mtype.Out(0); returnType != typeOfError //输出类型为error
......
//将方法存储在方法集中
methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
}
return methods
}
可以看到,在注册部分使用了大量的反射技术来获取服务和方法信息,最终也是以reflect包中定义的结构进行存储,因此需要掌握reflect包的使用方法才能理解这部分内容 。
4. 客户端
向服务端进行客户端连接后,调用call()函数,传递服务函数名(类型名.函数名)和调用函数参数,完成服务调用。
package main
import(
"../rpc_objects"
"fmt"
"log"
"net/rpc"
)
const serverAddress="localhost"
func main(){
client,err:=rpc.DialHTTP("tcp",serverAddress+":1234") //通过http协议连接服务器
if err!=nil{
log.Fatal(err)
}
args:=&rpc_objects.Args{13,4}
var reply int
err=client.Call("Args.Multiply",args,&reply) //进行服务调用
if err!=nil{
log.Fatal("Args error",err)
}
fmt.Println(reply)
}
看看rpc.DialHTTP方法:
func DialHTTP(network, address string) (*Client, error) {
return DialHTTPPath(network, address, DefaultRPCPath)
}
func DialHTTPPath(network, address, path string) (*Client, error) {
var err error
conn, err := net.Dial(network, address) //向目标服务器建立TCP连接
if err != nil {
return nil, err
}
io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n") //向目标服务器建立HTTP连接,请求路径为/_goRPC_,请求方法为CONNECT,CONNECT方法是需要使用tcp去直接连接的,所以不适合在网页开发中使用
//在转向RPC高层处理前,以CONNECT方式尝试进行成功连接
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
if err == nil && resp.Status == connected {
return NewClient(conn), nil //使用已建立的conn连接创建新Client实例
}
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,
}
}
func NewClient(conn io.ReadWriteCloser) *Client {
encBuf := bufio.NewWriter(conn)
client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
return NewClientWithCodec(client)
}
func NewClientWithCodec(codec ClientCodec) *Client {
client := &Client{
codec: codec, //gob编码解码器
pending: make(map[uint64]*Call), //rpc请求等待区
}
go client.input() //启动input()协程处理接收内容
return client
其中input方法用来接收服务端的响应数据,核心逻辑是解码并填充Reply等字段
5. 总结
-
net/rpc包在底层的HTTP连接阶段通过统一的HTTP访问路径和统一的method(CONNECT)完成了RPC握手。
具体流程是客户端发送一个“CONNECT /goRPC HTTP/1.0”的HTTP报文,只是没有header和body部分,服务端会用标准的http服务来处理,只是处理时,只返回“ CONNECT /goRPC HTTP/1.0”的响应。 -
在建立连接和完成握手之后,Client和Server端均通过操作TCP连接实例conn进行通信。
-
客户端是采用gob进行编码的,通过捕获客户端conn.Read()信息,
ServiceMethodt��
Seq�� Arith.Add&��
ArithRequest��AB��
发现确实是用gob来编码的
同理服务端也是
ServiceMethodse��
SeqError
ArithResponse��Result�� Arith.Add&��