Go的RPC微服务开发初探


观前提示:本文基于Go语言,基于语言的原生能力初步讨论了微服务的开发过程

RPC的概念

这里不想谈RPC微服务和单体服务的优缺点,网络上有很多很长的文章。这里假定读者同学对RPC有一个最基本的概念,只是分享一下我我对RPC的想法。
RPC是远程过程调用的简写(Remote Procedure Call),这个名词看起来有些抽象,我们一点点来分析。

过程调用

这里的RPC概念中的“过程调用”相信同学们并不陌生,在任何一个语言中def一个或者func一个 或者function一个函数之后,用函数名后面跟小括号,就完成了一次过程调用。而远程过程调用就是将函数定义过程和调用过程分开(这也就是远程的含义),然后利用网络协议进行数据传输。

和REST的区别

以上就是RPC最朴素的解释,而和REST的区别,也是讨论RPC时最为津津乐道的话题,个人认为,REST作为一种为url制定的标准,现在得到较为主流的使用,更注重目的,通过对HTTP的请求方式映射成对资源的增删改查的操作,尝试从数据库的视角描述操作的目的,本身并不关心背后是怎么实现的。而RPC更注重过程,尝试基于给定的函数名和函数参数,完成函数的调用过程。当然这是它们从设计的角度来说最主要的一个区别,从这个区别的描述中,我们也可以看出以下两个区别。

  • 网络协议
    • REST是url(http协议)的一种标准
    • RPC并不强调协议,事实上有很多种协议方式
  • 序列化与反序列化
    • REST是基于http协议,自然跟着http协议的序列化与反序列化的方式走
    • RPC也有很多协议方式,可以自定义,如Go原生的Gob、gRPC的Protobuf、包括比较古老的json和xml都可以实现

RPC的核心概念

三个核心概念:通过以上的讨论,我们可以发现RPC最重要的就是实现一个远程的【过程调用】,也就是调用方在不感知网络协议和序列化和反序列化方式的情况下,可以根据函数名和参数进行过程调用,就像在同一个文件中定义了这个函数那样进行调用。那么我们就很容易总结出RPC中比较关键的几个方面。

  • 传输中的网络协议
  • 传输中的序列化和反序列化的方式
  • 根据函数名在目标的地方找到对应的函数

我们也可以换一种方式理解这三个概念,想象一下【调用方】需要远方的一箱货,【被调用方】如何将这一箱货物给到【调用方】呢

  1. 首先需要一辆车能够按照指定路线到达(网络协议)
  2. 其次货物能够被装起来,也能够从箱子里面拿出来(序列化和反序列化)
  3. 最后也是最重要的就是【被调用方】能根据【调用方】的简单描述,知道是哪一箱货物(根据函数名在服务器端找到对应的函数)

四个核心过程:对于客户端来说比较容易,以下我们只讨论对于服务端来说(也就是定义函数的地方)的过程,主要有四个:【定义函数】,【定义服务端】,【绑定函数】,【启动服务】。听起来有点抽象是不是,别着急,让我们用刚才【一箱货物】来举例:

  • 备货(定义函数)
  • 备车(定义服务端)
  • 装货(绑定函数)
  • 发车(启动服务)

其他讨论:在远程调用中由于网络的不确定性和传输较慢的影响,还需要考虑:失败时的重试,积压时的限流,高负载时的负载均衡等问题。本篇文章我们不做过多讨论。

极简RPC的实现

在具体代码实现这里,主要用了net/rpc包和net包,前者实现了【网络协议】,后者实现了【根据函数名在远程定位函数】,至于序列化方式,可以用rpc包实现,也可以通过其他方式实现

Server

import "net"       // 提供网络协议
import "net/rpc"   // 根据函数名在远程定位函数、序列化方法(Gob)

type Hello struct{}
func (ho *Hello) Call (req string, resp *string){  // 1. 定义函数(备货)
	*resp = "RPC Hello " + req
	return nil
}

func main(){
     listener, _ := net.Listen("tcp", ":8888")    // 2. 实例化server(备车)
     _ = rpc.RegisterName("Hello", &Hello{})      // 3. 绑定函数(装货)   Struct需要实现的方法  request string reply *string
     for {
     	conn, _ := listener.Accept()              // 4. 启动服务(发车)
     	rpc.ServeConn(conn)                       // Gob的反序列化
     }

}

以上过程解释两个地方,一个是函数定义时接收的参数,必须是2个请求 string,响应 *string,比较好理解,因为在Go中接收响应的方法是通过传递指针,让函数内部改变外部变量指针指向的位置。这是Go中很典型的操作(json的反序列化或者接收命令行输入)个人理解,这种直接该地址的方式要比返回值的方式效率高。另一个是序列化和反序列化的方式,因为我们没有额外指定这个方式,所以用的是Go中默认的方式:Gob

Client

import "net/rpc"   // 映射方法、Gob的序列化
import "fmt"

func main(){
    client, _ := rpc.Dial("tcp", ":8888")     // rpc的拨号就是Gob的编码
    var resp string
    _ = client.Call("Hello.Call", "World", &resp)  // 通过传递地址进行修改
    fmt.Println(resp)
}

client端相对简单,就不多说了,此时输出,“RPC Hello World”

RPC改造

1. 换方式为json

Server端

刚才我们提到此时RPC利用的序列化和反序列化的方式是 Gob,这是一种Go特有的方式,但是这样,其他的语言就无法调用了,下面我们手动换一下方式,比如json。

import "net"
import "net/rpc"

type Hello struct{}
func (ho *Hello) Call (request string, reply *string){
	*reply = "RPC Hello " + request
	return nil
}

func main(){
     listener, _ := net.Listen("tcp", ":8888") 
     _ = rpc.RegisterName("Hello", &Hello{}) 
     conn, _ := listener.Accept()
     rpc.ServeCodec(jsonrpc.NewServerCodec(conn))  // json的反序列化
}

以上,我们只改了最后一行,在启动服务之后追加了json的反序列化的方式

Client端

import "net/rpc"
import "fmt"

func main(){
    conn, _ := net.Dial("tcp", ":8888")     // rpc的拨号就是Gob的编码,此时改用普通的拨号
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) // json的序列化
    var resp string
    _ := client.Call("Hello.Call", "World", &resp)  // 通过传递地址进行修改
    fmt.Println(resp)
}

此时client端就以json的格式发送数据,那到底是什么样的数据呢,别的语言能否发送数据呢。答案当然是肯定的,格式如下

{"method": "Hello.Call", "params": ["hello"], "id": 0}

明显这种序列化方式效率不高,真正有用的信息,还不到一半,但胜在人类可读,至于想验证其他语言能否调用这个服务端的小伙伴可以自己试一下,

注意:此时的网络协议是TCP而不是HTTP,需要用到socket编程

2. 换协议为http

刚才提到协议是TCP,我们可以使用HTTP吗

Server端

import "net/http"
import "net/rpc"

type Hello struct{}
func (ho *Hello) Call (request string, reply *string){
	*reply = "RPC Hello " + request
	return nil
}

func main(){
     //listener, _ := net.Listen("tcp", ":8888") 
     _ = rpc.RegisterName("Hello", &Hello{})             // 绑定handler
     http.HandleFunc("/httpjsonrpc", func(w http.ResponseWriter, r *http.Request){
     	var conn io.ReadWriteCloser = struct {           // 实例化server
     		io.Writer
     		io.ReadCloser
     	}{
			ReadCloser: r.Body, 
			Writer: w,
		}
		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))   // 序列化的方式
     })
     //conn, _ := listener.Accept()
     http.ListenAndServe("8888", nil)                    // 启动服务
}

这次代码的改动有些大,这是因为http.HandleFunc需要将一个url绑定到一个处理函数上,这就要求在这个处理函数中做更多的东西。不过还是可以看出,之前分析的那几个步骤还在。而此时的请求就是一个标准的http的请求了,client端也就是标准的http的请求的写法,也就不再赘述了

总结

本文由浅入深的介绍了RPC最重要的三部分组成,请求到函数的映射,网络协议和序列化/反序列化的方式,并且基于Go原生的能力实现了三版RPC的服务端和客户端,帮助读者同学理解RPC调用

深挖的话,还有很多不足,比如用字符串定位的方式,客户端如何知道服务端都有什么能力,以及之前提到的RPC由于网络传输带来的具有挑战的其他要求。实际开发过程中更多的是利用现在已有的开发框架gRPC,gRPC会帮我们处理好这些问题,也有着完善的生态,将在之后的文章中介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值