什么是RPC服务
RPC,就是远程过程调用,是分布式系统中不同节点调用的方式(进程间通信),属于 C/S 模式。 RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果返回给客户端。
RPC 的核心有两个: 通信协议和序列化。在 HTTP2.0之前,一般采用自定义 TCP 协议的方式进行通信,HTTP2.0出来后,也有采用该协议的, 比如流行的 gRPC。 序列化和反序列化是一种把传输的内容编码和解码的方式,常见的编解码方式有 JSON、Protobuf等。
在大多数的RPC架构设计中,都有Client、Client Stub、Servere、Server Stub这四个组件,Client 和 Server 之间通过 Socket 进行通信。RPC 架构设计如图:
RPC 调用的流程:
1. 客户端(Client) 调用客户端存根(Client Stub) ,同时把参数传给客户端存根;
1. 客户端存根将参数打包编码,并通过系统调用发送到服务器;
1. 客户端本地系统发送消息到服务器;
1. 服务器系统将信息发送到服务器存根(Server Stub);
1. 服务端存根解析信息,也就是解码;
1. 服务端存根调用真正的服务端程序(Server);
1. 服务端(Server) 处理后,通过同样的方式,把结果返回给客户端(Client)。
RPC 调用常用于大型项目,即我们常说的微服务,而且还有包含服务注册、治理、监控等功能,是一套完整的体系。
Go 语言 RPC 简单入门
RPC很流行,Go语言当然也会支持,在 Go SDK 中,已经内置 net/rpc包来帮助开发者实现RPC。简单来说,net/rpc 包提供了通过网络访问服务端对象方法的能力。演示一个服务端代码:
package server
type MathServer struct{
}
type Args struct {
A, B int
}
func (m *MathServer) Add(args Args, reply *int) error {
*reply = args.A + args.B
return nil
}
// 示例代码中:
// 定义 MathServer,用于表示一个远程服务对象
// Args 结构体用于表示参数
// Add 这个方法实现了加法功能,加法结果通过 reply 这个指针变量返回
有了定义好的服务对象,就可以把它注册到暴露的服务列表中,以供其他客户端使用了。在Go 语言中,要注册一个RPC服务对象还是很简单的,通过 RegisterName 方法即可,示例如下:
package main
import (
"gotest/server"
"log"
"net"
"net/rpc"
)
func main() {
rpc.RegisterName("MathService", new(server.MathService))
l, e := net.Listen("tcp", ":1111")
if e != nil {
log.Fatal("listen er")
}
rpc.Accept(l)
}
// 示例中, 通过 RegisterName 函数注册了一个服务对象,该函数接收两个参数:
// 服务名称(MathService)
// 具体服务对象,即刚定义的MathService 结构体
// 然后通过 net.Listen 函数简历一个 TCP 连接,在1111端口监听,最后通过rpc.Accept函数在该 TCP 链接上提供 MathService 这个 RPC服务。现在服务端可以看到MathService 这个服务以及它的Add方法了。
任何一个框架都有自己的规则, net/rpc 这个 Go语言提供的RPC框架也不例外。要想把一个对象注册为 RPC 服务,可以让 客户端远程访问,那么该对象(类型)的方法必须满足下条件:
- 方法的类型是可导出的(公开的);
- 方法本身也是可导出的;
- 方法必须有两个参数,并且参数类型是可导出或內建的;
- 方法必须返回一个 error 类型
func (t *T) MethName(argType T1, replyType *T2) error
T1、T2都是可以被encoding/gob 序列化的。
- argType 是调用者(客户端)提供的
- replyType 是返回给调用者的结果,必须是指针类型
有了提供好的RPC服务,这样客户端就可以调用了,示例代码如下:
package main
import (
"fmt"
"gotest/server"
"lgo"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1111")
if err != nil {
log.Fatal("dialing:", err)
}
args := server.Args{A:1, B:2}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error:", err)
}
fmt.Println("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
// 示例中,首先通过rpc.Dial 函数建立TCP链接, 需要注意的是 IP、 端口要和 RPC 服务提供的一致,确保可以建立TPC链接。
// TCP链接建立成功后,就需要准备远程方法需要的参数,即args和reply。参数准备好后,可以通过Call方法调用远程的RPC服务了。Call有三个参数,它们的作用分别介绍:
// 1 调用的远程方法的名字,MathService.Add, 点前面的是注册的服务名称, 点后面的是该服务的方法
// 2 客户端为了调用远程方法提供的参数,这里是 args
// 3 为了接收远程方法返回的结果,必须是一个指针,这样客户端就可以获得服务端返回的结果了
// 让我们来运行一下,看看效果
go run gotest/server_main.go
go run gotest/client_main.go
基于HTTP 的RPC
RPC除了可以通过TCP协议调用之外,还可以通过HTTP协议进行通信,而且内置的 net/rpc 包已经支持,修改上述示例,支持HTTP协议。
// service
fun main() {
rpc.RegisterName("MathService", new(service.MathService))
rpc.HandleHTTP()
l,e := net.Listen("tcp", ":1111")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)
}
// client
func main() {
client, err := rpc.DialHTTP("tcp", "localhost:1111")
}
// 客户端调用只需要把建立链接的方法从 Dial 换成 DialHTTP 即可。
此外,Go语言的 net/rpc 包提供的 HTTP协议的 RPC还有一个调试的URL,运行服务端代码后,在浏览器舒服 http://localhost:1111/debug/rpc 回车,即可看到服务端注册的RPC服务,以及每个服务的方法,如图
JSON RPC 跨平台通信
上面的RPC示例服务是基于gob编码的,这种编码在跨语言调用的时候比较困难,当前在微服务架构中,RPC 服务的实现者和调用者都可能是不同的编程语言,因为我们实现的RPC服务要支持多语言的调用。
基于 TCP 的 JSON RPC
实现跨语言 RPC 服务的核心在于选择一个通用的编码,这样大多数语言都支持,比如常用的JSON。在Go语言中,实现一个 JSON RPC服务非常简单,只需要 net/rpc/jsonrpc包即可。我们改造一下上述示例,支持 JSON的RPC服务,如下:
// service
func main() {
rpc.RegisterName("MathService", new(server.MathService))
l, e := net.Listen("tcp", ":1111")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, e := l.Accept()
if e != nil {
log.Println("jsonrpc,Server: accept:", err.Error())
return
}
// json rpc
go jsonrpc.ServeConn(conn)
}
}
// 相比gob编码的RPC服务, JSON的RPC服务把链接建立交到 jsonrpc.ServeConn 函数处理,达到了基于JSON进行RPC调用的目的
// client
func main(){
client, err := jsonrpc.Dial("tcp", "localhost:1111")
}
// 只需要把建立链接的Dial方法换成 jsonrpc 包中的即可。
其他编程语言只需要遵守JSON-RPC 规范 即可。
基于HTTP的JSON RPC
相比基于 TCP 调用的RPC来说,使用 HTTP 会更方便,也更通用。Go语言内置的 jsonrpc 并没有实现基于 HTTP的传输,所以要自己实现,我们参考 gob 编码的HTTP RPC实现方式,来实现基于HTTP 的JSON RPC 服务。
// service
func main () {
rpc.RegisterName("MathService", new(service.MathService))
// 注册一个path,用于提供基于http的json rpc 服务
http.HanleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Sequest) {
conn, err := rw.(http.Hijacker).Hijack()
if err != nil {
log.Print("rpc hijacking", r.RemoteAddr, ":", err.Error())
return
}
var connected = "200 Connected to JSON RPC"
io.WriteString(conn, "HTTP/1.0 " + connected+"\n\n")
jsonrpc.ServeConn(conn)
})
l, e := net.Listen("tcp", ":1111")
if e != nil {
log.Fatal("listen error: ", err)
}
http.Serve(l, nil)
}
// 以上代码的实现基于HTTP协议的核心,即使用 http.HandleFunc 注册一个path,对外提供基于HTTP 的JSON RPC服务。 在这个HTTP服务的实现中,通过 HiJack 方法劫持链接,然后转交给jsonrpc处理,这样就实现了基于 HTTP 协议的 JSON RPC服务。
// client
func main () {
client, err := DialHTTP("tcp", "localhost:1111")
if err != nil {
log.Fatal("dialing: ", err)
}
args := server.Args{a:1, B:1}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error:", err)
}
fmt.Printf("MathService.Add: %d+%d= %d", args.A, args.B, reply)
}
// DialHTTP connects to an HTTP RPC server at the specified network address listening on the default HTTP RPC path.
func DialHTTP(network, address string) (*rpc.Client, error) {
return DialHTTPPath(network, address, rpc.DefaultRPCPath)
}
// DialHTTPPath connects to an HTTP RPC server at the specified network address and path.
func DialHTTPPath(network, address, path string) (*rpc.Client, error) {
var err error
conn, err := net.Dial(network, address)
if err != nil {
retun nil, err
}
io.WriteString(conn, "GET" + 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: "GET"})
connected := "200 Connected to JSON RPC"
if err == nil && resp.Status == connected {
return jsonrpc.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,
}
}
// 这段代码的核心在于通过建立好的TCP链接,发送HTTP请求调用远程的HTTP RPC服务,这里使用 HTTP GET方法。
总结
我们了解了基于Go语言自带的RPC框架,学习了RPC服务的实现及调用。也了解了基于TCP 和 HTTP实现的RPC服务有什么不同,它们是如何实现的。实际开发中,使用Go语言原生的RPC框架并不多,大多数是第三方框架,比如 Google 的 gRPC框架,它是通过 Protobuf 序列化的,是基于 HTTP/2协议的二进制传输,并且支持很多编程语言,效率也比较高。