Golang实现RPC
RPC
一、简介
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议 ;
该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额 外地为这个交互作用编程 ;
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方 法调用 ;
二、Golang中实现RPC
Golang中实现RPC非常简单,官方提供了封装好的库,还有一些第三方的库,官方的net/rpc
库使用encoding/gob
进行编码,所以Golang的RPC只支持Golang开发的服务器和客户端之间交互。
官方还提供了net/rpc/jsonrpc
库来实现RPC方法,jsonrpc
采用了JSON
进行数据编码解码,因而支持跨语言调用,目前jsonrpc
库是基于tcp
协议实现的,暂不支持http
传输方式。
- Golang的RPC必须符合四个条件才可以:
- 结构体首字母必须大写,要跨域访问,所以大写
- 函数名首字母必须大写
- 函数第一个参数是接收参数,第二个参数是返回个客户端的参数,必须是指针类型
- 函数有一个返回值error
示例
1、Golang实现RPC程序,实现求矩形面积和周长。
- 服务端
package main
import (
"fmt"
"net/http"
"net/rpc"
)
// 服务端求矩形面积和周长
// 声明矩形对象
type Rect struct{}
// 声明参数结构体,字段首字母大写
type Params struct{
Width, Height int
}
// 定义求矩形面积的方法
func (r *Rect)Area(p Params,ret *int) error {
*ret = p.Width * p.Height
return nil
}
// 定义求矩形周长的方法
func (r *Rect) Perimeter(p Params, ret *int)error{
*ret = (p.Width + p.Height) * 2
return nil
}
func main() {
// 注册服务
rect := new(Rect)
rpc.Register(rect)
// 把服务处理绑定到http协议上
rpc.HandleHTTP()
// 监听服务,等待客户端调用求面试和周长的方法
if err := http.ListenAndServe(":8080",nil); err != nil {
fmt.Println(err)
}
}
- 客户端
package main
import (
"fmt"
"log"
"net/rpc"
)
type Params struct{
Width,Height int
}
// 调用服务
func main() {
// 连接远程rpc服务
client, err := rpc.DialHTTP("tcp","127.0.0.1:8080")
if err != nil {
fmt.Println(err)
}
// 调用远程的方法
// 定义接受服务端传回来的计算结果变量
ret := 0
// 求面积
if err = client.Call("Rect.Area",Params{50,100},&ret); err != nil {
log.Fatal(err)
}
fmt.Println("面积:", ret)
// 求周长
if err = client.Call("Rect.Perimeter",Params{50,100},&ret);err !=nil{
log.Fatal(err)
}
fmt.Println("周长:",ret)
}
2 、服务端接收两个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下:
9 * 2 =18
9 / 2, 商 = 4, 余数 = 1
- 服务端
package main
import (
"errors"
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
// 声明算术运算的结构体
type Arith struct{}
// 声明接收参数的结构体
type ArithRequest struct{
A, B int
}
// 声明返回客户端参数结构体
type ArithResponse struct{
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
// 乘法运算
func (a *Arith) Multiply(req ArithRequest, resp *ArithResponse)error{
resp.Pro = req.A * req.B
fmt.Println(resp.Pro)
return nil
}
// 商和余数
func (a *Arith) Divide(req ArithRequest, resp *ArithResponse) error {
if req.B == 0 {
return errors.New("除数不能为0")
}
resp.Quo = req.A / req.B
resp.Rem = req.A % req.B
return nil
}
// jsonRPC编码方式
func main() {
// 注册服务
rpc.Register(new(Arith))
// 监听
lis,err := net.Listen("tcp","127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
// 循环监听服务
for {
conn, err := lis.Accept()
if err != nil{
continue
}
go func(conn net.Conn) {
fmt.Println("a new Client")
jsonrpc.ServeConn(conn)
}(conn)
}
}
- 客户端
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
// 声明请求参数的结构体
type ArithRequest struct{
A, B int
}
// 声明响应的结构体
type ArithResponse struct{
Pro int
Quo int
Rem int
}
func main() {
// 连接远程的rpc
conn, err := jsonrpc.Dial("tcp","127.0.0.1:8080")
if err != nil{
log.Fatal(err)
}
req := ArithRequest{
A: 9,
B: 2,
}
resp := ArithResponse{}
// 商和余数
if err = conn.Call("Arith.Divide",req,&resp); err != nil {
log.Fatal(err)
}
// 乘法
if err = conn.Call("Arith.Multiply", req, &resp); err != nil {
log.Fatal(err)
}
fmt.Printf("%d 和 %d, 积=%d, 商=%d,余数=%d\n",req.A,req.B,resp.Pro,resp.Quo,resp.Rem)
}
三、RPC调用流程
微服务架构下数据交互一般都是对内RPC,对外REST。
将业务按功能模块拆分到各个微服务,具有提高项目协作效率,降低模块耦合度,提高系统系统可用性等优点,但是开发门槛较高,比如RPC框架的使用,后期服务监控等工作。
一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用。
四、网络传输数据格式
成熟的RPC框架会有自定义传输协议,这里网络传输格式定义如下,前面是固定长度消息头,后面是变长消息体。
- 编写连接会话
package rpc
import (
"encoding/binary"
"io"
"net"
)
// 编写会话中数据读写
// 会话连接的结构体
type Session struct {
conn net.Conn
}
// 创建新的连接
func NewSession(conn net.Conn) *Session{
return &Session{
conn: conn,
}
}
// 向连接中写入数据
func (s *Session) Write(data []byte)error{
// 4字节头+数据长度的切片
buf := make([]byte, 4 + len(data))
// 写入头部数据,记录数据长度
// binary只认固定长度的类型,所以使用了uint32,而不是直接写入
binary.BigEndian.PutUint32(buf[:4],uint32(len(data)))
// 写入数据
copy(buf[4:],data)
_, err := s.conn.Write(buf)
if err != nil {
return err
}
return nil
}
// 从连接中读数据
func (s *Session)Read()([]byte, error){
// 读取头部长度
header := make([]byte,4)
// 按头部长度,读取头部数据
_, err := io.ReadFull(s.conn, header)
if err != nil {
return nil, err
}
// 读取数据长度
dataLen := binary.BigEndian.Uint32(header)
// 按数据长度去读取数据
data := make([]byte,dataLen)
_, err = io.ReadFull(s.conn, data)
if err != nil{
return nil ,err
}
return data, nil
}
- 测试会话读写
package rpc
import (
"fmt"
"net"
"sync"
"testing"
)
// 测试读写
func TestSession_ReadWrite(t *testing.T) {
// 定义监听的ip和端口
addr := "127.0.0.1:8000"
// 定义传输的数据
my_data := "hello"
// 等待组
wg := sync.WaitGroup{}
// 协程 一个读,一个写
wg.Add(2)
// 写数据
go func(){
defer wg.Done()
// 创建tcp连接
lis, err := net.Listen("tcp",addr)
if err != nil{
t.Fatal(err)
}
conn, _ := lis.Accept()
s := Session{conn: conn}
// 写数据
if err := s.Write([]byte(my_data)); err != nil {
t.Fatal(err)
}
}()
// 读数据
go func() {
defer wg.Done()
conn, err := net.Dial("tcp",addr)
if err != nil {
t.Fatal(err)
}
s := Session{conn: conn}
// 读数据
data, err := s.Read()
if err != nil{
t.Fatal(err)
}
if string(data) != my_data{
t.Fatal(err)
}
fmt.Println(string(data))
}()
wg.Wait()
}
- 编写编解码
package rpc
import (
"bytes"
"encoding/gob"
)
// 定义数据格式和编解码
// 定义RPC交互的数据格式
type RPCData struct{
// 访问的函数
Name string
// 访问时穿的参数
Args []interface{}
}
// 编码
func encode(data RPCData)([]byte,error){
var buf bytes.Buffer
// 得到字节数据的编码器
bufEnc := gob.NewEncoder(&buf)
// 对数据进行编码
if err := bufEnc.Encode(data); err != nil{
return nil, err
}
return buf.Bytes(),nil
}
// 解码
func decode(b []byte)(RPCData,error){
buf := bytes.NewBuffer(b)
// 返回字节数组解码器
bufDec := gob.NewDecoder(buf)
var data RPCData
// 对数据解码
if err := bufDec.Decode(&data); err != nil {
return data, err
}
return data,nil
}
五、实现RPC服务端
1、服务端接收到的数据需要包括什么?
- 调用的函数名,参数列表
- 一般会约定函数的第二个返回值是error类型
- 通过反射实现
2、服务端需要解决的问题是什么?
- Client调用时只传过来函数名,需要维护函数名到函数之间的map映射
3、服务端的核心功能有哪些?
- 维护函数名导函数反射值的map
- client端传函数名,参数列表后,服务端要解析为反反射值,调用执行
- 函数的返回值打包,并通过网络返回个客户端
package rpc
import (
"fmt"
"net"
"reflect"
)
// 声明服务端
type Server struct {
// 地址
addr string
// 服务端维护的函数名到函数反射值的map
funcs map[string]reflect.Value
}
// 创建服务端对象
func NewServer(addr string)*Server{
return &Server{
addr: addr,
funcs: make(map[string]reflect.Value),
}
}
// 服务端绑定注册方法
// 将函数名与函数真正实现对应起来
// 第一个参数为函数名,第二个传入真正的函数
func (s *Server) Register (rpcName string, f interface{}){
if _, ok := s.funcs[rpcName]; ok {
return
}
// map 中没有值,则将映射添加到map,便于调用
fVal := reflect.ValueOf(f)
s.funcs[rpcName] = fVal
}
// 服务端等待调用
func (s *Server) Run(){
// 监听
lis, err := net.Listen("tcp",s.addr)
if err != nil {
fmt.Printf("监听 %s err:%v\n",s.addr,err)
return
}
for {
// 拿到连接
conn ,err := lis.Accept()
if err != nil {
fmt.Printf("accept err :%v\n",err)
return
}
// 创建会话
srvSession := NewSession(conn)
// RPC读取数据
b,err := srvSession.Read()
if err != nil {
fmt.Printf("read err:%v\n",err)
return
}
// 对数据进行解码
rpcData, err := decode(b)
if err != nil {
fmt.Printf("decode err:%v\n", err)
return
}
// 根据读取到数据的Name,得到调用的函数名
f, ok := s.funcs[rpcData.Name]
if !ok{
fmt.Printf("函数 %s 不存在", rpcData.Name)
return
}
// 解析遍历客户端出来的参数,放到一个数组中
inArgs := make([]reflect.Value,0,len(rpcData.Args))
for _, arg := range rpcData.Args{
inArgs = append(inArgs, reflect.ValueOf(arg))
}
// 反射调用方法, 传入参数
out := f.Call(inArgs)
// 解析遍历执行结果,放到一个数组中
outArgs := make([]interface{},0,len(out))
for _, o := range out{
outArgs = append(outArgs, o.Interface())
}
// 包装数据,返回给客户端
respRPCData := RPCData{rpcData.Name,outArgs}
// 编码
respBytes, err := encode(respRPCData)
if err != nil {
fmt.Printf("encode err:%v\n",err)
return
}
// 使用rpc写出数据
err = srvSession.Write(respBytes)
if err != nil {
fmt.Printf("session write err :%v\n",err)
return
}
}
}
六、实现RPC客户端
- 客户端只有函数原型,使用
reflect.MakeFunc()
可以完成原型到函数的调用 reflect.MakeFunc()
是Client从函数原型到网络调度的关键
package rpc
import (
"net"
"reflect"
)
// 声明客户端
type Client struct{
conn net.Conn
}
// 创建客户端对象
func NewClient(conn net.Conn) * Client {
return &Client{conn:conn}
}
// 实现通用的RPC客户端
// 绑定RPC访问的方法
// 传入访问的函数名
// 函数具体实现在Server端,Client只有函数原型
// 使用MakeFunc()完成原型到函数的调用
// fPtr指向函数原型
// xxx.callRPC("queryUser", &query)
func (c *Client)callRPC(rpcName string, fPtr interface{}){
// 通过反射,获取fPtr未初始化的函数原型
fn := reflect.ValueOf(fPtr).Elem()
// 另一个函数,作用是对用是对第一个函数操作
// 完成与Server的交互
f := func(args []reflect.Value)[]reflect.Value{
// 处理输入的参数
inArgs := make([]interface{},0, len(args))
for _, arg := range args {
inArgs = append(inArgs,arg.Interface())
}
// 创建连接
cliSession := NewSession(c.conn)
// 编码数据
reqRPC := RPCData{Name: rpcName,Args:inArgs}
b, err := encode(reqRPC)
if err != nil {
panic(err)
}
// 写出数据
if err= cliSession.Write(b); err != nil {
panic(err)
}
// 读取响应数据
respBytes, err := cliSession.Read()
if err != nil{
panic(err)
}
// 解码数据
respRPC, err := decode(respBytes)
if err != nil {
panic(err)
}
// 处理服务端返回的数据
outArgs := make([]reflect.Value,0,len(respRPC.Args))
for i, arg := range respRPC.Args{
// 必须对nil进行处理
if arg == nil {
// 必须填充一个真正的类型,不能是nil
outArgs = append(outArgs,reflect.Zero(fn.Type().Out(i)))
continue
}
outArgs = append(outArgs,reflect.ValueOf(arg))
}
return outArgs
}
// 参数1: 一个未初始化函数的方法值,类型时reflect.Type
// 参数2: 另一个函数,作用是对第一个函数参数操作
// 返回reflect.Value 类型
// MakeFunc 使用传入的函数原型,创建一个绑定 参数2 的新函数
v := reflect.MakeFunc(fn.Type(), f)
// 为函数fPtr赋值
fn.Set(v)
}
七、实现RPC通信测试
- 给服务端注册一个查询用户的方法,客户端去RPC调用。
package rpc
import (
"encoding/gob"
"fmt"
"net"
"testing"
)
// 用户查询
// 用于测试的结构体
type User struct{
Name string
Age int
}
// 用于测试的查询用户的方法
func queryUser(uid int)(User, error){
user := make(map[int]User)
user[0] = User{"zs",20}
user[1] = User{"ls",20}
user[2] = User{"ws",20}
// 模拟查询用户
if u, ok := user[uid]; ok {
return u, nil
}
return User{}, fmt.Errorf("ud %d not in user db", uid)
}
// 测试方法
func TestRPC(t *testing.T){
// 需要对interface{}可能产生的类型进行注册
gob.Register(User{})
addr := "127.0.0.1:8000"
// 创建服务端
srv := NewServer(addr)
// 将方法注册到服务端
srv.Register("queryUser", queryUser)
// 服务端等待调用
go srv.Run()
// 客户端获取连接
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Error(err)
}
// 创建客户端
cli := NewClient(conn)
// 声明函数原型
var query func(int)(User,error)
cli.callRPC("queryUser", &query)
// 得到查询结果
u, err := query(1)
if err != nil {
t.Fatal(err)
}
fmt.Println(u)
}
Golang实现RPC的更多相关文章
- github上的golang双向rpc,基于原生“net/rpc”库实现,可以注册回调
github上的golang双向rpc,基于原生“net/rpc”库实现,可以注册回调.仅支持一个server和一个client交互. 地址:https://github.com/rocket049/ ...
- golang高性能RPC:Apache Thrift安装使用完全攻略
在企业应用中RPC的使用可以说是十分的广泛,使用该技术可以方便的与各种程序交互而不用考虑其编写使用的语言. 如果你对RPC的概念还不太清楚,可以点击这里. 现今市面上已经有许多应用广泛的RPC框架,比 ...
- golang——net/rpc/jsonrpc包学习
1.jsonrpc包 该实现了JSON-RPC的ClientCodec和ServerCodec接口,可用于rpc包. 可用于跨语言使用go rpc服务. 2.常用方法 (1)func Dial(net ...
- golang——net/rpc包学习
1.rpc包 rpc包提供了通过网络或其他I/O连接对一个对象的导出方法的访问. 只有满足如下标准的方法才能用于远程访问,其余方法会被忽略: (1)方法是导出的(2)方法有两个参数,都是导出类型或内建 ...
- golang中的rpc包用法
RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样. 我所在公司的项目是采用基于Restful的微服务架构,随着微服 ...
- Golang入门教程(十六)Goridge -高性能的 PHP-to-Golang RPC编解码器库
什么是 RPC 框架? RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC协议假定某些传输协议的存在 ...
- golang rpc 简单范例
RPC(Remote Procedure Call Protocol)--远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议. 它的工作流程如下图: go ...
- golang中的RPC开发-2
RPC简介 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程 如果 ...
- golang中的rpc开发
golang中实现RPC非常简单,官方提供了封装好的库,还有一些第三方的库 golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp和http数据传输方式,由于其他语言不支 ...
- 【GoLang】golang 微服务框架 介绍
原文如下: rpcx是一个类似阿里巴巴 Dubbo 和微博 Motan 的分布式的RPC服务框架,基于Golang net/rpc实现. 谈起分布式的RPC框架,比较出名的是阿里巴巴的dubbo,包括 ...
随机推荐
- Qt使用QAudioInput、QAudioOutput实现局域网的音频通话
Qt使用QAudioInput.QAudioOutput实现局域网的音频通话 本文旨在介绍一下用Qt来实现局域网音频通话功能 文章目录 Qt使用QAudioInput.QAudioOutput实现局域 ...
- Qt QHeaderView 添加复选框
QT QTableView 表头添加复选框 最近在做表格,用QTableView,然后有一个需求是给表格添加表头,但是没有一个API能够在表头添加复选框,基本都是来重载QHeaderView,有两种方 ...
- Golang入门:协程(goroutine)
goroutine goroutine 是 Go 的并发模型的核心概念.为了理解 goroutine,我们来定义几个术语.第一个是进程.进程是程序的实例,由计算机的操作系统运行.操作系统将一些资源(如 ...
- 鸿蒙NEXT开发案例:程序员计算器
[环境准备] • 操作系统:Windows 10 • 开发工具:DevEco Studio 5.0.1 Release Build Version: 5.0.5.306 • 目标设备:华为Mate60 ...
- Redis 高可用方案
本文分享自天翼云开发者社区<Redis 高可用方案>,作者:芋泥麻薯 一.常见使用方式 Redis的几种常见使用方式包括: Redis单副本: • Redis多副本(主从): • Redi ...
- [每日算法] leetcode第3题:无重复字符的最长子串
leetcode第3题入口 题目描述 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: s = "abcabcbb" 输出: 3 解法1: ...
- 区块链特辑——solidity语言基础(五)
Solidity语法基础学习 九.实战项目(一): 学以致用 UP主捐款合约 ·合约要求: ·建立时,需将合约的建立者设定成owner(constructor,msg.sender) ·需记录每个观众 ...
- C++数据的共享和保护
1.函数原型作用域:C++中最小的作用域 ①在函数原型声明时,形参的作用范围就是函数原型作用域. 2.局部作用域/块作用域 3.类作用域 类可以被看做是一组有名成员的集合,类X的成员m具有类作用域,对 ...
- K8S 部署 Deepseek 要 3 天?别逗了!Ollama+GPU Operator 1 小时搞定
最近一年我都在依赖大模型辅助工作,比如 DeepSeek.豆包.Qwen等等.线上大模型确实方便,敲几个字就能生成文案.写代码.做表格,极大提高了效率.但对于企业来说:公司内部数据敏感.使用外部大模型 ...
- DPDI(Dispatch PDI)kettle调度管理平台之实操演练第001讲--手工调度本地PDI任务生成日期维度数据
DPDI实操演练第一讲 1.DPDI简介 DPDI Online 您的智能ETL任务调度专家 DPDI Online 是一款基于Kettle的强大在线任务调度平台,凭借其高效与灵活性,专为调度和监控K ...