[golang 微服务] 2. RPC架构介绍以及通过RPC实现微服务

一.简介

在上一节简单了解了微服务定义和优缺点之后,在使用微服务框架之前,需要首先了解一下RPC架构,通过RPC可以更形象了解微服务的工作流程
  1. RPC的概念

RPC(Remote Procedure Call Protocol),是 远程过程调用的缩写,通俗的说就是 调用远处的一个函数,与之相对应的是 本地函数调用,先来看一下本地函数调用:当写下如下代码的时候:
result := Add(1,2)
传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值,这时 参数返回值代码段都在一个进程空间内,这是 本地函数调用那有没有办法,能够调用一个 跨进程 (所以叫"远程",典型的事例,这个进程部署在 另一台服务器 上)的函数呢?
这就是 RPC主要实现的功能,也是 微服务的主要功能
  1. RPC入门

使用微服务化的一个好处就是:

(1).不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦

(2).每个服务都被封装成进程,彼此"独立"

(3).使用微服务可以跨进程通信

RPC协议可以实现不同语言的直接相互调用,在互联网时代, RPC已经和 IPC(进程间通信)一样成为一个不可或缺的基础构件

IPC: 进程间通信

RPC:远程进通信 —— 应用层协议(http协议同层),底层使用 TCP 实现


在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持,Go RPC可以利用tcphttp来传递数据,可以对要传递的数据使用多种类型编解码方式。golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用

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

除了golang官方提供的rpc库,还有许多第三方库为在golang中实现RPC提供支持,大部分第三方rpc库的实现都是使用protobuf进行数据编解码,根据protobuf声明文件自动生成rpc方法定义与服务注册代码,在golang中可以很方便的进行rpc服务调用

二.net/rpc库实现远程调用

  1. 使用http作为RPC的载体实现远程调用了解

演示如何使用golang官方的 net/rpc 库实现RPC方法,使用 http 作为RPC的载体,通过 net/http 包监听客户端连接请求。http基于tcp, 多一层封包和几次握手校验性能自然比直接用tcp实现网络传输要 差一些,所以RPC微服务中一般使用的都是tcp

(1).创建RPC微服务端

新建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认识与使用

猜你喜欢

转载自blog.csdn.net/zhoupenghui168/article/details/130898854