RPC原理与Go RPC详解

这篇具有很好参考价值的文章主要介绍了RPC原理与Go RPC详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

RPC原理与Go RPC

什么是RPC

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

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

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

本地调用

package main

import "fmt"

func add(x, y int) int {
	z := x + y
	return z
}

func main() {
	// 调用本地函数add
	a := 10
	b := 20
	ret := 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.go

package main

import (
	"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 POST
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Parse the request body
	var param addParam
	err := json.NewDecoder(r.Body).Decode(&param)
	if err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Perform the business logic
	ret := add(param.X, param.Y)

	// Return the response
	resp := 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.go

package main

import (
	"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 Result
	err = 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.go

package main

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.Y
	return 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.go

package main

import (
	"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 int
	err = 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 int
	divCall := 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.go

package main

import (
	"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.Y
	return 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.go

package main

import (
	"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 int
	err = 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 int
	divCall := 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.go

package main

import (
	"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.Y
	return 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.go

package main

import (
	"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 int
	err = 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 int
	divCall := 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 json

request = {
    "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 让远程调用就像本地调用一样,其调用过程可拆解为以下步骤。

RPC原理与Go RPC详解,Go,# RPC,rpc,golang

① 服务调用方(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/文章来源地址https://www.toymoban.com/news/detail-626941.html

到了这里,关于RPC原理与Go RPC详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Golang 实现一个简单的 RPC 服务

    分享一个简单的 rpc 服务框架 一、服务端实现 二、客户端实现

    2024年04月10日
    浏览(45)
  • Go RPC 库

    RPC(Remote Poresedure Call)是远程方法调用的缩写。Go的RPC库可以实现通过网络或者其他I/O方式远程调用对象的方法。 服务器注册一个对象,让它作为一个以对象类型命名的服务,让这个对象导出的方法可以被远程调用。一个服务器可以注册多个不同类型的对象,但是不能注册同一

    2024年02月19日
    浏览(61)
  • go rpc

    运用go标准库写一个rpc例子 服务端 注意绑定的对象方法,必须有两个参数,第二个参数必须是“指针类型”为传出参数。方法只有一个error类型的返回值。 客户端 注意:上面代码中rpc的远程函数调用采用的是golang中特有的序列化包gob,其他语音不能解析。所以,要进行不同

    2024年02月12日
    浏览(31)
  • 开源 Golang 微服务入门二:RPC 框架 Kitex

    前一篇笔记介绍了字节跳动的开源 Golang 微服务 HTTP 框架 Hertz, 如下: 开源 Golang 微服务入门一: HTTP 框架 Hertz 本文将要介绍同样是字节跳动开源的 Golang 微服务 RPC 框架 Kitex。 Kitex 简介 Kitex 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,在字节内部

    2024年02月09日
    浏览(44)
  • Go七天实现RPC

    本文是学习自7天用Go从零实现RPC框架GeeRPC | 极客兔兔 在此基础上,加入自己的学习过程与理解。 自己实现过程的完整代码:https://github.com/liwook/Go-projects/tree/main/geerpc RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。RPC 的客户端和服

    2024年01月19日
    浏览(40)
  • php-golang-rpc 简单的jsonrpc实践

    golang代码: package main import (     \\\"net\\\"     \\\"net/rpc\\\"     \\\"net/rpc/jsonrpc\\\" ) type App struct{} type Res struct {     Code int    `json:\\\"code\\\"`     Msg  string `json:\\\"msg\\\"`     Data any    `json:\\\"data\\\"` } func (*App) Hi(mp map[string]any, res *Res) error {     res.Code = 200     res.Msg = \\\"成功\\\"     var rmp = make(map[string]

    2024年02月14日
    浏览(47)
  • golang实现rpc方法二:使用jsonrpc库【跨平台】

    首先在golang实现rpc方法一net/rpc库中实现了RPC方法,但是那个方法不是跨平台的,没法在其他语言中调用这个实现的RPC方法,接下来我们可以通过jsonroc库实现跨语言的RPC方法。俩种实现方式的代码其实也是差不多的,大差不差,只是调用的库不同。 serverrpc.go实现代码如下 cl

    2024年01月17日
    浏览(61)
  • Go微服务实践 - Rpc核心概念理解

    从0研究一下Golang已经Golang的微服务生态体系,Golang的微服务首先要从Rpc开始,在升级到Grpc,详细介绍这些技术点都在解决什么技术问题。 Rpc Rpc (Remote Procedure Call) 远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。 对应Rpc的是本地过程调用,函数调用时最常

    2024年02月14日
    浏览(46)
  • php-golang-rpc使用roadrunner-server/goridge/v3/pkg/rpc和php的spiral/goridge3.2实践

    package main import (     \\\"fmt\\\"     \\\"net\\\"     \\\"net/rpc\\\"     goridgeRpc \\\"github.com/roadrunner-server/goridge/v3/pkg/rpc\\\" ) type App struct{} func (s *App) Hi(name string, r *string) error {     *r = fmt.Sprintf(\\\"Hello, %s!\\\", name)     return nil } func main() {     ln, err := net.Listen(\\\"tcp\\\", \\\":6001\\\")     if err != nil {         panic(er

    2024年02月15日
    浏览(41)
  • go-zero的rpc服务案例解析

    go-zero的远程调用服务是基于gRpc的gRPC教程与应用。 zero使用使用gRpc需要安装 protoc 插件,因为gRpc基于protoc插件使用protocol buffers文件生成rpc服务器和api的代码的。 gRPC 的代码生成还依赖 protoc-gen-go,protoc-gen-go-grpc 插件来配合生成 Go 语言的 gRPC 代码。 也可以使用go get命令安装

    2024年02月13日
    浏览(76)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包