写点什么

Go 语言:如何通过 RPC 来实现跨平台服务!

用户头像
微客鸟窝
关注
发布于: 1 小时前
Go语言:如何通过 RPC 来实现跨平台服务!

什么是 RPC 服务

RPC(Remote Procedure Call)远程过程调用,是在分布式系统中,不同节点之间的一种调用方式,可以理解为,在 A 服务器上,调用 B 服务器上应用提供的函数/方法,RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果再返回给客户端。


RPC 的核心点有两个:通信协议序列化。序列化和反序列化是一种把传输数据进行编码和解码的方式,常见的编解码方式有 JSON、Protobuf 等。


RPC 调用的流程:(图片来自百度百科)



  • 客户端调用客户端句柄,并把参数传给客户端句柄

  • 客户端句柄将参数打包编码

  • 客户端本地系统发送信息到服务器

  • 服务器系统将信息发送到服务端句柄

  • 服务端句柄解析信息(解码)

  • 服务端句柄调用真正的服务端程序

  • 服务端处理后,通过同样的方式,再把结果返回给客户端


网络通信一般是通过 Socket 通信。

Go 语言 RPC 简单入门

在 Go SDK 中,内置了 net/rpc 包来实现 RPC。net/rpc 包提供了通过网络访问服务端对象方法的能力。


我们通过一个加法运行来展示 RPC 的调用,服务端示例:


server/math_server.go


package server
type MathService struct {}
type Args struct { A, B int}
func (m *MathService) Add(args Args, reply *int) error { *reply = args.A + args.B return nil}
复制代码


上面代码中:


  • 定义了一个 MathService,表示一个远程服务对象;

  • Args 结构体表示参数;

  • Add 方法实现了加法功能,结果通过 replay 指针变量返回。


有了这个服务对象,就可以把它注册到暴露的服务列表中,来提供其他客户端的使用。注册 RPC 服务对象,通过 RegisterName 方法即可,代码如下:


server_main.go


package main
import ( "log" "net" "net/rpc" "rpctest/server")
func main() { rpc.RegisterName("MathService", new(server.MathService)) l, err := net.Listen("tcp", ":8088") //注意 “:” 不要忘了写 if err != nil { log.Fatal("listen error", err) } rpc.Accept(l)}
复制代码


上面代码中:


  • 通过 RegisterName 函数注册了一个服务对象,该函数有两个参数:

  • 服务名称,客户端调用时所使用(MathService)

  • 服务对象,也就是 MathService 这个结构体

  • 通过 net.Listen 函数建立一个 TCP 链接,在 8088 端口进行监听 ;

  • 最后通过 rpc.Accept 函数在该 TCP 链接上提供 MathService 这个 RPC 服务。如此,客户端便可看到 MathService 这个服务以及它的 Add 方法了。


net/rpc 提供的 RPC 框架,要想把一个对象注册为 RPC 服务,可以让客户端远程访问,那么该对象(类型)的方法必须满足如下条件:


  • 方法的类型是可导出的(公开的);

  • 方法本身也是可导出的;

  • 方法必须有 2 个参数,并且参数类型是可导出或者内建的;

  • 方法必须返回一个 error 类型。


总结就是:


func (t *T) MethodName(argType T1, replyType *T2) error
复制代码


此处 T1、T2 都是可以被 encoding/gob 序列化的。


  • 第一个参数 argType 是调用者(客户端)提供的;

  • 第二个参数 replyType 是返回给调用者结果,必须是指针类型。


我们完成了 RPC 服务,接下来继续完成客户端的调用:


client_main.go


package main
import ( "fmt" "log" "net/rpc" "rpctest/server")
func main() { client, err := rpc.Dial("tcp", "localhost:8088") if err != nil { log.Fatal("dialing") } 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.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)}
复制代码


上面代码中:

  • 通过 rpc.Dial 函数建立 TCP 链接,注意的是这里的 IP、端口要和 RPC 服务提供的一致;

  • 准备远程方法需要的参数,此处为示例中的 args 和 reply;

  • 通过 Call 方法调用远程的 RPC 服务;

  • Call 方法有 3 个参数,它们的作用:

  • 调用的远程方法的名字,此处为 MathService.Add,点前面的部分是注册的服务的名称,点后面的部分是该服务的方法;

  • 客户端为了调用远程方法提供的参数,示例中是 args;

  • 为了接收远程方法返回的结果,必须是一个指针,此处为 示例中的 &replay 。


服务端和客户端我们已经完成,整体目录结构:



接下来开始运行:


  1. 先运行服务端程序,提供 RPC 服务:


go run server_main.go


  1. 再运行客户端程序,调用 RPC :


go run client_main.go


运行结果:


MathService.Add: 1+2=3
复制代码


看到如上结果,说明 RPC 调用成功!✿✿ヽ(°▽°)ノ✿

基于 HTTP 的 RPC

RPC 除了可以通过 TCP 协议调用之外,还可以通过 HTTP 协议进行调用,还是通过内置的 net/rpc 包便可调用,我们将上门的代码改成 HTTP 协议的调用,服务端代码:


server_main.go


func main() {   rpc.RegisterName("MathService", new(server.MathService))   rpc.HandleHTTP()//新增的   l, err := net.Listen("tcp", ":8088")   if err != nil {      log.Fatal("listen error:", err)   }   http.Serve(l, nil)//换成http的服务}
复制代码


客户端部分代码修改:


client_main.go


func main()  {   client, err := rpc.DialHTTP("tcp",  "localhost:8088")   //此处省略其他没有修改的代码}
复制代码


修改完成后,我们分别运行服务端和客户端,结果和 tcp 连接时是一样的。

调试的 URL

net/rpc 包提供的 HTTP 协议的 RPC 还有一个调试的 URL,运行服务端代码后,在浏览器中访问 http://localhost:8088/debug/rpc 回车,即可看到服务端注册的 RPC 服务,以及每个服务的方法,如图:



如图,注册的 RPC 服务、方法的签名、已经被调用的次数都可以看到。

JSON RPC 跨平台通信

上面实现的 RPC 服务是基于 gob 编码的,这种编码在跨语言调用的时候比较困难,但是在 RPC 服务的实现者和调用者往往都可能是不同的编程语言,因此我们实现的 RPC 服务要支持多语言的调用。

TCP 的 JSON RPC

要实现跨语言的 RPC 服务,核心在于选择一个通用的编码,比如常用的 JSON。在 Go 语言中,只需要使用 net/rpc/jsonrpc 包便可实现一个 JSON RPC 服务。


我把上面的示例改造成支持 JSON 的 RPC 服务,服务端代码如下:


server_main.go


func main() {  rpc.RegisterName("MathService", new(server.MathService))  l, err := net.Listen("tcp", ":8088")  if err != nil {    log.Fatal("listen error:", err)  }  for {    conn, err := l.Accept()    if err != nil {      log.Println("jsonrpc.Serve: accept:", err.Error())      return    }    //json rpc    go jsonrpc.ServeConn(conn)  }}
复制代码


上面的代码中,对比 gob 编码的 RPC 服务,JSON 的 RPC 服务是把链接交给了 jsonrpc.ServeConn 这个函数处理,达到了基于 JSON 进行 RPC 调用的目的。


JSON RPC 的客户端代码修改的部分如下所示:


client_main.go


func main()  {   client, err := jsonrpc.Dial("tcp",  "localhost:8088")   //省略了其他没有修改的代码}
复制代码


只需要把建立链接的 Dial 方法换成 jsonrpc 包中的即可。

HTTP 的 JSON RPC

Go 语言内置的 jsonrpc 并没有实现基于 HTTP 的传输,这里参考 gob 编码的 HTTP RPC 实现方式,来实现基于 HTTP 的 JSON RPC 服务。


server_main.go


func main() {   rpc.RegisterName("MathService", new(server.MathService))   //注册一个path,用于提供基于http的json rpc服务   http.HandleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Request) {      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, err := net.Listen("tcp", ":8088")   if err != nil {      log.Fatal("listen error:", err)   }   http.Serve(l, nil)//换成http的服务}
复制代码


上面代码实现基于 HTTP 协议的核心,使用 http.HandleFunc 注册了一个 path,对外提供基于 HTTP 的 JSON RPC 服务。在这个 HTTP 服务的实现中,通过 Hijack 方法劫持链接,然后转交给 jsonrpc 处理,这样就实现了基于 HTTP 协议的 JSON RPC 服务。


客户端调用代码:


func main()  {     client, err := DialHTTP("tcp",  "localhost:8088")     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.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 {        return 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 JSON RPC 服务,这里使用的是 HTTP GET 方法。

分别运行服务端和客户端,就可以看到正确的 HTTP JSON RPC 调用结果了。


上面我们使用的是 Go 语言自带的 RPC 框架,但实际开发中,使用它的情况并不多,比较常用的是 Google 的 gRPC 框架,它是通过 Protobuf 序列化的,基于 HTTP/2 协议的二进制传输,且支持很多编程语言,效率也比较高。我们通过 RPC 的学习,再来学习 gRPC 是很容易入门的。

发布于: 1 小时前阅读数: 4
用户头像

微客鸟窝

关注

还未添加个人签名 2019.11.01 加入

公众号《微客鸟窝》笔者,目前从事web后端开发,涉及语言PHP、golang。获得美国《时代周刊》2006年度风云人物!

评论

发布
暂无评论
Go语言:如何通过 RPC 来实现跨平台服务!