乐趣区

关于golang: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 是很容易入门的。

退出移动版