一.简介
在上一节简单了解了微服务定义和优缺点之后,在使用微服务框架之前,需要首先了解一下RPC架构,通过RPC可以更形象了解微服务的工作流程
RPC的概念
RPC(Remote Procedure Call Protocol),是 远程过程调用的缩写,通俗的说就是 调用远处的一个函数,与之相对应的是 本地函数调用,先来看一下本地函数调用:当写下如下代码的时候:
result := Add(1,2)
传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值,这时 参数, 返回值, 代码段都在一个进程空间内,这是 本地函数调用。 那有没有办法,能够调用一个 跨进程 (所以叫”远程”,典型的事例,这个进程部署在 另一台服务器 上)的函数呢?
这就是 RPC主要实现的功能,也是 微服务的主要功能
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服务调用
二.net/rpc库实现远程调用
使用http作为RPC的载体实现远程调用(了解)
演示如何使用golang官方的 net/rpc 库实现RPC方法,使用 http 作为RPC的载体,通过 net/http 包监听客户端连接请求。http基于tcp, 多一层封包和几次握手校验, 性能自然比直接用tcp实现网络传输要 差一些,所以RPC微服务中一般使用的都是tcp
(1).创建RPC微服务端
新建server/main.go
package mainimport ("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 mainimport ("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)}
使用tcp作为RPC的载体实现远程调用
(1).创建RPC微服务端
新建server/main.go
package mainimport ("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 mainimport ("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 mainimport ("fmt""net""net/rpc")//rpc服务端//定义一个远程调用的结构体,并创建一个远程调用的函数,函数一般是放在结构体中的typeHello struct{}/*说明:1、方法只能有两个可序列化的参数,其中第二个参数是指针类型req 表示获取客户端传过来的数据res 表示给客户端返回数据2、方法要返回一个error类型,同时必须是公开的方法3、req和res的类型不能是:channel(通道)、func(函数),因为以上类型均不能进行 序列化*/func (this Hello) SayHello(req string, res *string) error {fmt.Println("请求的参数:", req)//设置返回的数据*res = "你好" + reqreturn 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 mainimport ("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 mainimport ("fmt""net""net/rpc")// goods微服务:服务端,传入struct,增加商品,获取商品//创建远程调用的函数,函数一般是放在结构体里面type Goods struct{}//AddGoods参数对应的结构体//增加商品请求参数结构体type AddGoodsReq struct {IdintTitle stringPrice float32Content string}//增加商品返回结构体type AddGoodsRes struct {Success boolMessage string}//GetGoods参数对应的结构体//获取商品请求结构体type GetGoodsReq struct {Id int}//获取商品返回结构体type GetGoodsRes struct {IdintTitle stringPrice float32Content 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,//商品idTitle: "服务器获取的数据",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 mainimport ("fmt""net/rpc")//AddGoods参数对应的结构体//增加商品请求参数结构体type AddGoodsReq struct {IdintTitle stringPrice float32Content string}//增加商品返回结构体type AddGoodsRes struct {Success boolMessage string}//GetGoods参数对应的结构体//获取商品请求结构体type GetGoodsReq struct {Id int}//获取商品返回结构体type GetGoodsRes struct {IdintTitle stringPrice float32Content 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 GetGoodsReserr3 := 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进行数据传输
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方法支持跨语言调用
创建RPC微服务端
使用 net/rpc/jsonrpc 库实现RPC方法:
和rpc微服务端区别: 在 5. 绑定服务步骤中使用 rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
package mainimport ("fmt""net""net/rpc""net/rpc/jsonrpc")//rpc服务端//定义一个远程调用的结构体,并创建一个远程调用的函数,函数一般是放在结构体中的typeHello struct{}/*说明:1、方法只能有两个可序列化的参数,其中第二个参数是指针类型req 表示获取客户端传过来的数据res 表示给客户端返回数据2、方法要返回一个error类型,同时必须是公开的方法3、req和res的类型不能是:channel(通道)、func(函数),因为以上类型均不能进行 序列化*/func (this Hello) SayHello(req string, res *string) error {fmt.Println("请求的参数:", req)//设置返回的数据*res = "你好" + reqreturn 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编解码器
创建RPC客户端
package mainimport ("fmt""net""net/rpc""net/rpc/jsonrpc")//rpc服务端/*把默认的rpc 改为jsonrpc1、rpc.Dial需要调换成net.Dial2、增加建立基于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编解码器
启动微服务端,以及客户端访问
启动微服务端
启动客户端
RPC跨语言
以PHP跨语言调用RPC微服务为案例
PHP代码
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端访问,结果如下:
RPC协议封装
后期使用微服务框架 GRPC 和 Go-Micro的时候,都是使用 框架封装好的服务和客户端,接下来通过一个简单的示例演示一下 如何封装,以此来理解 封装的原理,上面的代码服务名都是写死的,不够灵活(容易写错),这里对RPC的服务端和客户端再次进行一次封装,来 屏蔽掉服务名,具体代码如下:
服务端封装
新建server/models/tools
package modelsimport "net/rpc"var serverName = "HelloService"type RPCInterface interface {HelloWorld(string, *string) error}// 调用该方法时, 需要给 i 传参, 参数应该是 实现了 HelloWorld 方法的类对象!func RegisterService(i RPCInterface) {rpc.RegisterName(serverName, i)}
封装之后的服务端实现如下:
package mainimport ("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 modelsimport ("fmt""net""net/rpc""net/rpc/jsonrpc")var serverName = "HelloService"type RPCClient struct {Client *rpc.ClientConn 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 mainimport ("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认识与使用