RPC - Remote Procedure Call Protocol 就是想实现函数调用模式的网络化,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传给服务端,服务端解包到处理过程中执行,然后执行结果返回给客户端
运行时一次客户机对服务器的RPC调用步骤有:
- 调用客户端句柄,执行传送参数
- 调用本地系统内核发送网络信息
- 消息传送到远程主机
- 服务器句柄得到消息并取得参数
- 执行远程过程
- 执行的过程将结果返回服务器句柄
- 服务器句柄返回结果,调用远程系统内核
- 消息传回本地主机
- 客户句柄由内核接收消息
- 客户接收句柄返回的数据
标准库提供了 net/rpc 包用来实现基础的rpc 调用。
net/rpc 库使用encoding/gob 进行编解码,支持tcp 或http 数据传输方式,由于其他语言不支持gob 编解码方式,所以使用net/rpc 库实现的RPC 方法没办法进行跨语言调用。
主要有服务端和客户端。
首先是提供方法暴露的一方–服务器。
服务定义及暴露
在编程实现过程中,服务器端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法称之为输出方法,此输出方法可以被远程调用。当然,在定义输出方法时,能够被远程调用的方法需要遵循一定的规则。我们通过代码进行讲解:
func (t *T) MethodName(request T1,response *T2) error
T、T1和T2类型都必须能被encoding/gob包编解码
任何RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据
上述代码是go语言官方给出的对外暴露的服务方法的定义标准,其中包含了主要的几条规则,分别是:
- 对外暴露的方法有且只能有两个参数,这个两个参数只能是输出类型或内建类型,两种类型中的一种。
- 方法的第二个参数必须是指针类型。
- 方法的返回类型为error。
- 方法的类型是可输出的。
- 方法本身也是可输出的。
HTTP服务端代码
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
)
type Arithmetic int
type Args struct {
A, B int
}
func (a *Arithmetic) Add(args *Args, reply *int) error {
fmt.Println("接收到请求信息 :", args)
*reply = args.A + args.B
return nil
}
func main() {
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
defer l.Close()
_ = rpc.Register(new(Arithmetic))
rpc.HandleHTTP()
e = http.Serve(l, nil)
if e != nil {
log.Fatal("server error:", e)
}
}
HTTP客户端代码
package main
import (
"fmt"
"log"
"net/rpc"
)
type Args struct {
A, B int
}
func main() {
client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("client error:", err)
}
args := Args{4, 2}
var reply int
err = client.Call("Arithmetic.Add", args, &reply)
if err != nil {
log.Fatal("arithmetic error:", err)
}
fmt.Println(reply)
}
net/rpc 还提供了一个简易的统计页供查看: http://127.0.0.1:1234/debug/rpc
TPC服务端
func main() {
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
defer l.Close()
_ = rpc.Register(new(Arithmetic))
conn, e := l.Accept()
rpc.ServeConn(conn)
}
TCP客户端
func main() {
client, err := rpc.Dial("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("client error:", err)
}
args := Args{4, 2}
var reply int
err = client.Call("Arithmetic.Add", args, &reply)
if err != nil {
log.Fatal("arithmetic error:", err)
}
fmt.Println(reply)
}
原理解析
首先注册了两个路由与handle,这是 rpc.HandleHTTP() 做的事情。
func (server *Server) HandleHTTP(rpcPath, debugPath string) {
http.Handle(rpcPath, server)
http.Handle(debugPath, debugHTTP{server})
}
func HandleHTTP() {
DefaultServer.HandleHTTP(DefaultRPCPath, DefaultDebugPath)
}
其中
DefaultRPCPath = "/_goRPC_"
DefaultDebugPath = "/debug/rpc"
而且 /debug/rpc 在上面还用到过; /_goRPC_ 则是rpc服务的路由,然后通过传递的参数去调用指定对象的方法。
再就是注册服务,其实就是通过反射导出对象的可调用的方法存入一个map。这是 rpc.Register(new(repo.Order)) 做的事情。
源码如下:
func (server *Server) register(rcvr interface{}, name string, useName bool) error {
s := new(service)
s.typ = reflect.TypeOf(rcvr)
s.rcvr = reflect.ValueOf(rcvr)
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
s.method = suitableMethods(s.typ, true)
if len(s.method) == 0 {
str := ""
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)
}
if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
return errors.New("rpc: service already defined: " + sname)
}
return nil
}
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
...
...
methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
}
return methods
}
为了更直观的理解,可以看下面的例子。
package main
import (
"fmt"
"reflect"
)
type Peaple struct {
}
func (p *Peaple) Eat() {
}
func (p *Peaple) Drink() {
}
func main() {
a := new(Peaple)
getType := reflect.TypeOf(a)
getValue := reflect.ValueOf(a)
obj := reflect.Indirect(getValue).Type().Name()
num := getType.NumMethod()
for i := 0; i < num; i++ {
m := getType.Method(i)
fmt.Println(obj + "." + m.Name)
}
}
输出结果
Peaple.Drink
Peaple.Eat
也就是客户端调用的第一个参数。
参考地址
- https://www.cnblogs.com/wanghui-garcia/p/10451028.html
- https://studygolang.com/static/pkgdoc/pkg/net_rpc.htm
- https://blog.csdn.net/raoxiaoya/article/details/109473904
|