RPC的原理及Go RPC

RPC的原理及Go RPC

一. RPC 相关概念

RPC(Remote Procedure Call),即远程过程调用。它允许像调用本地函数一样去调用远程服务器上的函数。

比如我们在写代码获取用户ID时调用了 GetUserById 这个函数,看起来是个普通函数调用,但实际上这个函数是运行在另一台机器上的。

user,err:=mysql.GetUserById(1)

RPC 解决了:

  • 网络通信(TCP/HTTP)
  • 数据序列化与反序列化
  • 请求分发与结果返回

举个形象的例子,我们调用函数就像打一通“电话”,RPC 就是负责:

拨号(找到远程服务)——> 传话(序列化请求参数,发过去)——> 等回话(反序列化返回结果)

1.1 本地调用

为了更好理解RPC,首先我们来尝试一个简单的本地调用例子

package main

import "fmt"

func Add(a, b int) int {
	return a + b
}

func main() {
	x := 3
	y := 5
	res := Add(x, y)
	fmt.Println(res)
}

  1. 编译器把 Add(x, y) 替换成一次函数调用指令(CALL),它会把参数放入寄存器或堆栈中。
  2. x=3y=5 作为参数被压栈或传寄存器。
  3. 执行到 CALL Add 指令时,跳转到 Add() 的代码地址,在 Add() 内部执行 a + b,结果存放在返回寄存器
  4. 返回寄存器的值被带回调用点,赋值给变量 res

整个过程完全发生在同一个进程内存空间中,没有网络,没有序列化,定义Add函数的代码和调用Add函数的代码共享同一个内存空间,所以调用能够正常执行。

1.2 RPC调用

但是我们无法直接在另一个程序中调用Add函数,因为它们是两个程序——内存空间是相互隔离的。

意思是每个程序运行在自己的进程空间中,我们每运行一个Go程序,系统会创建一个进程,这个进程有自己独立的内存空间,堆,栈等,再运行另一个程序 app2,系统又创建了另一个进程,它的内存空间和 app1 完全分开。

RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。

实现RPC面临的问题

一. 如何确定要执行的函数?

我们知道在本地调用中,函数主体通过函数指针函数指定,然后调用 add 函数,编译器通过函数指针函数自动确定 add 函数在内存中的位置。

但在 RPC 中,情况完全不同:

  • 客户端和服务端是两个进程,甚至两台机器。
  • Add() 在客户端并不存在,它的机器码在远端。

所以我们不能用本地函数指针,也不能直接 CALL(函数调用指令)。
于是就需要一种“间接定位”的方式。

这里的解决思路是建立一个函数映射表,也可以理解为设立一个关于函数的注册中心,例如:

function nameID实际函数指针
“Add”1Add(a, b)
“Login”2Login(user, pass)

客户端调用时:

{
  "func_id": 1,
  "params": [3, 5]
}

服务端解析到 ID=1,就能找到对应的 Add() 函数,反射调用它。

因此,RPC 不靠内存地址找函数,而是靠函数名或 ID 查表调用。

二. 如何表达参数?(序列化问题)

本地调用时

在同一进程中,函数调用的参数是直接通过 栈内存寄存器 传递的,比如:

参数存储位置
a3栈上
b5栈上

CPU 直接取栈上的值就能算。

RPC 调用时

客户端和服务端的内存不共享。
你不能直接把栈内存传过去——因为对方进程根本看不到你内存的地址。

所以必须把参数**“打包”成可以跨网络传输的格式**。
这就叫序列化

序列化后就变成字节流,例如:

{"a":3,"b":5}

→ 转成二进制后发送出去。

服务端收到数据后,再进行反序列化,恢复成结构体。

RPC 不能传内存地址,只能把参数序列化成字节流传过去。

三. 如何进行网络传输?

本地调用时

调用在同一个进程空间内,不需要任何通信协议。

RPC 调用时

客户端和服务端一般在不同进程或不同机器上。
因此必须通过网络通信。
这就涉及两部分:

  1. 建立连接(TCP / HTTP)
  2. 发送请求字节流,等待响应字节流

此时我们会用到一些传输协议,比如TCP,比如 gRPC 使用 HTTP/2 协议

RPC 通过网络协议传输序列化后的函数调用请求和返回结果。

关系图

      Client (app2)                        Server (app1)
+--------------------------+      +------------------------------+
| func_id=1 ("Add")        |      | registry: {"Add"->Add()}     |
| params={3,5}              |      |                              |
| serialize -> bytes         |──TCP/HTTP──>| deserialize -> call Add(3,5) |
| wait for response bytes    |<────────────| serialize result=8           |
+--------------------------+      +------------------------------+

RPC 的原理

(这里借用七米老师博客的一张图)
rpc

① RPC Call

  • Client(调用方)像本地函数一样调用一个方法,例如:

    result := Add(3, 5)
    
  • 实际上,这个函数并不是真正的函数实现,而是一个代理(Client Stub)


② Client Stub 打包参数(bundle args)

  • Client Stub 把调用的函数名、参数等打包(序列化)成字节流。

  • 比如将:

    {"method": "Add", "params": [3, 5]}
    

    转换成可以通过网络传输的格式(如 JSON、protobuf、msgpack)。


③ 发送请求(send)

  • Client Stub 把序列化后的数据发送给本机的网络层(Network Service)。
  • 网络层通过 TCP/HTTP 等协议发送到 Server 所在的主机。

④ 网络传输(Network)

  • 数据包在网络上传输,从 Computer 1 发送到 Computer 2。

⑤ Server Stub 接收并解包参数(unbundle args)

  • Server 端的网络服务接收到请求数据,交给 Server Stub。

  • Server Stub 将收到的字节流反序列化(unmarshal)为实际参数。

    method = "Add"
    params = [3, 5]
    

⑥ 本地调用(local call)

  • Server Stub 根据解析结果调用真正的本地函数:

    result := Add(3, 5)
    
  • 这部分就和普通函数调用没区别。


⑦ 函数返回(local return)

  • 函数执行完返回结果:

    result = 8
    

⑧ Server Stub 打包返回值(bundle ret vals)

  • Server Stub 将返回值序列化成字节流。

    {"result": 8}
    

⑨ 发送返回数据(send)

  • Server Stub 通过网络服务把数据发回给客户端。

⑩ 客户端网络服务接收(receive)

  • 客户端网络层收到服务器返回的数据包。

⑪ Client Stub 解包返回值(unbundle ret vals)

  • Client Stub 将字节流反序列化为真正的结果值:

    result = 8
    

⑫ 返回给调用者(RPC return)

  • 最终,Client 得到返回值,就像本地函数执行完一样:

    fmt.Println(result) // 输出 8
    

二. 相关方法

在写代码前我们要明确编写架构,及我们需要客户端发起请求,需要服务端接收请求以及需要注册的方法(结构体等相关工具)

rpc_http_demo/
├── client.go   # 客户端:发起 RPC 调用
├── server.go   # 服务端:提供 RPC 服务
└── service.go  # 公共结构体与工具(请求/响应定义)

2.1 基于HTTP的RPC

service.go

// service.go
package main

// 封装 RPC 调用的参数
type Params struct {
	A, B int
}

// 定义一个结构体作为服务对象,承载RPC方法,在 RPC 框架里,方法必须属于某个类型(结构体)才能被注册
type ServiceA struct{}

// 定义Add方法
func (s *ServiceA) Add(p *Params, result *int) (err error) {
	*result = p.A + p.B
	return
}

server.go

package main

import (
	"log"
	"net"
	"net/http"
	"net/rpc"
)

// 注册服务
func main() {
	//创建实例
	service := new(ServiceA)
	//注册服务
	rpc.Register(service)
	//注册HTTP处理器
	rpc.HandleHTTP()
	//监听连接
	l, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal("listen error", err)
	}
	//启动服务,第二个参数为 nil,意味着使用默认的 HTTP Handler
	http.Serve(l, nil)
}

client.go

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
	//建立到服务端的HTTP连接
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatal("dial err:", err)
	}
	//传参
	param := &Params{1, 2}
	var result int
	err = client.Call("ServiceA.Add", param, &result)
	if err != nil {
		log.Fatal("ServiceA.Add err:", err)
	}
	fmt.Println("Add : %d + %d = %d\n", param.A, param.B, result)
}

验证方法

我们可以在本地开两个终端,先启动server.go, 再到另一个终端启动client.go,最后可以看到相加结果

2.2 基于TCP的RPC

我们需要在server和client端做一些修改

server.go

func main() {
	service := new(ServiceA)
	rpc.Register(service)
	//tcp协议
	l, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal("listen error:", err)
	}
	// tcp 核心逻辑
	for {
		conn, _ := l.Accept() //阻塞等待客户端连接(返回一个net.Conn对象)
		rpc.ServeConn(conn)   // 为这个连接启动一个 RPC 会话,专门在该连接上处理一次 RPC 请求/响应。
	}
}

client.go

func main() {
	// 建立TCP连接
	client, err := rpc.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	param := &Params{3, 5}
	var result int
	err = client.Call("ServiceA.Add", param, &result)
	if err != nil {
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", param.A, param.B, result)

}

验证方法都是一样的

2.3 基于JSON协议的RPC

server.go

func main() {
	service := new(ServiceA)
	rpc.Register(service)
	//tcp协议
	l, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal("listen error:", err)
	}
	// tcp 核心逻辑
	for {
		conn, _ := l.Accept() //阻塞等待客户端连接(返回一个net.Conn对象)
		//使用JSON协议 
		rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

client.go

func main() {
	// 建立TCP连接
	conn, err := rpc.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 使用JSON协议
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	
	param := &Params{3, 5}
	var result int
	err = client.Call("ServiceA.Add", param, &result)
	if err != nil {
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", param.A, param.B, result)

}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值