[golang microservices] 2. Introduction to RPC architecture and implementation of microservices through RPC

1. Introduction

After a brief understanding of the definition, advantages and disadvantages of microservices in the previous section, before using the microservice framework, you need to first understand the RPC architecture. Through RPC, you can understand the workflow of microservices more vividly
  1. The concept of RPC

RPC (Remote Procedure Call Protocol) is the abbreviation of remote procedure call . In layman's terms, it is to call a function in the distance. The corresponding one is a local function call . Let's take a look at the local function call first: When writing the following code When:
result := Add(1,2)
passes in two parameters of 1 and 2, calls an Add function in the local code, and gets the return value of result. At this time, the parameters , return value , and code segment are all in one process space, this is a local function call . Is there a way to call a function across processes (so called "remote", in a typical case, this process is deployed on another server )?
This is the main function of RPC, and it is also the main function of microservices.
  1. Getting started with RPC

One of the benefits of using microservices is:

(1). It does not limit the technology selection used by the service provider, and can realize the technology decoupling of the company's cross-team

(2). Each service is encapsulated into a process, "independent" of each other

(3). Use microservices to communicate across processes

The RPC protocol can realize direct mutual calls between different languages . In the Internet era, RPC has become an indispensable basic component just like IPC (Inter-Process Communication).

IPC: Inter-Process Communication

RPC: remote communication - application layer protocol (same layer as http protocol), the bottom layer is realized by TCP


It is very simple to implement RPC in golang. There are packaged official libraries and some third-party libraries to provide support. Go RPC can use tcp or http to transfer data, and can use various types of encoding and decoding methods for the data to be transferred . Golang's official net/rpc library uses encoding/gob for encoding and decoding , and supports tcp or http data transmission methods. Since other languages ​​​​do not support gob encoding and decoding methods , the RPC method implemented using the net/rpc library cannot be used for cross-language calls .

Golang also officially provides the net/rpc/jsonrpc library to implement the RPC method. JSON RPC uses JSON for data encoding and decoding, thus supporting cross-language calls . However, the current jsonrpc library is implemented based on the tcp protocol and does not support the use of http for data transmission for the time being. .

In addition to the official rpc library provided by golang, there are many third-party libraries that provide support for the implementation of RPC in golang. The implementation of most third-party rpc libraries uses protobuf for data encoding and decoding. According to the protobuf declaration file, the rpc method definition and Service registration code, it is very convenient to call rpc service in golang

2. Net/rpc library realizes remote call

  1. Use http as the carrier of RPC to realize remote calls ( understand )

Demonstrate how to use the official net/rpc library of golang to implement the RPC method, use http as the carrier of RPC, and monitor client connection requests through the net/http package. http is based on tcp, with one more layer of packets and several handshake checks . The performance is naturally worse than that of directly using tcp to realize network transmission , so tcp is generally used in RPC microservices

(1). Create an RPC micro server

Create a new server/main.go
package main
import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/rpc"
    "os"
)
// 定义类对象
type World struct {
}
// 绑定类方法
func (this *World) HelloWorld(req string, res *string) error {
    *res = req + " 你好!"
    return nil
    //return errors.New("未知的错误!")
}

// 绑定类方法
func (this *World) Print(req string, res *string) error {
    *res = req + " this is Print!"
    return nil
    //return errors.New("未知的错误!")
}

func main() {
    // 1. 注册RPC服务
    rpc.Register(new(World)) // 注册rpc服务
    rpc.HandleHTTP() // 采用http协议作为rpc载体
    // 2. 设置监听
    lis, err := net.Listen("tcp", "127.0.0.1:8800")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }
    fmt.Fprintf(os.Stdout, "%s", "start connection")
    // 3. 建立链接
    http.Serve(lis, nil)
}

注意:以上World结构体的方法方法必须满足Go语言的RPC规则

  • 方法只能有两个可序列化的参数,其中第二个参数是指针类型,参数的类型不能是channel(通道)、complex(复数类型)、func(函数),因为它们不能进行 序列化

  • 方法要返回一个error类型,同时必须是公开的方法

(2). 创建RPC客户端

客户端可以是 go web 也可以是一个 go应用,新建client/main.go
package main
import (
    "fmt"
    "net/rpc"
)

func main() {
    // 1. 用 rpc 链接服务器 --Dial()
    conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8800")
    if err != nil {
        fmt.Println("Dial err:", err)
        return
    }

    defer conn.Close()

    // 2. 调用远程函数
    var reply1 string // 接受返回值 --- 传出参数
    err1 := conn.Call("World.HelloWorld", "张三", &reply1)
    if err1 != nil {
        fmt.Println("Call:", err1)
        return
    }

    fmt.Println(reply1)

    var reply2 string // 接受返回值 --- 传出参数
    err2 := conn.Call("World.Print", "李四", &reply2)
    if err2 != nil {
        fmt.Println("Call:", err2)
        return
    }
    fmt.Println(reply2)
}
  1. 使用tcp作为RPC的载体实现远程调用

(1).创建RPC微服务端

新建server/main.go
package main

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

// 定义类对象
type World struct {}

// 绑定类方法
func (this *World) HelloWorld(req string, res *string) error {
    *res = req + " 你好!"
    return nil
}

func main() {
    // 1. 注册RPC服务
    err := rpc.RegisterName("hello", new(World))
    if err != nil {
        fmt.Println("注册 rpc 服务失败!", err)
        return
    }
    // 2. 设置监听
    listener, err := net.Listen("tcp", "127.0.0.1:8800")
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }

    defer listener.Close()
    fmt.Println("开始监听 ...")

    // 3. 建立链接
    for {
        //接收连接
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Accept() err:", err)
            return
        }
        // 4. 绑定服务
        go rpc.ServeConn(conn)
    }
}

注意:以上World结构体的方法方法必须满足Go语言的RPC规则

  • 方法只能有两个可序列化的参数,其中第二个参数是指针类型,参数的类型不能是channel(通道)、complex(复数类型)、func(函数),因为它们不能进行 序列化

  • 方法要返回一个error类型,同时必须是公开的方法

(2). 创建RPC客户端

新建client/main.go
package main

import (
    "fmt"
    "net/rpc"
)

func main() {
    // 1. 用 rpc 链接服务器 --Dial()
    conn, err := rpc.Dial("tcp", "127.0.0.1:8800")
    if err != nil {
        fmt.Println("Dial err:", err)
        return
    }

    defer conn.Close()

    // 2. 调用远程函数
    var reply string // 接受返回值 --- 传出参数
    err = conn.Call("hello.HelloWorld", "张三", &reply)
    if err != nil {
        fmt.Println("Call:", err)
        return
    }
    fmt.Println(reply)
}
说明:
首选是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法,在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别定义RPC方法的两个参数

三.使用tcp作为RPC的载体实现远程调用具体案例

案例1.简单使用

1.创建一个hello微服务端,编写微服务端RPC代码,完成后启动该微服务端
2.创建一个hello客户端,编写客户端RPC代码,完成后启动该客户端,访问微服务端RPC功能,并返回相关数据

(1).创建hello微服务端

创建mirco/server/hello/main.go文件,并编写代码,代码下所示:
package main

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

//rpc服务端

//定义一个远程调用的结构体,并创建一个远程调用的函数,函数一般是放在结构体中的
type  Hello struct  {

}

/*
说明:
    1、方法只能有两个可序列化的参数,其中第二个参数是指针类型
        req 表示获取客户端传过来的数据
        res 表示给客户端返回数据
    2、方法要返回一个error类型,同时必须是公开的方法
    3、req和res的类型不能是:channel(通道)、func(函数),因为以上类型均不能进行 序列化
*/
func (this Hello) SayHello(req string, res *string) error {
    fmt.Println("请求的参数:", req)
    //设置返回的数据
    *res = "你好" + req
    return nil
}

func main()  {
    //1、 注册RPC服务
    //hello: rpc服务名称
    err1 := rpc.RegisterName("hello", new(Hello))
    if err1 != nil {
        fmt.Println(err1)
    }

    //2、监听端口
    listen, err2 := net.Listen("tcp", "127.0.0.1:8080")
    if err2 != nil {
        fmt.Println(err2)
    }

    //3、应用退出的时候关闭监听端口
    defer listen.Close()

    for {  // for 循环, 一直进行连接,每个客户端都可以连接
        fmt.Println("开始创建连接")
        //4、建立连接
        conn, err3 := listen.Accept()
        if err3 != nil {
            fmt.Println(err3)
        }
        //5、绑定服务
        rpc.ServeConn(conn)
    }
}

(2).创建hello客户端

创建mirco/client/hello/main.go文件,并编写代码,代码下所示:
package main

import (
    "fmt"
    "net/rpc"
)

//rpc服务端

func main()  {
    //1、用 rpc.Dial和rpc微服务端建立连接
    conn, err1 := rpc.Dial("tcp", "127.0.0.1:8080")
    if err1 != nil {
        fmt.Println(err1)
    }
    //2、当客户端退出的时候关闭连接
    defer conn.Close()

    //3、调用远程函数
    //微服务端返回的数据
    var reply string
    /*
        1、第一个参数: hello.SayHello,hello 表示服务名称  SayHello 方法名称
        2、第二个参数: 给服务端的req传递数据
        3、第三个参数: 需要传入地址,获取微服务端返回的数据
    */
    err2 := conn.Call("hello.SayHello", "我是客户端", &reply)
    if err2 != nil {
        fmt.Println(err2)
    }
    //4、获取微服务返回的数据
    fmt.Println(reply)
}

(3).启动微服务端,以及客户端访问

启动微服务端

启动客户端

案例2.模拟实现一个goods的微服务,增加商品 获取商品功能

1.创建一个goods微服务端,编写微服务端RPC代码,增加函数: 增加商品函数,获取商品函数,完成后启动该微服务端
2.创建一个goods客户端,编写客户端RPC代码,完成后启动该客户端,访问微服务端RPC功能,并返回相关数据

(1).创建goods微服务端

创建mirco/server/goods/main.go文件,并编写代码,代码下所示:
package main

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

// goods微服务:服务端,传入struct,增加商品,获取商品

//创建远程调用的函数,函数一般是放在结构体里面
type Goods struct{}

//AddGoods参数对应的结构体
//增加商品请求参数结构体
type AddGoodsReq struct {
    Id      int
    Title   string
    Price   float32
    Content string
}
//增加商品返回结构体
type AddGoodsRes struct {
    Success bool
    Message string
}

//GetGoods参数对应的结构体
//获取商品请求结构体
type GetGoodsReq struct {
    Id int
}
//获取商品返回结构体
type GetGoodsRes struct {
    Id      int
    Title   string
    Price   float32
    Content string
}

/*
说明:
    1、方法只能有两个可序列化的参数,其中第二个参数是指针类型
        req 表示获取客户端传过来的数据
        res 表示给客户端返回数据
    2、方法要返回一个error类型,同时必须是公开的方法
    3、req和res的类型不能是:channel(通道)、func(函数),因为以上类型均不能进行 序列化
*/

//增加商品函数
func (this Goods) AddGoods(req AddGoodsReq, res *AddGoodsRes)  error {
    //1、执行增加 模拟
    fmt.Printf("%#v\n", req)
    *res = AddGoodsRes{
        Success: true, //根据增加结果,返回状态
        Message: "增加商品成功",
    }
    return nil
}

//获取商品函数
func (this Goods) GetGoods(req GetGoodsReq, res *GetGoodsRes) error {
    //1、执行获取商品 模拟
    fmt.Printf("%#v\n", req)

    //2、返回获取的结果
    *res = GetGoodsRes{
        Id:      12,  //商品id
        Title:   "服务器获取的数据",
        Price:   24.5,
        Content: "我是服务器数据库获取的内容",
    }
    return nil
}

func main()  {
    //1、 注册RPC服务
    //goods: rpc服务名称
    err1 := rpc.RegisterName("goods", new(Goods))
    if err1 != nil {
        fmt.Println(err1)
    }

    //2、监听端口
    listen, err2 := net.Listen("tcp", "127.0.0.1:8080")
    if err2 != nil {
        fmt.Println(err2)
    }
    //3、应用退出的时候关闭监听端口
    defer listen.Close()

    for {  // for 循环, 一直进行连接,每个客户端都可以连接
        fmt.Println("准备建立连接")
        //4、建立连接
        conn, err3 := listen.Accept()
        if err3 != nil {
            fmt.Println(err3)
        }
        //5、绑定服务
        rpc.ServeConn(conn)
    }
}

(2).创建goods客户端

创建mirco/client/goods/main.go文件,并编写代码,代码下所示:
package main

import (
    "fmt"
    "net/rpc"
)

//AddGoods参数对应的结构体
//增加商品请求参数结构体
type AddGoodsReq struct {
    Id      int
    Title   string
    Price   float32
    Content string
}
//增加商品返回结构体
type AddGoodsRes struct {
    Success bool
    Message string
}

//GetGoods参数对应的结构体
//获取商品请求结构体
type GetGoodsReq struct {
    Id int
}
//获取商品返回结构体
type GetGoodsRes struct {
    Id      int
    Title   string
    Price   float32
    Content string
}

func main() {
    //1、用 rpc.Dial和rpc微服务端建立连接
    conn, err1 := rpc.Dial("tcp", "127.0.0.1:8080")
    if err1 != nil {
        fmt.Println(err1)
    }
    //2、当客户端退出的时候关闭连接
    defer conn.Close()

    //3、调用远程函数
    //微服务端返回的数据
    var reply AddGoodsRes
    /*
        1、第一个参数: goods.AddGoods,goods 表示服务名称  AddGoods 方法名称
        2、第二个参数: 给服务端的req传递数据
        3、第三个参数: 需要传入地址,获取微服务端返回的数据
    */
    err2 := conn.Call("goods.AddGoods", AddGoodsReq{
        Id:      10,
        Title:   "商品标题",
        Price:   23.5,
        Content: "商品详情",
    }, &reply)

    if err2 != nil {
        fmt.Println(err1)
    }
    //4、获取微服务返回的数据
    fmt.Println("%#v\n", reply)

    // 5、 调用远程GetGoods函数
    var goodsData GetGoodsRes
    err3 := conn.Call("goods.GetGoods", GetGoodsReq{
        Id: 12,
    }, &goodsData)
    if err3 != nil {
        fmt.Println(err3)
    }
    //6、获取微服务返回的数据
    fmt.Printf("%#v", goodsData)
}

(3).启动微服务端,以及客户端访问

启动微服务端

启动客户端

四.net/rpc/jsonrpc库以及RPC跨语言

标准库的RPC默认采用Go语言特有的 gob编码没法实现跨语言调用,golang官方还提供了 net/rpc/jsonrpc库实现RPC方法,JSON RPC采 用JSON进行数据编解码,因而 支持跨语言调用, 但目前的jsonrpc库是 基于tcp协议 实现的,暂时不支持使用http进行数据传输
  1. Linux命令之nc创建tcp服务测试数据传输

nc是 netcat的简写,是一个功能强大的网络工具,有着网络界的瑞士军刀美誉,nc命令的 主要作用如下:
  • 实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口

  • 端口的扫描,nc可以作为client发起TCP或UDP连接

  • 机器之间传输文件

  • 机器之间网络测速

centos中如果找不到nc命令可以使用 yum install -y nc 安装

使用nc作为微服务server端接收客户端数据

nc -l 192.XXX.XXX.XXX 8080

nc作为微服务server端开启:

客户端请求和上面案例一致,也可以参考下面案例

上面讲解了使用 net/rpc 实现RPC的过程,但是没办法在其他语言中调用上面例子实现的RPC方法。所以接下来使用 net/rpc/jsonrpc 库实现RPC方法,此方式实现的 RPC方法支持跨语言调用
  1. 创建RPC微服务端

使用 net/rpc/jsonrpc 库实现RPC方法:
和rpc微服务端区别: 在 5. 绑定服务步骤中使用 rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
package main

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

//rpc服务端

//定义一个远程调用的结构体,并创建一个远程调用的函数,函数一般是放在结构体中的
type  Hello struct  {

}

/*
说明:
    1、方法只能有两个可序列化的参数,其中第二个参数是指针类型
        req 表示获取客户端传过来的数据
        res 表示给客户端返回数据
    2、方法要返回一个error类型,同时必须是公开的方法
    3、req和res的类型不能是:channel(通道)、func(函数),因为以上类型均不能进行 序列化
*/
func (this Hello) SayHello(req string, res *string) error {
    fmt.Println("请求的参数:", req)
    //设置返回的数据
    *res = "你好" + req
    return nil
}

func main()  {
    //1、 注册RPC服务
    //hello: rpc服务名称
    err1 := rpc.RegisterName("hello", new(Hello))
    if err1 != nil {
        fmt.Println(err1)
    }

    //2、监听端口
    listen, err2 := net.Listen("tcp", "127.0.0.1:8080")
    if err2 != nil {
        fmt.Println(err2)
    }

    //3、应用退出的时候关闭监听端口
    defer listen.Close()

    for {  // for 循环, 一直进行连接,每个客户端都可以连接
        fmt.Println("开始创建连接")
        //4、建立连接
        conn, err3 := listen.Accept()
        if err3 != nil {
            fmt.Println(err3)
        }
        //5、绑定服务
        //rpc.ServeConn(conn)

        // 5. 绑定服务
        /*
           jsonrpc和默认rpc的区别:
                   以前rpc.ServeConn(conn)绑定服务
                   jsonrpc中通过rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
        */
        rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}
代码中最大的变化是用 rpc.ServeCodec函数替代了 rpc.ServeConn函数,传入的参数是针对服务端的json编解码器
  1. 创建RPC客户端

package main

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

//rpc服务端

/*
把默认的rpc 改为jsonrpc
    1、rpc.Dial需要调换成net.Dial
    2、增加建立基于json编解码的rpc服务  client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    3、conn.Call 需要改为client.Call
*/

func main()  {
    //1、用 net.Dial和rpc微服务端建立连接
    conn, err1 := net.Dial("tcp", "127.0.0.1:8080")
    if err1 != nil {
        fmt.Println(err1)
    }
    //2、当客户端退出的时候关闭连接
    defer conn.Close()

    //建立基于json编解码的rpc服务
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))


    //3、调用远程函数
    //微服务端返回的数据
    var reply string
    /*
        1、第一个参数: hello.SayHello,hello 表示服务名称  SayHello 方法名称
        2、第二个参数: 给服务端的req传递数据
        3、第三个参数: 需要传入地址,获取微服务端返回的数据
    */
    err2 := client.Call("hello.SayHello", "张三", &reply)
    if err2 != nil {
        fmt.Println(err2)
    }
    //4、获取微服务返回的数据
    fmt.Println(reply)
}
先手工调用 net.Dial函数建立TCP链接,然后基于该链接建立针对客户端的json编解码器
  1. 启动微服务端,以及客户端访问

启动微服务端

启动客户端

  1. RPC跨语言

以PHP跨语言调用RPC微服务为案例

PHP代码

<?php
class JsonRPC {
    private $conn;

    function __construct($host, $port) {
        $this->conn = fsockopen($host, $port, $errno, $errstr, 3);
        if (!$this->conn) {
            return false;
        }
    }

    public function Call($method, $params) {
        if (!$this->conn) {
            return false;
        }
        $err = fwrite($this->conn, json_encode(array(
                'method' => $method,
                'params' => array($params),
                'id' => 0,
            ))."\n");
        if ($err === false) {
            return false;
        }
        stream_set_timeout($this->conn, 0, 3000);
        $line = fgets($this->conn);
        if ($line === false) {
            return NULL;
        }
        return json_decode($line,true);
        }
    }
    $client = new JsonRPC("127.0.0.1", 8080);
    $args = "this is php aaa";
    $r = $client->Call("Hello.SayHello", $args);
    print_r($r);
?>

服务端启动和上面微服务端启动一致,php端访问,结果如下:

  1. RPC协议封装

后期使用微服务框架 GRPC Go-Micro的时候,都是使用 框架封装好的服务和客户端,接下来通过一个简单的示例演示一下 如何封装,以此来理解 封装的原理,上面的代码服务名都是写死的,不够灵活(容易写错),这里对RPC的服务端和客户端再次进行一次封装,来 屏蔽掉服务名,具体代码如下:

服务端封装

新建server/models/tools
package models

import "net/rpc"

var serverName = "HelloService"

type RPCInterface interface {
    HelloWorld(string, *string) error
}

// 调用该方法时, 需要给 i 传参, 参数应该是 实现了 HelloWorld 方法的类对象!
func RegisterService(i RPCInterface) {
    rpc.RegisterName(serverName, i)
}

封装之后的服务端实现如下:

package main

import (
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "server/models"
)
// 定义类对象
type World struct {}

// 绑定类方法
func (this *World) HelloWorld(req string, res *string) error {
    fmt.Println(req)
    *res = req + " 你好!"
    return nil
    //return errors.New("未知的错误!")
}

func main() {
    //注册rpc服务 维护一个hash表,key值是服务名称,value值是服务的地址
    // rpc.RegisterName("HelloService", new(World))
    models.RegisterService(new(World))
    //设置监听
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    for {
        //接收连接
        conn, err := listener.Accept()
        if err != nil {
            panic(err)
        }
        //给当前连接提供针对json格式的rpc服务
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

客户端封装

新建client/models/tools
package models

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

var serverName = "HelloService"

type RPCClient struct {
    Client *rpc.Client
    Conn net.Conn
}

func NewRpcClient(addr string) RPCClient {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        fmt.Println("链接服务器失败")
        return RPCClient{}
    }
    //套接字和rpc服务绑定
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    return RPCClient{Client: client, Conn: conn}
}

func (this *RPCClient) CallFunc(req string, resp *string) error {
    return this.Client.Call(serverName+".HelloWorld", req, resp)
}

封装之后客户端实现

package main

import (
    "client/models"
    "fmt"
)

func main() {
    //建立tcp连接
    client := models.NewRpcClient("127.0.0.1:8080")
    //关闭连接
    defer client.Conn.Close()

    var reply string // 接受返回值 --- 传出参数
    err := client.CallFunc("this is client", &reply)

    if err != nil {
        fmt.Println("Call:", err)
        return
    }
    fmt.Println(reply)
}

[上一节][golang 微服务] 1.单体式架构以及微服务架构介绍

[下一节][golang 微服务] 3. ProtoBuf认识与使用

Guess you like

Origin blog.csdn.net/zhoupenghui168/article/details/130898854