Go语言RPC学习记录
RPC概念
RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数。与之相对应的是本地函数调用,我们先来看一下本地函数调用。当我们写下如下代码的时候:
规则
result := Add(1,2)
我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。
那有没有办法,我们能够调用一个跨进程(所以叫"远程",典型的事例,这个进程部署在另一台服务器上)的函数呢?
这也是RPC主要实现的功能。
为什么微服务需要RPC
我们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦。
这样的话,如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题。
RPC版的"hello world"
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。接着我们尝试基于rpc实现一个类似的例子。我们先构造一个HelloService类型,其中的Hello方法用于实现打印功能:
type HelloService struct{}
func(p *HelloService)Hello(request string,reply *string)error{
*reply = "hello:" + request
return nil
}
Hello方法方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行 序列化
然后就可以将HelloService类型的对象注册为一个RPC服务:
func main(){
//rpc注册服务
//注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
rpc.RegisterName("HelloService",new(HelloService))
//设置服务监听
listener,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
//接受传输的数据
conn,err := listener.Accept()
if err != nil {
panic(err)
}
//rpc调用,并返回执行后的数据
//1.read,获取服务名称和方法名,获取请求数据
//2.调用对应服务里面的方法,获取传出数据
//3.write,把数据返回给client
rpc.ServeConn(conn)
}
其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
下面是客户端请求HelloService服务的代码:
func main(){
//用rpc连接
client,err := rpc.Dial("tcp","localhost:1234")
if err != nil {
panic(err)
}
var reply string
//调用服务中的函数
err = client.Call("HelloService.Hello","world",&reply)
if err != nil {
panic(err)
}
fmt.Println("收到的数据为,",reply)
}
首选是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。
跨语言的RPC
标准库的RPC默认采用Go语言特有的gob编码。因此,其它语言调用Go语言实现的RPC服务将比较困难。跨语言是互联网时代RPC的一个首要条件,这里我们再来实现一个跨语言的RPC。得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。
这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言RPC。
首先是基于json编码重新实现RPC服务:
func main(){
//注册rpc服务
rpc.RegisterName("HelloService",new(HelloService))
//设置监听
listener,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
for{
//接收连接
conn,err := listener.Accept()
if err != nil {
panic(err)
}
//给当前连接提供针对json格式的rpc服务
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,传入的参数是针对服务端的json编解码器。
然后是实现json版本的客户端:
func main(){
//简历tcp连接
conn,err := net.Dial("tcp","localhost:1234")
if err !=nil{
panic(err)
}
//简历基于json编解码的rpc服务
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var reply string
//调用rpc服务方法
err = client.Call("HelloService.Hello"," world",&reply)
if err != nil {
panic(err)
}
fmt.Println("收到的数据为:",reply)
}
先手工调用net.Dial函数建立TCP链接,然后基于该链接建立针对客户端的json编解码器。
在确保客户端可以正常调用RPC服务的方法之后,我们可以用命令来查看一下客户端发给服务端的到底是什么数据。这里我们使用 ==nc -l 1234== 这条命令 模拟服务器监听1234端口接收的数据,然后重新运行客户端,将会发现nc输出了以下的信息:
{"method":"HelloService.Hello","params":["hello"],"id":0}
nc常用有两种一种是连接到指定ip和端口
nc hostname port
另外一种是监听端口,等待连接
nc -l port
这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用端维护的一个唯一的调用编号。
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的:
type clientRequest struct {
Method string `json:"method"`
Params []interface{} `json:"params"`
Id uint64 `json:"id"`
}
type serverRequest struct {
Method string `json:"method"`
Params *json.RawMessage `json:"params"`
Id *json.RawMessage `json:"id"`
}
了解了客户端需要发送哪些数据之后,我们可以再来看看服务器接收到客户端传输的数据之后会返回哪些数据,还是用我们的nc命令。操作如下:
echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}'| nc localhost 1234
返回的数据如下:
其中id对应输入的id参数,result为返回的结果,error部分在出问题时表示错误信息。对于顺序调用来说,id不是必须的。但是Go语言的RPC框架支持异步调用,当返回结果的顺序和调用的顺序不一致时,可以通过id来识别对应的调用。
返回的json数据也是对应内部的两个结构体:客户端是clientResponse,服务端是serverResponse。两个结构体的内容同样也是类似的:
type clientResponse struct {
Id uint64 `json:"id"`
Result *json.RawMessage `json:"result"`
Error interface{} `json:"error"`
}
type serverResponse struct {
Id *json.RawMessage `json:"id"`
Result interface{} `json:"result"`
Error interface{} `json:"error"`
}
因此无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就解用json简单实现了跨语言的RPC。
但是一般在开发的时候除了用json做跨语言的RPC服务之外,现在很多公司还会选用protobuf做跨语言的RPC服务。那什么是ProtoBuf呢?接下来我们详细了解一下。
RPC协议封装
上面的代码服务名都是写死的,不够灵活(容易写错),这里我们对RPC的服务端和客户端再次进行一次封装,来屏蔽掉服务名,具体代码如下
服务端封装
//抽离服务名称
var serverName = "LoginService"
//定义一个父类
type RPCDesign interface {
Hello(string,*string)error
}
//实现工厂函数
func RegisterRPCServer(srv RPCDesign)error{
return rpc.RegisterName(serverName,srv)
}
封装之后的服务端实现如下:
type RpcServer struct{}
//5 + 3i chan func complex
func (this *RpcServer) Hello(req string, resp *string) error {
*resp += req + "你好"
return nil
}
func main() {
//设置监听
listener, err := net.Listen("tcp", ":8899")
if err != nil {
fmt.Println("设置监听错误")
return
}
defer listener.Close()
fmt.Println("开始监听....")
for {
//接收链接
conn, err := listener.Accept()
if err != nil {
fmt.Println("获取连接失败")
return
}
defer conn.Close()
fmt.Println(conn.RemoteAddr().String() + "连接成功")
//rpc表 注册rpc服务
if err = RegisterRPCServer(new(RpcServer)); err != nil {
fmt.Println("注册rpc服务失败")
return
}
//把rpc服务和套接字绑定
//rpc.ServeConn(conn)
rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
客户端封装
type RPCClient struct {
rpcClient *rpc.Client
}
func NewRpcClient(addr string)(RPCClient){
conn,err := net.Dial("tcp",addr)
if err != nil {
fmt.Println("链接服务器失败")
return RPCClient{}
}
defer conn.Close()
//套接字和rpc服务绑定
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
return RPCClient{rpcClient:client}
}
func (this*RPCClient)CallFunc(req string,resp*string)error{
return this.rpcClient.Call(serverName+".Hello",req,resp)
}
封装之后客户端实现
func main() {
//初始化对象 与服务名有关的内容完全封装起来了
client := NewRpcClient("127.0.0.1:8899")
//调用成员函数
var temp string
client.CallFunc("xiaoming",&temp)
fmt.Println(temp)
}
有疑问加站长微信联系(非本文作者)