1. RPC简介

RPC是远程系统调用的缩写,通俗地讲就是调用远处的一个函数,可能是一个文件内的不同函数,也可能是一个机器上另一个进程的函数,也可能是远处机器上的函数。

RPC是分布式系统中不同节点之间的通信方式,Go的标准库也实现了一个简单的RPC。

2. RPC简单使用

首先构造一个HelloService类型,其中的Hello方法用于实现打印功能,Login实现简单的用户验证

其中RPC方法必须满足golang的RPC规则:

  • 方法只能有两个可序列化的参数,其中第二个参数是指针类型
  • 返回一个error
  • 必须是公开的方法,首字母大写
type HelloService struct {
    conn    net.Conn
    isLogin bool
}

// Hello:
func (p *HelloService) Hello(request string, reply *string) error {
    if !p.isLogin {
        return fmt.Errorf("please login")
    }
    *reply = "hello:" + request + ",from" + p.conn.RemoteAddr().String()
    return nil
}

// Login: 提供用户登录验证
func (p *HelloService) Login(request string, reply *string) error {
    if request != "user:password" {
        return fmt.Errorf("auth failed")
    }
    log.Println("login ok")
    p.isLogin = true
    return nil
}

然后我们可以将HelloService类型的对象注册为一个RPC服务:

func main() {

    // 开启监听
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }
        go func() {
            defer conn.Close()
            p := rpc.NewServer()

            // RegisterName调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数
            // 所有注册的方法会放在HelloService服务空间之下
            p.Register(&HelloService{conn: conn})

            // ServeConn函数在conn这个TCP连接上为对方提供RPC服务
            p.ServeConn(conn)
        }()
    }
}

下面是客户端请求HelloService服务的代码:

func main() {

    // 拨号RPC服务
    client, err := rpc.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal("Dail error:", err)
    }

    // 通过Call()调用RPC的具体方法
    var reply string
    err = client.Call("HelloService.Login", "user:password", &reply)
    if err != nil {
        log.Fatal(err)
    }
    err = client.Call("HelloService.Hello", "client", &reply)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply)
}

我们在终端开启server和client,看看会发生什么:

go run server.go
go run client.go

##################
hello:client,from[::1]:56769

3. 跨语言的RPC

标准库的RPC默认采用go语言特有的Gob编码,因此从其他语言调用Go语言实现的RPC服务比较困难。

go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上,我们可以将RPC架设在不同的通信协议之上。

我们利用net/rpc/jsonrpc实现一个跨语言的RPC。

func main() {
    rpc.RegisterName("HelloService", new(HelloService))
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal(err)
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        // 使用rpc.ServeCodec代替rpc.ServeConn函数
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}
func main() {
    conn, err := net.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal(err)
    }

    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

    var reply string
    err = client.Call("HelloService.Hello", "hello", &reply)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply)
}

无论采用什么样的语言,只要遵循一致的json映射结构,以同样的流程就可以实现和go语言编写的RPC服务进行通信

4. HTTP上的RPC服务

我们尝试在HTTP协议上提供jsonrpc服务

新的RPC服务其实就是一个类似于REST规范的接口,接收请求并采用相应的处理流程:

type HelloService struct{}

func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello: " + request
    return nil
}

func main() {
    rpc.RegisterName("HelloService", new(HelloService))
    http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
        var conn io.ReadWriteCloser = struct {
            io.Writer
            io.ReadCloser
        }{
            ReadCloser: r.Body,
            Writer:     w,
        }
        rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
    })

    http.ListenAndServe(":1234", nil)
}

RPC服务架设在/jsonrpc路径,在处理函数中基于http.ResponseWriter和*http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道,然后基于conn构建针对服务器端的json编码解码器,最后通过rpc.ServeRequest( )函数为每次请求处理一次RPC方法调用。

让我们启动RPC服务,并打开postman,测试我们的RPC服务:

可以清楚的看到我们POST请求的body和response信息,都是json格式的,其实这两种格式基本都是固定写法。

因为在内部都是使用类似的结构体来封装的:

type clientRequest struct{
  Method  string            `json:"method"`
  Params  []interface{} `json:"params"`
  Id          uint64          `json:"id"`
}
type serverResponse struct{
  Id *json.RawMessage `json:"id"`
  Result interface{} `json:"result"`
  Error interface{} `json:"error"`  
}