RPC的概念以及Go语言中标准库rpc的基本使用

一、什么是rpc

RPC(Remote Procedure Call),即远程过程调用。它允许像调用本地服务一样调用远程服务。在分布式计算,远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。 RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。 接口定义语言 (IDL) - 用于描述软件组件的应用程序编程接口 (API) 的规范语言 - 常用于远程过程调用软件。在这种情况下,IDL 在链路两端的机器之间提供了一座桥梁,这些机器可能使用不同的操作系统 (OS) 和计算机语言。

1.1本地调用

package main

import "fmt"

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

func main() {
	// 调用本地函数add
	a := 10
	b := 20
	ret := add(a, b)
	fmt.Println(ret)
}

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

  1. 将变量 a 和 b 的值分别压入堆栈上
  2. 执行 add 函数,从堆栈中获取 a 和 b 的值,并将它们分配给 x 和 y
  3. 计算 x + y 的值并将其保存到堆栈中
  4. 退出 add 函数并将 x + y 的值赋给 ret

1.2RPC调用

本地过程调用发生在同一进程中,共享同一个内存空间,但 RPC 通信需要跨不同的机器和不同的进程,因此需要解决以下三个问题。

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

1.3HTTP调用RESTful API

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

// http_demo/server/main.go
package main

import (
	"encoding/json"
	"io/ioutil"
	"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) {
	// 解析参数,接收客户端发来请求中的参数
	b, _ := ioutil.ReadAll(r.Body)
	var param addParam
	// 将从客户端得到的json进行反序列化
	json.Unmarshal(b, &param)
	// 业务逻辑,将获得的两个参数进行相加
	ret := add(param.X, param.Y)
	// 返回响应,将两数相加的结果进行序列化,返回给客户端
	respBytes, _ := json.Marshal(addResult{Code: 0, Data: ret})
	w.Write(respBytes)
}

func main() {
	http.HandleFunc("/add", addHandler)
	log.Fatal(http.ListenAndServe(":9090", nil))
}

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

// http_demo/client/main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"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 main() {
	// 通过HTTP请求调用其他服务器上的add服务
	url := "http://127.0.0.1:9090/add"
	param := addParam{
		X: 10,
		Y: 20,
	}
	// 将参数转为json并进行序列化
	paramBytes, _ := json.Marshal(param)
	// 使用http的post方法将参数发送给服务端
	resp, _ := http.Post(url, "application/json", bytes.NewReader(paramBytes))
	// 延迟调用是在 defer 所在函数结束时进行
	defer resp.Body.Close()
	// 处理从服务端返回的数据
	respBytes, _ := ioutil.ReadAll(resp.Body)
	var respData addResult
	// 反序列化,打印两数相加之和
	json.Unmarshal(respBytes, &respData)
	fmt.Println(respData.Data) // 30
}

这种模式是我们目前比较常见的跨服务或跨语言之间基于RESTful API的服务调用模式。

RESTful API通常是基于HTTP协议,传输数据采用JSON等文本协议,相较于RPC 直接使用TCP协议,传输数据多采用二进制协议来说,RPC通常相比RESTful API性能会更好。

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

二、net/rpc

2.1基础RPC示例

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

在下面的代码中我们定义一个ServiceA类型,并为其定义了一个可导出的Add方法。

// rpc rpc_base_demo/server/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
}

通过下面的代码将上面定义的ServiceA类型注册为一个服务,其Add方法就支持RPC调用了。

// rpc rpc_base_demo/server/server.go

package main

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

func main() {
	//给ServiceA类型注册为一个服务,其add方法就只支持RPC调用了
	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 rpc_base_demo/client/client.go

package main

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

type Args 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 := &Args{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)
}

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

先启动 server 端。
go run server.go service.go
再启动 client 端。
go run client.go
会看到如下输出结果。
ServiceA.Add: 10+20=30 <nil> 30

2.2基于TCP协议的RPC

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

server 端代码修改如下。

// rpc rpc_tcp_demo/server/server.go

package main

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

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 rpc_tcp_demo/client/client.go

package main

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

type Args 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 := &Args{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)
}

2.3使用JSON协议的RPC

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

server 端代码修改如下。

// rpc rpc_json_demo/server/server.go

package main

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

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 rpc_json_demo/client/client.go

package main

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

type Args 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 := &Args{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)
}

2.4Python调用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)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准Go语言标准非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值