当前位置: 首页 > news >正文

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 mainimport "fmt"func Add(a, b int) int {return a + b
}func main() {x := 3y := 5res := 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.Breturn
}

server.go

package mainimport ("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 Handlerhttp.Serve(l, nil)
}

client.go

package mainimport ("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 interr = 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 interr = 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 interr = 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)}
http://www.dtcms.com/a/470814.html

相关文章:

  • 青岛的网站建设怎做网站
  • 智能网站建设软件有哪些潍坊网络推广个人合作
  • Python下载实战:高效稳定技巧大全
  • 手机如何创建简易网站设计签名免费网站
  • 扎染毕业设计代做网站网站备案和域名备案区别
  • NX581NX600美光SSD固态闪存NX601NX602
  • 网络科技公司网站首页蚌埠做网站有哪些公司
  • 建设网站费用要进固定资产吗易语言做网站教程
  • UE5 测量 -4,长度测量:P10点击按钮清除距离测量,P11最终测量效果。
  • 返回链接 网站惩罚检查 错误检查百度一下官方下载安装
  • 房地产网站方案网络广告策划书案例
  • VS(QT)调用Matlab函数的方法
  • 企业网站搜索优化网络推广网站首页的logo这么修改
  • fastapi集成各个组件
  • SLAM基础原理介绍
  • 如何利用网站新闻做推广成功营销网站
  • wordpress基础主题站wordpress order插件
  • 福建宁德建设局网站医院网站建设中标
  • 无锡网站制作8大米品牌推广方案
  • 网站开发实践意义seo招聘要求
  • 常州百度网站排名优化网站关键字怎么优化
  • 【Java数据结构】选择排序编码关键细节与避坑指南
  • 什么是企业营销型网站上海做网站的多吗
  • map与multimap
  • 从数据库直连到缓存预热:城市列表查询的性能优化全流程
  • 大兴做网站免费追剧的app下载
  • 建设一个网站需要哪些网站优化 北京
  • CSS 子元素宽高继承与盒模型规则
  • 学习随笔-回流和重绘
  • 石狮网站建设哪家好游戏网站开发有限公司