微服务架构攀登之路(二)之RPC

℡╲_俬逩灬. 提交于 2019-11-25 21:58:26

1. RPC 简介

⚫  远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议

⚫  该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程

⚫  如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用

2. 流行 RPC 框架的对比

3. golang 中如何实现 RPC

⚫  golang 中实现 RPC 非常简单,官方提供了封装好的库,还有一些第三方的库

⚫  golang 官方的 net/rpc 库使用 encoding/gob 进行编解码,支持 tcp 和 http 数据传输方式,由于其他语言不支持 gob 编解码方式,所以 golang 的 RPC 只支持 golang 开发的服务器与客户端之间的交互

⚫  官方还提供了net/rpc/jsonrpc 库实现RPC 方法,jsonrpc 采用JSON 进行数据编解码,因而支持跨语言调用,目前 jsonrpc 库是基于 tcp 协议实现的,暂不支持 http 传输方式

⚫  golang 的 RPC 必须符合 4 个条件才可以

◼         结构体字段首字母要大写,要跨域访问,所以大写

◼         函数名必须首字母大写(可以序列号导出的)

◼         函数第一个参数是接收参数,第二个参数是返回给客户端参数,必须是指针类型

◼         函数必须有一个返回值 error

⚫ 例题:golang 实现 RPC 程序,实现求矩形面积和周长

   server端

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/rpc"
)

// 服务端,求矩形面积和周长

// 声明矩形对象
type Rect struct {
}

// 声明参数结构体,字段首字母大写
type Params struct {
	// 长和宽
	Width, Height int
}

// 定义求矩形面积的方法
func (r *Rect) Area(p Params, ret *int) error {
	*ret = p.Width * p.Height
	return nil
}

//定义求矩形周长的方法
func (r *Rect) Perimeter(p Params, ret *int) error {
	*ret = (p.Width + p.Height) * 2
	return nil
}

func main() {
	// 1. 注册服务
	rect := new(Rect)
	rpc.Register(rect)
	// 2.把服务处理绑定到http协议上
	rpc.HandleHTTP()
	fmt.Println("------ rpc service is already on ------")
	// 3. 监听服务,等待客户端调用求周长和面积的方法
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}
}

  客户端

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

type Params struct {
	Width, Height int
}

// 调用服务
func main(){
	// 1. 连接远程RPC服务
	rp, err := rpc.DialHTTP("tcp","182.254.179.186:8080")
	if err != nil {
		log.Fatal(err)
	}
	// 2.调用远程方法
	// 定义接收服务器传回来的计算结果的值
	ret := 0
	err2 := rp.Call("Rect.Area",Params{50,100},&ret)
	if err2!=nil {
		log.Fatal(err2)
	}
	fmt.Println("面积",ret)

	//求周长
	err3:= rp.Call("Rect.Perimeter",Params{50,100},&ret)
	if err3!=nil {
		log.Fatal(err3)
	}
	fmt.Println("周长",ret)

}

⚫ 练习:模仿前面例题,自己实现 RPC 程序,服务端接收 2 个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下:

package main

import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "net/rpc"
)

type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
    A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
    //乘积
    Pro int
    // 商
    Quo int
    // 余数
    Rem int
}
//乘法
func (this *Arith)Multiply(req ArithRequest,res *ArithResponse)error {
    res.Pro = req.A * req.B
    return nil
}
// 商和余数
func (this *Arith)Divide(req *ArithRequest, res *ArithResponse)error{
    if req.B == 0 {
        return errors.New("出书不能为0")
    }
    //商
    res.Quo = req.A / req.B
    //余数
    res.Rem = req.A % req.B
    return nil
}

func main()  {
    //注册服务
    rpc.Register(new(Arith))
    // 采用http作为rpc载体
    rpc.HandleHTTP()
    fmt.Println("------ rpc service is already on ------")
    // 监听服务,等待客户端调用响应的方法
    err := http.ListenAndServe(":8081",nil)
    if err!=nil {
        log.Fatal(err)
    }
}
server.go
package main

import (
    "fmt"
    "log"
    "net/rpc"
)

type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
    A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
    //乘积
    Pro int
    // 商
    Quo int
    // 余数
    Rem int
}

func main()  {
    conn, err := rpc.DialHTTP("tcp", "182.254.179.186:8081")
    //conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8081")
    if err!= nil{
        log.Fatal(err)
    }
    req := ArithRequest{9, 2}
    var res ArithResponse
    err2 := conn.Call("Arith.Multiply",req, &res)
    if err2 != nil {
        log.Println(err2)
    }
    fmt.Printf("%d * %d = %d\n", req.A,req.B,res.Pro)
    err3 := conn.Call("Arith.Divide",req, &res)
    if err3 != nil {
        log.Println(err3)
    }
    fmt.Printf("%d / %d = %d,%d", req.A,req.B,res.Quo,res.Rem)
}
client.go

⚫ 另外,net/rpc/jsonrpc 库通过 json 格式编解码,支持跨语言调用

package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

type Arith struct {
}

// 声明接收的参数结构体
type ArithRequest struct {
    A, B int
}

// 声明返回客户端的参数结构体
type ArithResponse struct {
    //乘积
    Pro int
    // 商
    Quo int
    // 余数
    Rem int
}

//乘法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 商和余数
func (this *Arith) Divide(req *ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("出书不能为0")
    }
    //商
    res.Quo = req.A / req.B
    //余数
    res.Rem = req.A % req.B
    return nil
}

func main() {
    //注册服务
    rpc.Register(new(Arith))
    // 采用http作为rpc载体
    fmt.Println("------ jsonrpc service is already on ------")
    // 监听服务,等待客户端调用响应的方法
    listener, err := net.Listen("tcp","127.0.0.1:8081")
    if err != nil {
        log.Fatal(err)
    }
    //循环监听服务
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        //协程
        go func(conn net.Conn) {
            fmt.Println("a new client visit -----")
            jsonrpc.ServeConn(conn)
        }(conn)
    }
}
server.go
package main

import (
    "fmt"
    "log"
    "net/rpc/jsonrpc"
)

type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
    A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
    //乘积
    Pro int
    // 商
    Quo int
    // 余数
    Rem int
}

func main()  {
    conn, err := jsonrpc.Dial("tcp", "182.254.179.186:8081")
    //conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8081")
    if err!= nil{
        log.Fatal(err)
    }
    req := ArithRequest{9, 2}
    var res ArithResponse
    err2 := conn.Call("Arith.Multiply",req, &res)
    if err2 != nil {
        log.Println(err2)
    }
    fmt.Printf("%d * %d = %d\n", req.A,req.B,res.Pro)
    err3 := conn.Call("Arith.Divide",req, &res)
    if err3 != nil {
        log.Println(err3)
    }
    fmt.Printf("%d / %d = %d,%d", req.A,req.B,res.Quo,res.Rem)
client.go

4. RPC 调用流程

⚫  微服务架构下数据交互一般是对内 RPC,对外 REST

⚫   将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC  框架的使用、后期的服务监控等工作

⚫   一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用

5. 网络传输数据格式

⚫  成熟的 RPC 框架会有自定义传输协议,这里网络传输格式定义如下,前面是固定长度消息头,后面是变长消息体

 

6. 实现 RPC 服务端

⚫  服务端接收到的数据需要包括什么?

◼  调用的函数名、参数列表

◼  一般会约定函数的第二个返回值是 error 类型

◼  通过反射实现

⚫ 服务端需要解决的问题是什么?

◼  Client 调用时只传过来函数名,需要维护函数名到函数之间的 map 映射

⚫  服务端的核心功能有哪些?

◼  维护函数名到函数反射值的 map

◼  client 端传函数名、参数列表后,服务端要解析为反射值,调用执行

◼  函数的返回值打包,并通过网络返回给客户端

7. 实现 RPC 客户端

⚫ 客户端只有函数原型,使用reflect.MakeFunc()  可以完成原型到函数的调用

⚫  reflect.MakeFunc()是 Client 从函数原型到网络调用的关键

8. 实现 RPC 通信测试

⚫ 给服务端注册一个查询用户的方法,客户端去 RPC 调用

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!