在上一节简单了解了微服务定义和优缺点之后,在使用微服务框架之前,需要首先了解一下RPC架构,通过RPC可以更形象了解微服务的工作流程
RPC(Remote Procedure Call Protocol),是 远程过程调用的缩写,通俗的说就是 调用远处的一个函数,与之相对应的是 本地函数调用,先来看一下本地函数调用:当写下如下代码的时候:
result := Add(1,2)
传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值,这时 参数, 返回值, 代码段都在一个进程空间内,这是 本地函数调用。 那有没有办法,能够调用一个 跨进程 (所以叫"远程",典型的事例,这个进程部署在 另一台服务器 上)的函数呢?
这就是 RPC主要实现的功能,也是 微服务的主要功能
使用微服务化的一个好处就是:
(1).不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦
(2).每个服务都被封装成进程,彼此"独立"
(3).使用微服务可以跨进程通信
RPC协议可以实现不同语言的直接相互调用,在互联网时代, RPC已经和 IPC(进程间通信)一样成为一个不可或缺的基础构件
IPC: 进程间通信
RPC:远程进通信 —— 应用层协议(http协议同层),底层使用 TCP 实现
在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持,Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。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服务调用
演示如何使用golang官方的 net/rpc 库实现RPC方法,使用 http 作为RPC的载体,通过 net/http 包监听客户端连接请求。http基于tcp, 多一层封包和几次握手校验, 性能自然比直接用tcp实现网络传输要 差一些,所以RPC微服务中一般使用的都是tcp
新建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类型,同时必须是公开的方法
客户端可以是 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)
}
新建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类型,同时必须是公开的方法
新建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方法的两个参数
1.创建一个hello微服务端,编写微服务端RPC代码,完成后启动该微服务端
2.创建一个hello客户端,编写客户端RPC代码,完成后启动该客户端,访问微服务端RPC功能,并返回相关数据
创建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)
}
}
创建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)
}
1.创建一个goods微服务端,编写微服务端RPC代码,增加函数: 增加商品函数,获取商品函数,完成后启动该微服务端
2.创建一个goods客户端,编写客户端RPC代码,完成后启动该客户端,访问微服务端RPC功能,并返回相关数据
创建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)
}
}
创建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)
}
标准库的RPC默认采用Go语言特有的 gob编码, 没法实现跨语言调用,golang官方还提供了 net/rpc/jsonrpc库实现RPC方法,JSON RPC采 用JSON进行数据编解码,因而 支持跨语言调用, 但目前的jsonrpc库是 基于tcp协议 实现的,暂时不支持使用http进行数据传输
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方法支持跨语言调用
使用 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编解码器
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编解码器
以PHP跨语言调用RPC微服务为案例
PHP代码
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端访问,结果如下:
后期使用微服务框架 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认识与使用