RPC原理与Go RPC详解

文章目录

  • RPC原理与Go RPC
    • 什么是RPC
      • 本地调用
      • RPC调用
      • HTTP调用RESTful API
    • net/rpc
      • 基础RPC示例
      • 基于TCP协议的RPC
      • 使用JSON协议的RPC
      • Python调用RPC
    • RPC原理

RPC原理与Go RPC

什么是RPC

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

RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

首先与RPC(远程过程调用)相对应的是本地调用。

本地调用

package mainimport "fmt"func add(x, y int) int {z := x + yreturn z
}func main() {// 调用本地函数adda := 10b := 20ret := add(a, b)fmt.Println(ret)
}

将上述程序编译成二进制文件——app1后运行,会输出结果30。

app1程序中本地调用add函数的执行流程,可以理解为以下四个步骤。

  1. 将a和b的值压栈
  2. 通过函数指针找到add函数,进入函数取出栈中的值10和20,将其赋予x和y
  3. 计算x*y,并将结果存在z
  4. 将z的值压栈,然后从 add函数返回
  5. 从栈中取出z返回值,并赋值给ret

RPC调用

本地过程调用发生在同一进程中——定义add函数的代码和调用add函数的代码共享同一个内存空间,所以调用能够正常执行。

但是我们无法直接在另一个程序——app2中调用add`函数,因为它们是两个程序——内存空间是相互隔离的。(app1和app2可能部署在同一台服务器上也可能部署在互联网的不同服务器上。)

RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。

  1. 如何确定要执行的函数? 在本地调用中,函数主体通过函数指针函数指定,然后调用 add 函数,编译器通过函数指针函数自动确定 add 函数在内存中的位置。但是在 RPC 中,调用不能通过函数指针完成,因为它们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数。
  2. 如何表达参数? 本地过程调用中传递的参数是通过堆栈内存结构实现的,但 RPC 不能直接使用内存传递参数,因此参数或返回值需要在传输期间序列化并转换成字节流,反之亦然。
  3. 如何进行网络传输? 函数的调用方和被调用方通常是通过网络连接的,也就是说,function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。例如,一些 RPC 框架使用 TCP 协议,一些使用 HTTP。

以往实现跨服务调用的时候,我们会采用RESTful API的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接收API接口返回的响应数据。下面的示例是将add函数包装成一个RESTful API。

HTTP调用RESTful API

首先,我们编写一个基于HTTP的server服务,它将接收其他程序发来的HTTP请求,执行特定的程序并将结果返回。

// server/main.gopackage mainimport ("encoding/json""log""net/http"
)type addParam struct {X int `json:"x"`Y int `json:"y"`
}type addResult struct {Code int `json:"code"`Data int `json:"data"`
}func add(x, y int) int {return x + y
}func addHandler(w http.ResponseWriter, r *http.Request) {// Check for the HTTP method to be POSTif r.Method != http.MethodPost {http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)return}// Parse the request bodyvar param addParamerr := json.NewDecoder(r.Body).Decode(&param)if err != nil {http.Error(w, "Invalid request body", http.StatusBadRequest)return}// Perform the business logicret := add(param.X, param.Y)// Return the responseresp := addResult{Code: 0, Data: ret}w.Header().Set("Content-Type", "application/json")err = json.NewEncoder(w).Encode(resp)if err != nil {log.Println("Error encoding response:", err)}
}func main() {http.HandleFunc("/add", addHandler)log.Fatal(http.ListenAndServe(":9090", nil))
}

我们编写一个客户端来请求上述HTTP服务,传递x和y两个整数,等待返回结果。

// client/main.gopackage mainimport ("bytes""encoding/json""fmt""io""net/http"
)type Param struct {X int `json:"x"`Y int `json:"y"`
}type Result struct {Code int `json:"code"`Data int `json:"data"`
}func main() {// 通过HTTP请求调用其他服务器上的add服务url := "http://127.0.0.1:9090/add"param := Param{X: 10,Y: 20,}paramBytes, err := json.Marshal(param)if err != nil {fmt.Println("Error marshalling request body:", err)return}resp, err := http.Post(url, "application/json", bytes.NewReader(paramBytes))if err != nil {fmt.Println("Error making HTTP POST request:", err)return}defer resp.Body.Close()respBytes, err := io.ReadAll(resp.Body)if err != nil {fmt.Println("Error reading response body:", err)return}var respData Resulterr = json.Unmarshal(respBytes, &respData)if err != nil {fmt.Println("Error unmarshalling response body:", err)return}fmt.Println(respData.Data) // 30
}

这种模式是我们目前比较常见的跨服务或跨语言之间基于RESTful API的服务调用模式。 既然使用API调用也能实现类似远程调用的目的,为什么还要用RPC呢?

使用 RPC 的目的是让我们调用远程方法像调用本地方法一样无差别。并且基于RESTful API通常是基于HTTP协议,传输数据采用JSON等文本协议,相较于RPC 直接使用TCP协议,传输数据多采用二进制协议来说,RPC通常相比RESTful API性能会更好。

RESTful API多用于前后端之间的数据传输,而目前微服务架构下各个微服务之间多采用RPC调用。

net/rpc

基础RPC示例

Go语言的 rpc 包提供对通过网络或其他 i/o 连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。注册后,对象的导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务) ,但是不支持注册同一类型的多个对象。

在下面的代码中我们定义一个ServiceA类型,并为其定义了一个可导出的Add方法。并将ServiceA类型注册为一个服务,其Add方法就支持RPC调用了。

// rpc demo/service.gopackage maintype Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply = args.X + args.Yreturn nil
}func main() {service := new(ServiceA)rpc.Register(service) // 注册RPC服务rpc.HandleHTTP()      // 基于HTTP协议l, e := net.Listen("tcp", ":9091")if e != nil {log.Fatal("listen error:", e)}http.Serve(l, nil)
}

此时,client 端便能看到一个拥有“Add”方法的“ServiceA”服务,想要调用这个服务需要使用下面的代码先连接到server端再执行远程调用。

// rpc demo/client.gopackage mainimport ("fmt""log""net/rpc"
)type ClientArgs struct {X, Y int
}func main() {// 建立HTTP连接client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")if err != nil {log.Fatal("dialing:", err)}// 同步调用args := &ClientArgs{10, 20}var reply interr = client.Call("ServiceA.Add", args, &reply)if err != nil {log.Fatal("ServiceA.Add error:", err)}fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)// 异步调用var reply2 intdivCall := client.Go("ServiceA.Add", args, &reply2, nil)replyCall := <-divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}

a. 同步调用:

  • client.Call("ServiceA.Add", args, &reply): 该行代码表示使用client连接对象对名为"ServiceA.Add"的远程方法进行同步调用,传递了args作为参数,并将结果存储在reply中。
  • 如果调用出现错误,则通过log.Fatal输出错误信息。

b. 异步调用:

  • client.Go("ServiceA.Add", args, &reply2, nil): 该行代码表示使用client连接对象对名为"ServiceA.Add"的远程方法进行异步调用,传递了args作为参数,并将结果存储在reply2中。此处使用了Go方法,该方法会立即返回一个rpc.Call对象,它代表了异步调用的状态。
  • <-divCall.Done: 通过使用<-操作符,我们等待异步调用完成,这里divCall.Done是一个通道,它会在异步调用结束时收到一个通知。
  • replyCall.Error: 获取异步调用结果的错误信息(如果有的话)。
  • reply2: 获取异步调用的返回值。

执行上述两个程序,查看 RPC 调用的结果。

会看到如下输出结果。

ServiceA.Add: 10+20=30
<nil>
30

基于TCP协议的RPC

当然 rpc 包也支持直接使用 TCP 协议而不使用HTTP协议。

server 端代码修改如下。

// rpc demo/service.gopackage mainimport ("log""net""net/rpc"
)type Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply = args.X + args.Yreturn nil
}func main() {service := new(ServiceA)rpc.Register(service) // 注册RPC服务l, e := net.Listen("tcp", ":9091")if e != nil {log.Fatal("listen error:", e)}for {conn, _ := l.Accept()rpc.ServeConn(conn)}
}

client 端代码修改如下。

// rpc demo/client.gopackage mainimport ("fmt""log""net/rpc"
)type ClientArgs struct {X, Y int
}func main() {// 建立TCP连接client, err := rpc.Dial("tcp", "127.0.0.1:9091")if err != nil {log.Fatal("dialing:", err)}// 同步调用args := &ClientArgs{10, 20}var reply interr = client.Call("ServiceA.Add", args, &reply)if err != nil {log.Fatal("ServiceA.Add error:", err)}fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)// 异步调用var reply2 intdivCall := client.Go("ServiceA.Add", args, &reply2, nil)replyCall := <-divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}

使用JSON协议的RPC

rpc 包默认使用的是 gob 协议对传输数据进行序列化/反序列化,比较有局限性。下面的代码将尝试使用 JSON 协议对传输数据进行序列化与反序列化。

server 端代码修改如下。

// rpc demo/service.gopackage mainimport ("log""net""net/rpc""net/rpc/jsonrpc"
)type Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply = args.X + args.Yreturn nil
}func main() {service := new(ServiceA)rpc.Register(service) // 注册RPC服务l, e := net.Listen("tcp", ":9091")if e != nil {log.Fatal("listen error:", e)}for {conn, _ := l.Accept()// 使用JSON协议rpc.ServeCodec(jsonrpc.NewServerCodec(conn))}
}

client 端代码修改如下。

// rpc demo/client.gopackage mainimport ("fmt""log""net""net/rpc""net/rpc/jsonrpc"
)type ClientArgs struct {X, Y int
}func main() {// 建立TCP连接conn, err := net.Dial("tcp", "127.0.0.1:9091")if err != nil {log.Fatal("dialing:", err)}// 使用JSON协议client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))// 同步调用args := &ClientArgs{10, 20}var reply interr = client.Call("ServiceA.Add", args, &reply)if err != nil {log.Fatal("ServiceA.Add error:", err)}fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)// 异步调用var reply2 intdivCall := client.Go("ServiceA.Add", args, &reply2, nil)replyCall := <-divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}

Python调用RPC

下面的代码演示了如何使用 python client 远程调用上面 Go server中 serviceA的Add方法。

import socket
import jsonrequest = {"id": 0,"params": [{"x":10, "y":20}],  # 参数要对应上Args结构体"method": "ServiceA.Add"
}client = socket.create_connection(("127.0.0.1", 9091),5)
client.sendall(json.dumps(request).encode())rsp = client.recv(1024)
rsp = json.loads(rsp.decode())
print(rsp)

输出结果:

{'id': 0, 'result': 30, 'error': None}

RPC原理

RPC 让远程调用就像本地调用一样,其调用过程可拆解为以下步骤。

image-20230804131507445

① 服务调用方(client)以本地调用方式调用服务;

② client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

③ client stub找到服务地址,并将消息发送到服务端;

④ server 端接收到消息;

⑤ server stub收到消息后进行解码;

⑥ server stub根据解码结果调用本地的服务;

⑦ 本地服务执行并将结果返回给server stub;

⑧ server stub将返回结果打包成能够进行网络传输的消息体;

⑨ 按地址将消息发送至调用方;

⑩ client 端接收到消息;

⑪ client stub收到消息并进行解码;

⑫ 调用方得到最终结果。

使用RPC框架的目标是只需要关心第1步和最后1步,中间的其他步骤统统封装起来,让使用者无需关心。例如社区中各式RPC框架(grpc、thrift等)就是为了让RPC调用更方便。

References:https://www.liwenzhou.com/posts/Go/golang-menu/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/75617.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 配置多数据源【最简单的方式】

Druid连接池 Spring Boot 配置多数据源【最简单的方式】 文章目录 Druid连接池 Spring Boot 配置多数据源【最简单的方式】 0.前言1.基础介绍2.步骤2.1. 引入依赖2.2. 配置文件2.3. 核心源码Druid数据源创建器Druid配置项 DruidConfig 3.示例项目3.1. pom3.1.1. 依赖版本定义3.…

matlab使用教程(8)—绘制三维曲面图

1网格图和曲面图 MATLAB 在 x-y 平面中的网格上方使用点的 z 坐标来定义曲面图&#xff0c;并使用直线连接相邻的点。mesh 和surf 函数以三维形式显示曲面图。 • mesh 生成仅使用颜色来标记连接定义点的线条的线框曲面图。 • surf 使用颜色显示曲面图的连接线和面。 MATL…

网络安全 Day26-PHP 简单学习

PHP 简单学习 1. 为什么要学习PHP2. PHP语法3. php 变量4. 字符串数据5. PHP 函数6. 数组 1. 为什么要学习PHP php存量多开源软件多很多安全流程 渗透方法 sql注入基于PHP语言入门简单 2. PHP语法 格式: <?php 内容?>或<?内容?>结尾分号例子<?php phpin…

深度学习实践——循环神经网络实践

系列实验 深度学习实践——卷积神经网络实践&#xff1a;裂缝识别 深度学习实践——循环神经网络实践 深度学习实践——模型部署优化实践 深度学习实践——模型推理优化练习 代码可见于&#xff1a;https://download.csdn.net/download/weixin_51735061/88131380?spm1001.201…

【Linux】进程间通信——管道

目录 写在前面的话 什么是进程间通信 为什么要进行进程间通信 进程间通信的本质理解 进程间通信的方式 管道 System V IPC POSIX IPC 管道 什么是管道 匿名管道 什么是匿名管道 匿名管道通信的原理 pipe()的使用 匿名管道通信的特点 拓展代码 命名管道 什么是命…

ChatGPT结合知识图谱构建医疗问答应用 (二) - 构建问答流程

一、ChatGPT结合知识图谱 上篇文章对医疗数据集进行了整理&#xff0c;并写入了知识图谱中&#xff0c;本篇文章将结合 ChatGPT 构建基于知识图谱的问答应用。 下面是上篇文章的地址&#xff1a; ChatGPT结合知识图谱构建医疗问答应用 (一) - 构建知识图谱 这里实现问答的流程…

备忘录模式——撤销功能的实现

1、简介 1.1、概述 备忘录模式提供了一种状态恢复的实现机制&#xff0c;使得用户可以方便地回到一个特定的历史步骤。当新的状态无效或者存在问题时&#xff0c;可以使用暂时存储起来的备忘录将状态复原。当前很多软件都提供了撤销&#xff08;Undo&#xff09;操作&#xf…

Spring AOP

1.什么是 Spring AOP&#xff1f; AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff1a;面向切面编程&#xff0c;它是⼀种思想&#xff0c;它是对某⼀类事情的集中处理。⽐如⽤户登录权限的效验&#xff0c;没学 AOP 之前&#xff0c;我们所有需要判断⽤户登…

ClickHouse(七):Clickhouse数据类型-2

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,Kerberos安全认证,大数据OLAP体系技术栈-CSDN博客 &…

openlayers渲染rgb三波段cog时达到类似rgba的效果(去掉黑底)

图是arcgis渲染成rgb的&#xff0c;由于没有透明度波段&#xff0c;底下是黑的。 为了能在前端显示透明效果&#xff0c;之前是用python处理数据&#xff0c;给它加个透明度波段 后来研究了一下ol的样式表达式&#xff0c;可以直接在前端去掉黑底 样式设置代码如下 const s…

Socks IP轮换:为什么是数据挖掘和Web爬取的最佳选择?

在数据挖掘和Web爬取的过程中&#xff0c;IP轮换是一个非常重要的概念。数据挖掘和Web爬取需要从多个网站或来源获取数据&#xff0c;而这些网站通常会对来自同一IP地址的请求进行限制或封锁。为了避免这些问题&#xff0c;数据挖掘和Web爬取过程中需要使用Socks IP轮换技术。在…

云原生势不可挡,如何跳离云原生深水区?

云原生是云计算领域一大热词&#xff0c;伴随云原生概念而来的是数字产业迎来井喷、数字变革来临、数字化得以破局以及新一波的技术红利等等。云原生即“云”原生&#xff0c;顾名思义是让“应用”最大程度地利用云的能力&#xff0c;发挥云价值的最佳路径。具体来说&#xff0…

Eureka增加账号密码认证登录

一、业务背景 注册中心Eureka在微服务开发中经常使用到&#xff0c;用来管理发布的微服务&#xff0c;供前端或者外部调用。但是如果放到生产环境&#xff0c;我们直接通过URL访问的话&#xff0c;这显然是不安全的。 所以需要给注册中心加上登录认证。 通过账号和密码认证进行…

【机器学习】西瓜书习题3.5Python编程实现线性判别分析,并给出西瓜数据集 3.0α上的结果

参考代码 结合自己的理解&#xff0c;添加注释。 代码 导入相关的库 import numpy as np import pandas as pd import matplotlib from matplotlib import pyplot as plt导入数据&#xff0c;进行数据处理和特征工程 得到数据集 D { ( x i , y i ) } i 1 m , y i ∈ { 0 ,…

小程序商品如何设置限购

限购是一种常用的小程序商品销售策略&#xff0c;可以帮助商家提高销售额、控制库存和增加用户的购买欲望。那么&#xff0c;小程序产品怎么设置限购呢&#xff1f;下面将为您详细介绍。 1. 设置限购数量 可以设置最低购买数量来鼓励用户批量购买或满足特定的销售需求。例如&…

FFmpeg常见命令行(一):FFmpeg工具使用基础

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》。本文是Android音视频任务列表的其中一个&#xff0c; 对应的要学习的内容是&#xff1a;FFmpe…

沙箱逃逸复现

当this指向window 原理 1.this直接指向window&#xff0c;拿到window的tostring的constructor来利用构造函数拿到process 是对象且指向沙箱外部&#xff0c;才可以利用 const vm require(vm); const script const process this.toString.constructor(return process)() pr…

OpenCL编程指南-9.1命令、队列、事件

概述 命令队列是OpenCL的核心。平台定义了一个上下文&#xff0c;其中包含一个或多个计算设备。每个计算设备可以有一个或多个命令队列。提交到这些队列的命令将完成OpenCL程序的具体工作。 在一个简单的OpenCL程序中&#xff0c;提交到一个命令队列的命令会按顺序执行。一个…

面试热题100(二叉树的右视图)

给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 树这类问题用的最多的就是递归&#xff0c;因为树具有天然的递归结构&#xff1a; 我们来分析一下题目&#xff0c;给定一棵树根结…

vue拖拽改变宽度

1.封装组件ResizeBox.vue <template><div ref"resize" class"resize"><div ref"resizeHandle" class"handle-resize" /><slot /></div> </template> <script> export default {name: Resi…