1. RPC 简介

远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议

该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程

如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用

2. 流行 RPC 框架的对比

3. golang 中如何实现 RPC

golang 中实现 RPC 非常简单,官方提供了封装好的库,还有一些第三方的库

golang 官方的 net/rpc 库使用 encoding/gob 进行编解码,支持 tcp 和 http 数据传输方式,由于其他语言不支持 gob 编解码方式,所以 golang 的 RPC 只支持 golang 开发的服务器与客户端之间的交互

官方还提供了net/rpc/jsonrpc 库实现RPC 方法,jsonrpc 采用JSON 进行数据编解码,因而支持跨语言调用,目前 jsonrpc 库是基于 tcp 协议实现的,暂不支持 http 传输方式

golang 的 RPC 必须符合 4 个条件才可以

结构体字段首字母要大写,要跨域访问,所以大写

函数名必须首字母大写(可以序列号导出的)

函数第一个参数是接收参数,第二个参数是返回给客户端参数,必须是指针类型

函数必须有一个返回值 error

例题:golang 实现 RPC 程序,实现求矩形面积和周长

   server端

package main

import (
"fmt"
"log"
"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() {
// 1. 注册服务
rect := new(Rect)
rpc.Register(rect)
// 2.把服务处理绑定到http协议上
rpc.HandleHTTP()
fmt.Println("------ rpc service is already on ------")
// 3. 监听服务,等待客户端调用求周长和面积的方法
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}

 客户端

package main

import (
"fmt"
"log"
"net/rpc"
) type Params struct {
Width, Height int
} // 调用服务
func main(){
// 1. 连接远程RPC服务
rp, err := rpc.DialHTTP("tcp","182.254.179.186:8080")
if err != nil {
log.Fatal(err)
}
// 2.调用远程方法
// 定义接收服务器传回来的计算结果的值
ret := 0
err2 := rp.Call("Rect.Area",Params{50,100},&ret)
if err2!=nil {
log.Fatal(err2)
}
fmt.Println("面积",ret) //求周长
err3:= rp.Call("Rect.Perimeter",Params{50,100},&ret)
if err3!=nil {
log.Fatal(err3)
}
fmt.Println("周长",ret) }

练习:模仿前面例题,自己实现 RPC 程序,服务端接收 2 个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下:

package main

import (
"errors"
"fmt"
"log"
"net/http"
"net/rpc"
) type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
//乘积
Pro int
// 商
Quo int
// 余数
Rem int
}
//乘法
func (this *Arith)Multiply(req ArithRequest,res *ArithResponse)error {
res.Pro = req.A * req.B
return nil
}
// 商和余数
func (this *Arith)Divide(req *ArithRequest, res *ArithResponse)error{
if req.B == 0 {
return errors.New("出书不能为0")
}
//商
res.Quo = req.A / req.B
//余数
res.Rem = req.A % req.B
return nil
} func main() {
//注册服务
rpc.Register(new(Arith))
// 采用http作为rpc载体
rpc.HandleHTTP()
fmt.Println("------ rpc service is already on ------")
// 监听服务,等待客户端调用响应的方法
err := http.ListenAndServe(":8081",nil)
if err!=nil {
log.Fatal(err)
}
}

server.go

package main

import (
"fmt"
"log"
"net/rpc"
) type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
//乘积
Pro int
// 商
Quo int
// 余数
Rem int
} func main() {
conn, err := rpc.DialHTTP("tcp", "182.254.179.186:8081")
//conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8081")
if err!= nil{
log.Fatal(err)
}
req := ArithRequest{9, 2}
var res ArithResponse
err2 := conn.Call("Arith.Multiply",req, &res)
if err2 != nil {
log.Println(err2)
}
fmt.Printf("%d * %d = %d\n", req.A,req.B,res.Pro)
err3 := conn.Call("Arith.Divide",req, &res)
if err3 != nil {
log.Println(err3)
}
fmt.Printf("%d / %d = %d,%d", req.A,req.B,res.Quo,res.Rem)
}

client.go

另外,net/rpc/jsonrpc 库通过 json 格式编解码,支持跨语言调用

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 (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
} // 商和余数
func (this *Arith) Divide(req *ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("出书不能为0")
}
//商
res.Quo = req.A / req.B
//余数
res.Rem = req.A % req.B
return nil
} func main() {
//注册服务
rpc.Register(new(Arith))
// 采用http作为rpc载体
fmt.Println("------ jsonrpc service is already on ------")
// 监听服务,等待客户端调用响应的方法
listener, err := net.Listen("tcp","127.0.0.1:8081")
if err != nil {
log.Fatal(err)
}
//循环监听服务
for {
conn, err := listener.Accept()
if err != nil {
continue
}
//协程
go func(conn net.Conn) {
fmt.Println("a new client visit -----")
jsonrpc.ServeConn(conn)
}(conn)
}
}

server.go

package main

import (
"fmt"
"log"
"net/rpc/jsonrpc"
) type Arith struct {
}
// 声明接收的参数结构体
type ArithRequest struct {
A,B int
}
// 声明返回客户端的参数结构体
type ArithResponse struct {
//乘积
Pro int
// 商
Quo int
// 余数
Rem int
} func main() {
conn, err := jsonrpc.Dial("tcp", "182.254.179.186:8081")
//conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8081")
if err!= nil{
log.Fatal(err)
}
req := ArithRequest{9, 2}
var res ArithResponse
err2 := conn.Call("Arith.Multiply",req, &res)
if err2 != nil {
log.Println(err2)
}
fmt.Printf("%d * %d = %d\n", req.A,req.B,res.Pro)
err3 := conn.Call("Arith.Divide",req, &res)
if err3 != nil {
log.Println(err3)
}
fmt.Printf("%d / %d = %d,%d", req.A,req.B,res.Quo,res.Rem)

client.go

4. RPC 调用流程

微服务架构下数据交互一般是对内 RPC,对外 REST

将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC  框架的使用、后期的服务监控等工作

一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用

5. 网络传输数据格式

成熟的 RPC 框架会有自定义传输协议,这里网络传输格式定义如下,前面是固定长度消息头,后面是变长消息体

6. 实现 RPC 服务端

服务端接收到的数据需要包括什么?

调用的函数名、参数列表

一般会约定函数的第二个返回值是 error 类型

通过反射实现

服务端需要解决的问题是什么?

Client 调用时只传过来函数名,需要维护函数名到函数之间的 map 映射

服务端的核心功能有哪些?

维护函数名到函数反射值的 map

client 端传函数名、参数列表后,服务端要解析为反射值,调用执行

函数的返回值打包,并通过网络返回给客户端

7. 实现 RPC 客户端

客户端只有函数原型,使用reflect.MakeFunc()  可以完成原型到函数的调用

reflect.MakeFunc()是 Client 从函数原型到网络调用的关键

8. 实现 RPC 通信测试

给服务端注册一个查询用户的方法,客户端去 RPC 调用

package main

import (
"encoding/binary"
"io"
"log"
"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{
log.Fatal(err)
}
return nil
} // 从连接中读取数据
func (s *Session)Read()([]byte, error) {
// 读取头部长度
header := make([]byte,4)
// 按头部长度,读取头部数据
_,err := io.ReadFull(s.conn, header)
if err != nil{
log.Fatal(err)
}
// 读取数据长度
datalen := binary.BigEndian.Uint32(header)
// 按照数据长度去读取数据
data := make([]byte, datalen)
_,err = io.ReadFull(s.conn, data)
if err != nil{
log.Fatal(err)
}
return data,nil
}

session.go

package main

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.NewReader(b)
// 返回字节数组解码器
bufDec := gob.NewDecoder(buf)
var data RPCData
// 对数据解码
if err := bufDec.Decode(&data); err!= nil{
return data,err
}
return data, nil
}

codec.go

package main

import (
"fmt"
"net"
"reflect"
) //声明服务器
type Server struct {
// 地址
addr string
// 服务端维护的函数名到函数反射值的map
funcs map[string]reflect.Value
}
// 创建服务端对象
func NewServer(addr string)*Server {
return &Server{addr,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", s.addr, err)
return
}
for {
//拿到连接
conn,err := lis.Accept()
if err != nil {
fmt.Printf("accept err:%v",err)
}
// 创建会话
srvSession := NewSession(conn)
// RPC读取数据
b, err := srvSession.Read()
if err != nil {
fmt.Printf("read err:%v", err)
}
// 对数据解码
rpcData, err := decode(b)
if err != nil {
fmt.Printf("read err:%v", err)
}
// 根据读取到的数据的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", err)
return
}
// 使用rpc写出数据
err = srvSession.Write(respBytes)
if err != nil {
fmt.Printf("session write err:%v", err)
return
}
}
}

server.go

package main

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()
// 另一个函数,作用是对第一个函数参数操作
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{rpcName, inArgs}
b, err := encode(reqRPC)
if err != nil {
panic(err)
}
// 写出数据
err = cliSession.Write(b)
if 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)
}

client.go

  • simple_rpc_test.go
package main

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{"张三",20}
user[1] = User{"李四",21}
user[2] = User{"王五",22}
// 模拟查询用户
if u, ok := user[uid];ok {
return u,nil
}
return User{},fmt.Errorf("id %d not in user db",uid)
} // 测试方法
func TestRPC(t *testing.T) {
// 需要对interface{}可能产生的类型进行注册
gob.Register(User{})
addr := "127.0.0.1:8080"
// 创建服务端
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)
}

  • 测试读写
package main

import (
"fmt"
"log"
"net"
"sync"
"testing"
) // 测试读写
func TestSession_ReadWrite(t *testing.T) {
// 定义监听IP和端口
addr := "127.0.0.1:8000"
//定义传输的数据
my_data := "hello"
// 等待组
wg := sync.WaitGroup{}
// 协程1个读,1个写
wg.Add(2)
// 写数据协程
go func() {
defer wg.Done()
//创建rpc连接
listener,err := net.Listen("tcp",addr)
if err!=nil {
log.Fatal(err)
}
conn,_ := listener.Accept()
s := Session{conn:conn}
// 写数据
err = s.Write([]byte(my_data))
if err!=nil {
log.Fatal(err)
}
}()
// 读数据协程
go func() {
defer wg.Done()
conn,err := net.Dial("tcp",addr)
if err!=nil {
log.Fatal(err)
}
s := Session{conn:conn}
// 读数据
data, err := s.Read()
if err!=nil {
log.Fatal(err)
}
if string(data) != my_data {
t.Fatal(err)
}
fmt.Println(string(data))
}()
wg.Wait()
}

微服务架构攀登之路(二)之RPC的更多相关文章

  1. 微服务架构攀登之路(三)之gRPC入门

    一.gRPC入门 1. gRPC 简介 gRPC 由 google 开发,是一款语言中立.平台中立.开源的远程过程调用系统 gRPC 客户端和服务端可以在多种环境中运行和交互,例如用 java 写一个 ...

  2. 微服务架构攀登之路(五)之Go-micro入门

    一.go-micro入门 1. go-micro 简介 Go Micro 是一个插件化的基础框架,基于此可以构建微服务,Micro 的设计哲学是可插拔的插件化架构 在架构之外,它默认实现了 consu ...

  3. 微服务架构攀登之路(四)之使用gRPC构建微服务

    做一个处理用户信息的微服务 客户端通过用户名,可以从服务端查询用户的基本信息 gRPC proto user.proto 定义客户端请求.服务端响应的数据格式 user.pb.go 自动生成的,为数据 ...

  4. 从 Spring Cloud 开始,聊聊微服务架构实践之路

    [编者的话]随着公司业务量的飞速发展,平台面临的挑战已经远远大于业务,需求量不断增加,技术人员数量增加,面临的复杂度也大大增加.在这个背景下,平台的技术架构也完成了从传统的单体应用到微服务化的演进. ...

  5. 离不开的微服务架构,脱不开的RPC细节

    服务化有什么好处? 服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队的技术解耦,如下图所示: 服务A:欧洲团队维护,技术背景是Java 服务B:美洲团队维护,用C++实现 ...

  6. 离不开的微服务架构,脱不开的RPC细节(值得收藏)!!!

    服务化有什么好处? 服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队的技术解耦,如下图所示: 服务A:欧洲团队维护,技术背景是Java 服务B:美洲团队维护,用C++实现 ...

  7. Re:从0开始的微服务架构:(一)重识微服务架构--转

    原文地址:http://www.infoq.com/cn/articles/micro-service-architecture-from-zero?utm_source=infoq&utm_ ...

  8. spring cloud+dotnet core搭建微服务架构:服务发现(二)

    前言 上篇文章实际上只讲了服务治理中的服务注册,服务与服务之间如何调用呢?传统的方式,服务A调用服务B,那么服务A访问的是服务B的负载均衡地址,通过负载均衡来指向到服务B的真实地址,上篇文章已经说了这 ...

  9. JHipster生成微服务架构的应用栈(二)- 认证微服务示例

    本系列文章演示如何用JHipster生成一个微服务架构风格的应用栈. 环境需求:安装好JHipster开发环境的CentOS 7.4(参考这里) 应用栈名称:appstack 认证微服务: uaa 业 ...

随机推荐

  1. HDC2021技术分论坛:进程崩溃/应用卡死,故障频频怎么办?

    ​作者:jiwenqiang,DFX技术专家 提到开发一个产品,我们通常首先想到的是要实现什么样的功能,但是除了功能之外,非功能属性也会很大程度上影响一个产品的体验效果,比如不定时出现的应用卡死.崩溃 ...

  2. 【阿菜漏洞复现】DeFi 平台 MonoX Finance 漏洞分析及复现

    前言 2021 年 11 ⽉ 30 ⽇,DeFi 平台 MonoX Finance 遭遇攻击,损失共计约 3100 万美元. 造成本次攻击的漏洞主要有两个: 移除流动性的函数未对调用者进行检测,使得任 ...

  3. Moment.js使用笔记

    零.前情提要 上个月开发了数据平台,用的框架是vue + Ant Design of Vue,其中用了组件[range-picker]日期选择框,涉及到时间方法就去看了momentJS,以此记录~ 如 ...

  4. Linux下编译生成SO并进行调用执行

    Linux下编译生成SO并进行调用执行 参考博客的博客: C编译: 动态连接库 (.so文件) - Vamei - 博客园 (cnblogs.com) C 多个动态库存在同名函数问题处理方法:-fvi ...

  5. UVA12412 师兄帮帮忙 A Typical Homework (a.k.a Shi Xiong Bang Bang Mang) 题解

    Content 自己去看题面去. Solution 算不上很繁琐的一道大模拟. 首先,既然是输出 \(0\) 才退出,那么在此之前程序应当会执行菜单 \(\Rightarrow\) 子操作 \(\Ri ...

  6. 在JSP页面里,时间控件的JS位置在下面然后就显示不出来

    在JSP页面里,时间空间的JS位置在下面然后就显示不出来,放到前面然后就显示出来了, 情何以堪啊,开始还以为是什么错误.

  7. SpringBoot整合quartz框架启动定时任务报错:the given trigger will never fire.

    org.quartz.SchedulerException: Based on configured schedule, the given trigger 'DEFAULT.cron_b1a91e1 ...

  8. 关于Spring MVC的问题

    一.SpringMVC的流程是什么? 1. 用户发送请求至前端控制器DispatcherServlet: 2. DispatcherServlet收到请求后,调用HandlerMapping处理器映射 ...

  9. windows10下vscode+cmake编译Qt5代码(2)

    概述 本文依赖 前文 本文将介绍使用cmake语法屏蔽运行Qt exe的cmd窗口 解决办法 配置VS项目属性, 缺点: cmake重新 configure后,项目属性会重置,再次运行还将出现cmd弹 ...

  10. 【LeetCode】1005. Maximize Sum Of Array After K Negations 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 小根堆 日期 题目地址:https://leetco ...