RPC简介

  • 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议
  • 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
  • 如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用

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程序,实现求矩形面积和周长

  • 服务端
package main

import (
"net/http"
"net/rpc"
) type Params struct {
Width, Height int
} type Rect struct {} // 计算面积
func (t Rect) Area(p Params, area *int) error {
*area = p.Width * p.Height
return nil
}
// 计算周长
func (t Rect) Perimeter(p Params, perimeter *int) error {
*perimeter = (p.Width + p.Height) * 2
return nil
} func main() {
// 注册服务
rect := new(Rect)
_ = rpc.Register(rect)
// 服务处理绑定到http协议上
rpc.HandleHTTP()
// 监听服务
_ = http.ListenAndServe(":8000", nil)
}
  • 客户端
package main

import (
"fmt"
"net/rpc"
) type Params struct {
Width, Height int
} func main() {
// 连接远程RPC服务
client, _ := rpc.DialHTTP("tcp", ":8000")
var params = Params{10, 5} // 调用方法
var areaRet int
_ = client.Call("Rect.Area", params, &areaRet)
fmt.Println("area:", areaRet) // 调用方法
var perimeterRet = new(int)
_ = client.Call("Rect.Perimeter", params, perimeterRet)
fmt.Println("perimeter:", *perimeterRet) }
  • golang写RPC程序,必须符合4个基本条件,不然RPC用不了

    1. 结构体字段首字母要大写,可以别人调用
    2. 函数名必须首字母大写
    3. 函数第一参数是接收参数,第二个参数是返回给客户端的参数,必须是指针类型
    4. 函数还必须有一个返回值error
  • 练习:模仿前面例题,自己实现RPC程序,服务端接收2个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下:

  1. 服务端代码
点击查看代码
package main

import (
"net/http"
"net/rpc"
) // 用于注册的
type Arith struct {} // 请求结构体
type ArithRequest struct {
A, B int
} // 响应结构体
type ArithResponse struct {
Cheng int
Chu int
Yu int
} // 乘法计算
func (a Arith) Cheng(request ArithRequest, response *ArithResponse) error {
response.Cheng = request.A * request.B
return nil
}
// 商和余数计算
func (a Arith) Divide(request ArithRequest, response *ArithResponse) error {
// 除
response.Chu = request.A / request.B
// 取模
response.Yu = request.A % request.B
return nil
} func main() {
// 注册服务
rect := new(Arith)
_ = rpc.Register(rect)
// 服务处理绑定到http协议上
rpc.HandleHTTP()
// 监听服务
_ = http.ListenAndServe(":8000", nil)
}
  1. 客户端代码
点击查看代码
package main

import (
"fmt"
"net/rpc"
) // 请求结构体
type ArithRequest struct {
A, B int
} // 响应结构体
type ArithResponse struct {
Cheng int
Chu int
Yu int
} func main() {
// 连接RPC服务器
client, _ := rpc.DialHTTP("tcp", ":8000") var arithRequest = ArithRequest{20, 8}
var arithResponse = new(ArithResponse) // 调用乘方法
_ = client.Call("Arith.Cheng", arithRequest, arithResponse)
fmt.Println(*arithResponse) // 调用除和取余方法
_ = client.Call("Arith.Divide", arithRequest, arithResponse)
fmt.Println(*arithResponse) }
  • 另外,net/rpc/jsonrpc库通过json格式编解码,支持跨语言调用
  1. go服务端代码
package main

import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
) type Hello struct {}
func (h *Hello) Hello(req string, rsp *string) error {
*rsp = "hello " + req
return nil
} func main() {
// 注册服务
_ = rpc.Register(new(Hello))
listener, _ := net.Listen("tcp", ":8000")
for {
conn, _ := listener.Accept()
// 服务客户端
go jsonrpc.ServeConn(conn)
}
}
  1. go客户端代码
import (
"fmt"
"net/rpc/jsonrpc"
) func main() {
client, _ := jsonrpc.Dial("tcp", ":8000")
var rsp = new(string)
_ = client.Call("Hello.Hello", "张三", rsp)
fmt.Println(*rsp)
}
  1. python客户端代码
import socket
import json data = {
"id": 100,
"method": "Hello.Hello",
"params": ["懂事儿"],
} client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8000))
client.send(json.dumps(data).encode())
ret = json.loads(client.recv(1024).decode())
print(ret, type(ret))

python打印结果:{'id': 100, 'result': 'hello 懂事儿', 'error': None} <class 'dict'>

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.BigEndian.PutUint32(buf[:4], uint32(len(data)))
// 将整个数据,放到4后边,将源片复制到目标片,前面是目标片,后面是源片
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 (
"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服务端

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

调用的函数名、参数列表,还有一个返回值error类型

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

Map维护客户端传来调用函数,服务端知道去调谁

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

维护函数map

客户端传来的东西进行解析

函数的返回值打包,传给客户端

点击查看代码
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)}
} // 服务端需要一个注册Register
// 第一个参数函数名,第二个传入真正的函数
func (s *Server) Register(rpcName string, f interface{}) {
// 维护一个map
// 若map已经有键了
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 {
return
}
serSession := NewSession(conn)
// 使用RPC方式读取数据
b, err := serSession.Read()
if err != nil {
return
}
// 数据解码
rpcData, err := decode(b)
if err != nil {
return
}
// 根据读到的name,得到要调用的函数
f, ok := s.funcs[rpcData.Name]
if !ok {
fmt.Println("函数 %s 不存在", rpcData.Name)
return
}
// 遍历解析客户端传来的参数,放切片里
inArgs := make([]reflect.Value, 0, len(rpcData.Args))
for _, arg := range rpcData.Args {
inArgs = append(inArgs, reflect.ValueOf(arg))
}
// 反射调用方法
// 返回Value类型,用于给客户端传递返回结果,out是所有的返回结果
out := f.Call(inArgs)
// 遍历out ,用于返回给客户端,存到一个切片里
outArgs := make([]interface{}, 0, len(out))
for _, o := range out {
outArgs = append(outArgs, o.Interface())
}
// 数据编码,返回给客户端
respRPCData := RPCData{rpcData.Name, outArgs}
bytes, err := encode(respRPCData)
if err != nil {
return
}
// 将服务端编码后的数据,写出到客户端
err = serSession.Write(bytes)
if err != nil {
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客户端
// 传入访问的函数名
// fPtr指向的是函数原型
//var select fun xx(User)
//cli.callRPC("selectUser",&select)
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{Name: rpcName, Args: 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 {
// reflect.Zero()会返回类型的零值的value
// .out()会返回函数输出的参数类型
outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
continue
}
outArgs = append(outArgs, reflect.ValueOf(arg))
}
return outArgs
}
// 完成原型到函数调用的内部转换
// 参数1是reflect.Type
// 参数2 f是函数类型,是对于参数1 fn函数的操作
// fn是定义,f是具体操作
v := reflect.MakeFunc(fn.Type(), f)
// 为函数fPtr赋值,过程
fn.Set(v)
}

实现RPC通信

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

package main

import (
"encoding/gob"
"fmt"
"ginSourceCode/rpc"
"net"
) // 定义用户对象
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", 21}
user[2] = User{"ww", 22}
// 模拟查询用户
if u, ok := user[uid]; ok {
return u, nil
}
return User{}, fmt.Errorf("%d err", uid)
} func main() {
// 编码中有一个字段是interface{}时,要注册一下
gob.Register(User{})
addr := "127.0.0.1:8000"
// 创建服务端
srv := rpc.NewServer(addr)
// 将服务端方法,注册一下
srv.Register("queryUser", queryUser)
// 服务端等待调用
go srv.Run()
// 客户端获取连接
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Println("err")
}
// 创建客户端对象
cli := rpc.NewClient(conn)
// 需要声明函数原型
var query func(int) (User, error)
cli.CallRPC("queryUser", &query)
// 得到查询结果
u, err := query(0)
if err != nil {
fmt.Println("err")
}
fmt.Println(u)
}

参考链接

最新文章

  1. CoolPlist 帧动画自动生成工具
  2. Android 坐标系和 MotionEvent 分析、滑动
  3. Elasticsearch安装
  4. hdu 3518 (后缀数组)
  5. oracle控制文件丢失恢复
  6. 仿SiteMap实现Asp.net 网站的菜单和权限管理
  7. 在子线程中使用runloop,正确操作NSTimer计时的注意点 三种可选方法
  8. linux命令行下导出导入.sql文件
  9. WPF中的WebBrowser
  10. JSOI2008星球大战(并查集)
  11. 2015第37周五javascript函数arguments对象巧用一
  12. HDOJ 2052 Picture
  13. C# VS 面向对象基础(一)
  14. centOS6.4安装python3.5,并且安装pip
  15. 带你重拾JavaScript(2)之console的你所不知道的功能
  16. 详细教程:将本地项目上传到github
  17. 【做题】SRM704 Div1 Median - ModEquation——数论
  18. JAVA线程池ScheduledExecutorService周期性地执行任务 与单个Thread周期性执行任务的异常处理
  19. idea 配置 maven 项目
  20. Python中os.system和os.popen区别

热门文章

  1. JAVA将Object对象转byte数组
  2. IDEA微服务项目SpringBoot一键(批量)顺序启动
  3. 【经验】 Java BigInteger类以及其在算法题中的应用
  4. 【剑指Offer】07. 重建二叉树 解题报告(Java & Python & C++)
  5. 【LeetCode】693. Binary Number with Alternating Bits 解题报告(Python)
  6. 【LeetCode】388. Longest Absolute File Path 解题报告(Python)
  7. Network (poj1144)
  8. AI实战分享 | 基于CANN的辅助驾驶应用案例
  9. Java代码性能优化
  10. 使用zTree插件实现可拖拽的树