文章目录
- RPC 介绍
- 简介
- Go 语言中的 RPC 库
- RPC 程序示例
- 不同协议的 RPC
- HTTP 协议
- TCP 协议
- JSON 协议
- RPC 程序分析
- 服务器程序代码分析
- 客户端程序代码分析
RPC 介绍
简介
远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。
远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数,执行结果将返回给客户端, RPC 本身是 client-server 模型,也是一种 request-response 协议。
程序调用的方式有以下几种:
(1)RPC 调用,按照以下步骤编写 Go 程序:
- 在任意目录下创建一个 server 项目文件并初始化(
go mod init server
),将 HelloService 类型的对象注册为一个 RPC 服务,具体的程序代码如下:
package main
import (
"log"
"net/rpc"
"net"
)
// 构造一个 HelloService 类型,Hello() 方法用于实现打印功能
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello " + request
return nil
}
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
}
Hello() 方法必须满足 Go 语言的 RPC 规则(方法只能有两个可序列化的参数,其中第二个参数是指针类型并且返回一个 error 类型,同时必须是公开的方法)。
rpc.Register() 函数调用会将对象类型中所有满足 RPC 规则的对象方法注册为 RPC 函数,所有注册的方法会放在 “HelloService” 服务空间之下,然后建立一个唯一的 TCP 链接并通过 rpc.ServeConn() 函数在该 TCP 链接上为对方提供 RPC 服务。
- 在任意目录下创建 client 项目文件夹并初始化(
go mod init client
),编写客户端程序请求 HelloService 服务,具体的程序代码如下:
package main
import (
"log"
"net/rpc"
"fmt"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("HelloService.Hello", "cqupthao", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
通过 rpc.Dial() 函数拨号 RPC 服务,然后通过 client.Call() 函数调用具体的 RPC 方法。在调用 client.Call() 函数时,第一个参数是用点号链接的 RPC 服务名字和方法名字,第二和第三个参数分别定义 RPC 方法的两个参数。
分别执行客户端和服务端程序,输出如下的结果:
hello cqupthao
(2)本地调用,在任意目录下创建 LoacalCall 项目文件并初始化( go mod init LocalCall
),编写 Go 程序实现本地调用,该程序的具体代码如下:
package main
import "fmt"
func add(x, y int)int{
return x + y
}
func main(){
// 调用本地函数 add
a := 20
b := 19
ret := add(a, b)
fmt.Printf("%v + %v = ", a, b)
fmt.Println(ret)
}
执行以上程序,输出如下的结果:
20 + 19 = 39
在上述的程序中本地调用 add() 函数的执行流程,可以理解为以下四个步骤:
- 将变量 a 和 b 的值分别压入堆栈上;
- 执行 add() 函数,从堆栈中获取 a 和 b 的值,并将它们分配给 x 和 y ;
- 计算 x + y 的值并将其保存到堆栈中;
- 退出 add() 函数并将 x + y 的值赋给 ret 。
(3)HTTP 调用 RESTful API ,Go 程序实现参考以下的步骤:
- 在任意目录下创建 server 项目文件并初始化(
go mod init sever
),编写一个基于 HTTP 的 server 服务,它将接收其他程序发来的 HTTP 请求,执行特定的程序并将结果返回,该程序的具体代码如下:
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
)
type addParam struct {
X int `json:"x"`
Y int `json:"y"`
}
type addResult struct {
Code int `json:"code"`
Data int `json:"data"`
}
func add(x, y int) int {
return x + y
}
func addHandler(w http.ResponseWriter, r *http.Request) {
// 解析参数
b, _ := ioutil.ReadAll(r.Body)
var param addParam
json.Unmarshal(b, ¶m)
// 业务逻辑
ret := add(param.X, param.Y)
// 返回响应
respBytes , _ := json.Marshal(addResult{Code: 0, Data: ret})
w.Write(respBytes)
}
func main() {
http.HandleFunc("/add", addHandler)
log.Fatal(http.ListenAndServe(":9090", nil))
}
- 在任意目录下创建 client 项目文件并初始化(
go mod init client
),编写一个客户端程序来请求上述 HTTP 服务,传递 x 和 y 两个整数,等待返回结果,该程序的具体代码如下:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type addParam struct {
X int `json:"x"`
Y int `json:"y"`
}
type addResult struct {
Code int `json:"code"`
Data int `json:"data"`
}
func main() {
// 通过 HTTP 请求调用其他服务器上的 add 服务
url := "http://127.0.0.1:9090/add"
param := addParam{
X: 10,
Y: 20,
}
paramBytes, _ := json.Marshal(param)
resp, _ := http.Post(url, "application/json", bytes.NewReader(paramBytes))
defer resp.Body.Close()
respBytes, _ := ioutil.ReadAll(resp.Body)
var respData addResult
json.Unmarshal(respBytes, &respData)
fmt.Print(param.X , "+", param.Y , "=")
fmt.Println(respData.Data) // 30
}
执行该程序,输出如下的结果:
10+20=30
RPC 是为了解决类似远程、跨内存空间、的函数/方法调用的。
- 在本地调用中,函数主体通过函数指针函数指定,然后调用 add() 函数,编译器通过函数指针函数自动确定 add() 函数在内存中的位置。但在 RPC 中,调用不能通过函数指针完成,因为它们的内存地址可能完全不同。故调用方和被调用方都需要维护一个 { function <-> ID } 映射表,以确保调用正确的函数;
- 在本地过程调用中传递的参数是通过堆栈内存结构实现的,但 RPC 不能直接使用内存传递参数,故参数或返回值需要在传输期间序列化并转换成字节流,反之亦然;
- 函数的调用方和被调用方通常是通过网络连接的,即 function ID 和序列化字节流需要通过网络传输,故只要能够完成传输,调用方和被调用方就不受某个网络协议的限制(如在一些 RPC 框架使用 TCP 协议,一些使用 HTTP)。
Go 语言中的 RPC 库
Go 语言提供了一个 RPC 库( net/rpc
包),RPC 提供了通过网络访问一个对象的输出方法的能力,RPC 让远程调用就像本地调用一样,其调用过程如下图所示的步骤:
- ① 服务调用方(client)以本地调用方式调用服务;
- ② client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- ③ client stub 找到服务地址,并将消息发送到服务端;
- ④ server 端接收到消息;
- ⑤ server stub 收到消息后进行解码;
- ⑥ server stub 根据解码结果调用本地的服务;
- ⑦ 本地服务执行并将结果返回给server stub;
- ⑧ server stub 将返回结果打包成能够进行网络传输的消息体;
- ⑨ 按地址将消息发送至调用方;
- ⑩ client 端接收到消息;
- ⑪ client stub 收到消息并进行解码;
- ⑫ 调用方得到最终结果。
Go RPC 库提供了通过网络访问一个对象方法的能力,服务器需要注册对象, 通过对象的类型名暴露这个服务。注册后这个对象的输出方法就可以远程调用,这个库封装了底层传输的细节,包括序列化(默认 GOB 序列化器)。
服务器可以注册多个不同类型的对象,但是注册相同类型的多个对象的时候会出错。同时,如果对象的方法要能远程访问,它们必须满足一定的条件,否则这个对象的方法会被忽略,这些条件为以下内容:
- 方法的类型是可输出的;
- 方法本身也是可输出的;
- 方法必须由两个参数,必须是输出类型或者是内建类型;
- 方法的第二个参数必须是指针类型 ;
- 方法返回类型为 error 。
所以一个输出方法的格式如下:
func (t *T) MethodName(argType T1, replyType *T2) error
这里的 T、T1、T2 能够被 encoding/gob
序列化,即使使用其它的序列化框架。这个方法的第一个参数代表调用者(client)提供的参数,第二个参数代表要返回给调用者的计算结果,方法的返回值如果不为空, 则它作为一个字符串返回给调用者;如果返回 error ,则 reply 参数不会返回给调用者。
(1)服务器通过调用 ServeConn 在一个连接上处理请求,更典型地, 它可以创建一个 network listener 然后 accept 请求。
对于 HTTP listener 来说,可以调用 HandleHTTP() 方法 和 http.Serve() 函数 。
(2)客户端可以调用 Dial 和 DialHTTP 建立连接, 客户端有两个方法调用服务(Call 和 Go),可以同步地或者异步地调用服务,调用服务时需要把服务名、方法名和参数传递给服务器。异步方法调用 Go 通过 Done channel 通知调用结果返回。
除非显示的设置 codec ,否则这个库默认使用包
encoding/gob
作为序列化框架。
RPC 程序示例
(1)在任意目录下创建 server 项目文件并初始化( go mod init server
),编写一个使用字符串操作的服务程序,首先定义远程调用过程调用相关接口传入参数和返回参数的数据结构,如下代码所示调用字符串操作的请求包包括两个参数(字符串 A 和 字符串 B):
type StringRequest struct {
A string
B string
}
(2)定义一个服务对象,这个服务对象可以很简单, 比如类型是 int 或 interface{} ,重要的是它输出的方法。这里定义一个字符串服务类型的 interface ,其名称为 Service ,它有两个方法(字符串拼接 Concat() 方法和字符串差异 Diff() 方法),定义一个名为 StringService 的结构体并实现 Service 接口,实现以上的两个方法,具体的程序代码如下所示:
package main
import (
"net"
"log"
"net/rpc"
"net/http"
"strings"
"errors"
)
type StringRequest struct {
A string
B string
}
const StrMaxSize = 12
var ErrMaxSize = errors.New("out the StrMaxsize")
type Service interface {
Concat(req StringRequest, ret *string) error
Diff(req StringRequest, ret *string) error
}
type StringService struct {}
func (s *StringService) Concat(req StringRequest, ret *string) error {
if len(req.A) + len(req.B) > StrMaxSize {
*ret = ""
return ErrMaxSize
}
*ret = req.A + req.B
return nil
}
func (s *StringService) Diff(req StringRequest, ret *string) error {
if len(req.A) < 1 || len(req.B) < 1 {
*ret = ""
return nil
}
res := ""
if len(req.A) >= len(req.B) {
for _, char := range req.B {
if strings.Contains(req.A, string(char)) {
res = res + string(char)
}
}
} else {
for _, char := range req.A {
if strings.Contains(req.B, string(char)) {
res = res + string(char)
}
}
}
*ret = res
return nil
}
(3)实现 RPC 服务器,生成一个 StringService 结构体并使用 rpc.Register() 函数注册这个服务,通过调用 net.Listen() 方法来监听对应的 socket 并对外提供服务,具体的代码如下所示:
func main() {
stringService := new(StringService)
rpc.Register(stringService)
rpc.HandleHTTP()
l, err := net.Listen("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("listen error: ", err)
}
http.Serve(l, nil)
}
(4)在任意目录下创建 client 项目文件并初始化( go mod int client
),编写客户端程序来远程调用定义的服务,该程序具体的代码如下所示:
package main
import (
"net/rpc"
"log"
"fmt"
)
type StringRequest struct {
A string
B string
}
func main() {
client , err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("dailing error: ", err)
}
// 同步调用方式
stringReq := &StringRequest{"A", "B"}
var reply string
err = client.Call("StringService.Concat", stringReq, &reply)
if err != nil {
log.Fatal("Concat error: ", err)
}
fmt.Printf("StringService Concat : %s concat %s = %s \n", stringReq.A, stringReq.B, reply)
// 异步调用方式
stringReq = &StringRequest{"ABC", "CDEF"}
call := client.Go("StringService.Diff", stringReq, &reply, nil)
replyCall := <-call.Done
fmt.Println(replyCall.Error)
fmt.Printf("StringService Diff: %s Diff %s = %s \n",stringReq.A ,stringReq.B, reply)
}
(6)分别编译执行服务端和客户端程序,输出的结果如下:
StringService Concat : A concat B = AB
<nil>
StringService Diff: ABC Diff CDEF = C
Go 语言原生的 RPC 支持同步和异步两种调用方式,分别对应 Client 的 Call() 方法 和 Go() 方法。同步调用直接会返回响应值,而异步方式则返回此次调用的 Call 结构体,然后等待 Call 结构体的 Done 管道返回调用结果。
不同协议的 RPC
HTTP 协议
Go 语言中 net/rpc
包提供对通过网络或其他 i/o
连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。注册后对象的导出方法将支持远程访问,服务器可以注册不同类型的多个对象(服务) ,但是不支持注册同一类型的多个对象。
(1)定义一个 ServiceA 类型并为其定义了一个可导出的 Add() 方法。
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的 Add() 方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
(2)在任意目录下创建 server 项目文件并初始化( go mod init server
),将上面定义的 ServiceA 类型注册为一个服务,实现 HTTP RPC 服务端程序,该程序的具体代码如下:
package main
import (
"log"
"net"
"net/http"
"net/rpc"
)
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
func main() {
service := new(ServiceA)
rpc.Register(service) // 注册 RPC 服务
rpc.HandleHTTP() // 基于 HTTP 协议
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)
}
(3)在任意目录下创建 client 项目文件并初始化( go mod init client
),编写客户端程序调用 “Add” 方法的 “ServiceA” 服务,该程序的具体代码如下:
package main
import (
"fmt"
"log"
"net/rpc"
)
type Args struct {
X, Y int
}
func main() {
// 建立 HTTP 连接
client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")
if err != nil {
log.Fatal("dialing:", err)
}
// 同步调用
args := &Args{10, 20}
var reply int
err = client.Call("ServiceA.Add", args, &reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
// 异步调用
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done // 接收调用结果
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}
(4)分别编译并执行服务端和客户端程序,查看 RPC 调用的结果,输出如下的结果:
ServiceA.Add: 10+20=30
<nil>
30
该程序的 RPC 调用过程可以简化如下图所示:
TCP 协议
net/rpc
包也支持直接使用 TCP 协议 而不使用 HTTP 协议。
(1)定义一个 ServiceA 类型并为其定义了一个可导出的 Add() 方法。
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的 Add() 方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
(2)在任意目录下创建 server 项目文件并初始化( go mod init server
),将上面定义的 ServiceA 类型注册为一个服务,实现 TCP RPC 服务端程序,该程序的具体代码如下:
package main
import (
"log"
"net"
"net/rpc"
)
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的 Add() 方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
func main() {
service := new(ServiceA)
rpc.Register(service) // 注册RPC服务
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, _ := l.Accept()
rpc.ServeConn(conn)
}
}
(3)在任意目录下创建 client 项目文件并初始化( go mod init client
),编写客户端程序调用 “Add” 方法的 “ServiceA” 服务,该程序的具体代码如下:
package main
import (
"fmt"
"log"
"net/rpc"
)
type Args struct {
X, Y int
}
func main() {
// 建立 TCP 连接
client, err := rpc.Dial("tcp", "127.0.0.1:9091")
if err != nil {
log.Fatal("dialing:", err)
}
// 同步调用
args := &Args{10, 20}
var reply int
err = client.Call("ServiceA.Add", args, &reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
// 异步调用
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done // 接收调用结果
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}
(4)分别编译并执行服务端和客户端程序,查看 RPC 调用的结果,输出如下的结果:
ServiceA.Add: 10+20=30
<nil>
30
JSON 协议
net/rpc
包默认使用的是 gob 协议对传输数据进行序列化/反序列化,从其它语言调用 Go 语言实现的 RPC 服务将比较困难。在互联网的微服务时代,每个 RPC 以及服务的使用者都可能采用不同的编程语言,跨语言是互联网时代 RPC 的一个首要条件,可以通过官方自带的 net/rpc/jsonrpc
扩展实现一个跨语言的 PPC ,下面的程序使用 JSON 协议对传输数据进行序列化与反序列化。
(1)定义一个 ServiceA 类型并为其定义了一个可导出的 Add() 方法。
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的 Add() 方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
(2)在任意目录下创建 server 项目文件并初始化( go mod init server
),将上面定义的 ServiceA 类型注册为一个服务,实现 TCP RPC 服务端程序,该程序的具体代码如下:
package main
import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为 ServiceA 类型增加一个可导出的 Add() 方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
func main() {
service := new(ServiceA)
rpc.Register(service) // 注册 RPC 服务
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, _ := l.Accept()
// 使用 JSON 协议
rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
(3)在任意目录下创建 client 项目文件并初始化( go mod init client
),编写客户端程序调用 “Add” 方法的 “ServiceA” 服务,该程序的具体代码如下:
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type Args struct {
X, Y int
}
func main() {
// 调用 net.Dial() 函数建立 TCP 链接,然后基于该链接建立针对客户端的json编解码器。
conn, err := net.Dial("tcp", "127.0.0.1:9091")
if err != nil {
log.Fatal("dialing:", err)
}
// 使用 JSON 协议
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
// 同步调用
args := &Args{10, 20}
var reply int
err = client.Call("ServiceA.Add", args, &reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
// 异步调用
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done // 接收调用结果
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}
(4)分别编译并执行服务端和客户端程序,查看 RPC 调用的结果,输出如下的结果:
ServiceA.Add: 10+20=30
<nil>
30
RPC 程序分析
服务器程序代码分析
(1)调用 net/rpc
包定义了一个缺省的 Server ,所以 Server 的很多方法可以直接调用,这对于一个简单的 Server 的实现更方便,但如果需要配置不同的 Server ,比如不同的监听地址或端口,就需要自己生成 Server ,生成方式如下代码所示:
var DefaultServer = NewServer()
Server 端有多种 Socket 监听的方式,具体参考以下的方法:
func (server *Server) Accept(lis net.Listener)
func (server *Server) HandleHTTP(rpcPath, debugPath string)
func (server *Server) ServeCodec(codec ServerCodec)
func (server *Server) ServeConn(conn io.ReadWriteCloser)
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (server *Server) ServeRequest(codec ServerCodec) error
- ServeHTTP() 方法实现了处理 http 请求的业务逻辑, 它首先处理 http 的 CONNECT 请求, 接收后就 Hijacker 这个连接 conn , 然后调用 ServeConn() 方法在这个连接上处理这个客户端的请求,它其实是实现了 http.Handler 接口,一般不直接调用这个方法。
- Server.HandleHTTP() 方法设置 RPC 的上下文路径,rpc.HandleHTTP() 使用默认的上下文路径 DefaultRPCPath 、 DefaultDebugPath 。
当启动一个 http server 的时候,在 http.ListenAndServe() 方法上设置的上下文将用作 RPC 传输,这个上下文的请求会交给 ServeHTTP() 方法来处理。
以上是 RPC over http 的实现,可以看出 ·net/rpc· 只是利用 http CONNECT 建立连接,这和普通的 RESTful API 还是不一样的。
- Accept() 用来处理一个监听器,一直在监听客户端的连接,一旦监听器接收了一个连接,则还是交给 ServeConn 在另外一个 goroutine 中去处理,程序代码如下:
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)
}
}
可以看出,很重要的一个方法就是 ServeConn :
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
buf := bufio.NewWriter(conn)
srv := &gobServerCodec{
rwc: conn,
dec: gob.NewDecoder(conn),
enc: gob.NewEncoder(buf),
encBuf: buf,
}
server.ServeCodec(srv)
}
连接其实是交给一个 ServerCodec 去处理,这里默认使用 gobServerCodec 去处理,这是一个未输出默认的编解码器,可以使用其它的编解码器,ServeCodec 的实现参考以下源码:
func (server *Server) ServeCodec(codec ServerCodec) {
sending := new(sync.Mutex)
for {
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
}
go service.call(server, sending, mtype, req, argv, replyv, codec)
}
codec.Close()
}
从上述源码可知:
- 它其实一直从连接中读取请求,然后调用 go service.call 在另外的 goroutine 中处理服务调用。
- 一个 codec 实例必然和一个 connnection 相关,因为它需要从 connection 中读取 request 和发送 response 。
- Go 语言中的 RPC 官方库的消息(request 和 response)的定义就是消息头( header ) + 内容体( body )。
请求的消息头的定义如下,包括服务的名称和序列号:
type Request struct {
ServiceMethod string // format: "Service.Method"
Seq uint64 // sequence number chosen by client
// contains filtered or unexported fields
}
消息体就是传入的参数,返回的消息头的定义如下:
type Response struct {
ServiceMethod string // echoes that of the Request
Seq uint64 // echoes that of the request
Error string // error, if any.
// contains filtered or unexported fields
}
消息体是 reply 类型的序列化后的值,Server 还提供了两个注册服务的方法,其方法的声明如下:
func (server *Server) Register(rcvr interface{}) error
func (server *Server) RegisterName(name string, rcvr interface{}) error
第二个方法为服务起一个别名,否则服务名已它的类型命名,它们底层调用 register() 方法进行服务的注册,如以下的源码:
func (server *Server) register(rcvr interface{}, name string, useName bool) error
受限于 Go 语言的特点, 不可能在接到客户端的请求的时候,根据反射动态的创建一个对象,故在 Go 语言中需要预先创建一个服务 map (这是在编译的时候完成的),如以下的程序代码:
server.serviceMap = make(map[string]*service)
同时每个服务还有一个方法 map: map[string]*methodType ,通过 suitableMethods 建立,具体参考以下的程序源码:
func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType
这样 RPC 在读取请求 header ,通过查找这两个 map ,就可以得到要调用的服务及它的对应方法,方法的调用程序源码如下所示:
func (s *service) call(server *Server, sending *sync.Mutex, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
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)
}
客户端程序代码分析
客户端要建立和服务器的连接,可以有以下几种方式:
func Dial(network, address string) (*Client, error)
func DialHTTP(network, address string) (*Client, error)
func DialHTTPPath(network, address, path string) (*Client, error)
func NewClient(conn io.ReadWriteCloser) *Client
func NewClientWithCodec(codec ClientCodec) *Client
其中,DialHTTP() 和 DialHTTPPath() 函数是通过 HTTP 的方式和服务器建立连接,区别之在于是否设置上下文路径,具体参考以下的程序源码:
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,
}
}
首先发送 CONNECT 请求,如果连接成功则通过 NewClient(conn) 创建 client ,而 Dial() 函数则通过 TCP 直接连接服务器,具体参考以下的程序源码:
func Dial(network, address string) (*Client, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
return NewClient(conn), nil
}
根据服务是 over HTTP还是 over TCP 选择合适的连接方式,NewClient 则创建一个缺省 codec 为 glob 序列化库的客户端,具体参考以下的程序源码:
func NewClient(conn io.ReadWriteCloser) *Client {
encBuf := bufio.NewWriter(conn)
client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
return NewClientWithCodec(client)
}
如果用其它的序列化库,可以调用 NewClientWithCodec() 函数,其函数的声明如下:
func NewClientWithCodec(codec ClientCodec) *Client {
client := &Client{
codec: codec,
pending: make(map[uint64]*Call),
}
go client.input()
return client
}
重要的是 input 方法,它以一个死循环的方式不断地从连接中读取 response ,然后调用 map 中读取等待的 Call.Done channel 通知完成。
消息的结构和服务器一致,都是 Header + Body 的方式。
客户端的调用有两个方法: Go 和 Call , Go 方法是异步的,它返回一个 Call 指针对象, 它的 Done 是一个 channel ,如果服务返回,Done 就可以得到返回的对象(实际是 Call 对象,包含 Reply 和 error 信息);Call 方法是同步的方式调用,它实际是调用 Go 方法来实现的,具体参考以下的程序源码:
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
}
从一个 Channel 中读取对象会被阻塞住,直到有对象可以读取,这种实现很简单,异步调用的 Client.Go() 方法实现如下:
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
call.Done = make(chan *Call, 10) // buffered.
client.send(call)
return call
}
首先是构造一个表示当前调用的 call 变量,然后通过 client.send 将 call 的完整参数发送到 RPC 框架。client.send() 方法调用是线程安全的,因此可以从多个 Goroutine 同时向同一个 RPC 链接发送调用指令,当调用完成或者发生错误时,将调用 call.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().
}
}
从 Call.done() 方法的实现可以得知 call.Done 管道会将处理后的 call 结构体返回。