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

  • 服务端
  1. package main
  2. import (
  3. "net/http"
  4. "net/rpc"
  5. )
  6. type Params struct {
  7. Width, Height int
  8. }
  9. type Rect struct {}
  10. // 计算面积
  11. func (t Rect) Area(p Params, area *int) error {
  12. *area = p.Width * p.Height
  13. return nil
  14. }
  15. // 计算周长
  16. func (t Rect) Perimeter(p Params, perimeter *int) error {
  17. *perimeter = (p.Width + p.Height) * 2
  18. return nil
  19. }
  20. func main() {
  21. // 注册服务
  22. rect := new(Rect)
  23. _ = rpc.Register(rect)
  24. // 服务处理绑定到http协议上
  25. rpc.HandleHTTP()
  26. // 监听服务
  27. _ = http.ListenAndServe(":8000", nil)
  28. }
  • 客户端
  1. package main
  2. import (
  3. "fmt"
  4. "net/rpc"
  5. )
  6. type Params struct {
  7. Width, Height int
  8. }
  9. func main() {
  10. // 连接远程RPC服务
  11. client, _ := rpc.DialHTTP("tcp", ":8000")
  12. var params = Params{10, 5}
  13. // 调用方法
  14. var areaRet int
  15. _ = client.Call("Rect.Area", params, &areaRet)
  16. fmt.Println("area:", areaRet)
  17. // 调用方法
  18. var perimeterRet = new(int)
  19. _ = client.Call("Rect.Perimeter", params, perimeterRet)
  20. fmt.Println("perimeter:", *perimeterRet)
  21. }
  • golang写RPC程序,必须符合4个基本条件,不然RPC用不了

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

  1. 服务端代码
点击查看代码
  1. package main
  2. import (
  3. "net/http"
  4. "net/rpc"
  5. )
  6. // 用于注册的
  7. type Arith struct {}
  8. // 请求结构体
  9. type ArithRequest struct {
  10. A, B int
  11. }
  12. // 响应结构体
  13. type ArithResponse struct {
  14. Cheng int
  15. Chu int
  16. Yu int
  17. }
  18. // 乘法计算
  19. func (a Arith) Cheng(request ArithRequest, response *ArithResponse) error {
  20. response.Cheng = request.A * request.B
  21. return nil
  22. }
  23. // 商和余数计算
  24. func (a Arith) Divide(request ArithRequest, response *ArithResponse) error {
  25. // 除
  26. response.Chu = request.A / request.B
  27. // 取模
  28. response.Yu = request.A % request.B
  29. return nil
  30. }
  31. func main() {
  32. // 注册服务
  33. rect := new(Arith)
  34. _ = rpc.Register(rect)
  35. // 服务处理绑定到http协议上
  36. rpc.HandleHTTP()
  37. // 监听服务
  38. _ = http.ListenAndServe(":8000", nil)
  39. }
  1. 客户端代码
点击查看代码
  1. package main
  2. import (
  3. "fmt"
  4. "net/rpc"
  5. )
  6. // 请求结构体
  7. type ArithRequest struct {
  8. A, B int
  9. }
  10. // 响应结构体
  11. type ArithResponse struct {
  12. Cheng int
  13. Chu int
  14. Yu int
  15. }
  16. func main() {
  17. // 连接RPC服务器
  18. client, _ := rpc.DialHTTP("tcp", ":8000")
  19. var arithRequest = ArithRequest{20, 8}
  20. var arithResponse = new(ArithResponse)
  21. // 调用乘方法
  22. _ = client.Call("Arith.Cheng", arithRequest, arithResponse)
  23. fmt.Println(*arithResponse)
  24. // 调用除和取余方法
  25. _ = client.Call("Arith.Divide", arithRequest, arithResponse)
  26. fmt.Println(*arithResponse)
  27. }
  • 另外,net/rpc/jsonrpc库通过json格式编解码,支持跨语言调用
  1. go服务端代码
  1. package main
  2. import (
  3. "net"
  4. "net/rpc"
  5. "net/rpc/jsonrpc"
  6. )
  7. type Hello struct {}
  8. func (h *Hello) Hello(req string, rsp *string) error {
  9. *rsp = "hello " + req
  10. return nil
  11. }
  12. func main() {
  13. // 注册服务
  14. _ = rpc.Register(new(Hello))
  15. listener, _ := net.Listen("tcp", ":8000")
  16. for {
  17. conn, _ := listener.Accept()
  18. // 服务客户端
  19. go jsonrpc.ServeConn(conn)
  20. }
  21. }
  1. go客户端代码
  1. import (
  2. "fmt"
  3. "net/rpc/jsonrpc"
  4. )
  5. func main() {
  6. client, _ := jsonrpc.Dial("tcp", ":8000")
  7. var rsp = new(string)
  8. _ = client.Call("Hello.Hello", "张三", rsp)
  9. fmt.Println(*rsp)
  10. }
  1. python客户端代码
  1. import socket
  2. import json
  3. data = {
  4. "id": 100,
  5. "method": "Hello.Hello",
  6. "params": ["懂事儿"],
  7. }
  8. client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
  9. client.connect(("127.0.0.1", 8000))
  10. client.send(json.dumps(data).encode())
  11. ret = json.loads(client.recv(1024).decode())
  12. print(ret, type(ret))

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

RPC调用流程

  • 微服务架构下数据交互一般是对内 RPC,对外 REST
  • 将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC 框架的使用、后期的服务监控等工作
  • 一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用

网络传输数据格式

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

自己定义数据格式的读写

点击查看代码
  1. package rpc
  2. import (
  3. "encoding/binary"
  4. "io"
  5. "net"
  6. )
  7. // 测试网络中读写数据的情况
  8. // 会话连接的结构体
  9. type Session struct {
  10. conn net.Conn
  11. }
  12. // 构造方法
  13. func NewSession(conn net.Conn) *Session {
  14. return &Session{conn: conn}
  15. }
  16. // 向连接中去写数据
  17. func (s *Session) Write(data []byte) error {
  18. // 定义写数据的格式
  19. // 4字节头部 + 可变体的长度
  20. buf := make([]byte, 4+len(data))
  21. // 写入头部,记录数据长度
  22. binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))
  23. // 将整个数据,放到4后边,将源片复制到目标片,前面是目标片,后面是源片
  24. copy(buf[4:], data)
  25. _, err := s.conn.Write(buf)
  26. if err != nil {
  27. return err
  28. }
  29. return nil
  30. }
  31. // 从连接读数据
  32. func (s *Session) Read() ([]byte, error) {
  33. // 读取头部记录的长度
  34. header := make([]byte, 4)
  35. // 按长度读取消息
  36. _, err := io.ReadFull(s.conn, header)
  37. if err != nil {
  38. return nil, err
  39. }
  40. // 读取数据
  41. dataLen := binary.BigEndian.Uint32(header)
  42. data := make([]byte, dataLen)
  43. _, err = io.ReadFull(s.conn, data)
  44. if err != nil {
  45. return nil, err
  46. }
  47. return data, nil
  48. }

编码和解码

点击查看代码
  1. package rpc
  2. import (
  3. "bytes"
  4. "encoding/gob"
  5. )
  6. // 定义RPC交互的数据结构
  7. type RPCData struct {
  8. // 访问的函数
  9. Name string
  10. // 访问时的参数
  11. Args []interface{}
  12. }
  13. // 编码
  14. func encode(data RPCData) ([]byte, error) {
  15. //得到字节数组的编码器
  16. var buf bytes.Buffer
  17. bufEnc := gob.NewEncoder(&buf)
  18. // 编码器对数据编码
  19. if err := bufEnc.Encode(data); err != nil {
  20. return nil, err
  21. }
  22. return buf.Bytes(), nil
  23. }
  24. // 解码
  25. func decode(b []byte) (RPCData, error) {
  26. buf := bytes.NewBuffer(b)
  27. // 得到字节数组解码器
  28. bufDec := gob.NewDecoder(buf)
  29. // 解码器对数据节码
  30. var data RPCData
  31. if err := bufDec.Decode(&data); err != nil {
  32. return data, err
  33. }
  34. return data, nil
  35. }

实现RPC服务端

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

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

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

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

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

维护函数map

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

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

点击查看代码
  1. package rpc
  2. import (
  3. "fmt"
  4. "net"
  5. "reflect"
  6. )
  7. // 声明服务端
  8. type Server struct {
  9. // 地址
  10. addr string
  11. // map 用于维护关系的
  12. funcs map[string]reflect.Value
  13. }
  14. // 构造方法
  15. func NewServer(addr string) *Server {
  16. return &Server{addr: addr, funcs: make(map[string]reflect.Value)}
  17. }
  18. // 服务端需要一个注册Register
  19. // 第一个参数函数名,第二个传入真正的函数
  20. func (s *Server) Register(rpcName string, f interface{}) {
  21. // 维护一个map
  22. // 若map已经有键了
  23. if _, ok := s.funcs[rpcName]; ok {
  24. return
  25. }
  26. // 若map中没值,则将映射加入map,用于调用
  27. fVal := reflect.ValueOf(f)
  28. s.funcs[rpcName] = fVal
  29. }
  30. // 服务端等待调用的方法
  31. func (s *Server) Run() {
  32. // 监听
  33. lis, err := net.Listen("tcp", s.addr)
  34. if err != nil {
  35. fmt.Printf("监听 %s err :%v", s.addr, err)
  36. return
  37. }
  38. for {
  39. // 服务端循环等待调用
  40. conn, err := lis.Accept()
  41. if err != nil {
  42. return
  43. }
  44. serSession := NewSession(conn)
  45. // 使用RPC方式读取数据
  46. b, err := serSession.Read()
  47. if err != nil {
  48. return
  49. }
  50. // 数据解码
  51. rpcData, err := decode(b)
  52. if err != nil {
  53. return
  54. }
  55. // 根据读到的name,得到要调用的函数
  56. f, ok := s.funcs[rpcData.Name]
  57. if !ok {
  58. fmt.Println("函数 %s 不存在", rpcData.Name)
  59. return
  60. }
  61. // 遍历解析客户端传来的参数,放切片里
  62. inArgs := make([]reflect.Value, 0, len(rpcData.Args))
  63. for _, arg := range rpcData.Args {
  64. inArgs = append(inArgs, reflect.ValueOf(arg))
  65. }
  66. // 反射调用方法
  67. // 返回Value类型,用于给客户端传递返回结果,out是所有的返回结果
  68. out := f.Call(inArgs)
  69. // 遍历out ,用于返回给客户端,存到一个切片里
  70. outArgs := make([]interface{}, 0, len(out))
  71. for _, o := range out {
  72. outArgs = append(outArgs, o.Interface())
  73. }
  74. // 数据编码,返回给客户端
  75. respRPCData := RPCData{rpcData.Name, outArgs}
  76. bytes, err := encode(respRPCData)
  77. if err != nil {
  78. return
  79. }
  80. // 将服务端编码后的数据,写出到客户端
  81. err = serSession.Write(bytes)
  82. if err != nil {
  83. return
  84. }
  85. }
  86. }

实现RPC客户端

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

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

点击查看代码
  1. package rpc
  2. import (
  3. "net"
  4. "reflect"
  5. )
  6. // 声明服务端
  7. type Client struct {
  8. conn net.Conn
  9. }
  10. // 构造方法
  11. func NewClient(conn net.Conn) *Client {
  12. return &Client{conn: conn}
  13. }
  14. // 实现通用的RPC客户端
  15. // 传入访问的函数名
  16. // fPtr指向的是函数原型
  17. //var select fun xx(User)
  18. //cli.callRPC("selectUser",&select)
  19. func (c *Client) CallRPC(rpcName string, fPtr interface{}) {
  20. // 通过反射,获取fPtr未初始化的函数原型
  21. fn := reflect.ValueOf(fPtr).Elem()
  22. // 需要另一个函数,作用是对第一个函数参数操作
  23. f := func(args []reflect.Value) []reflect.Value {
  24. // 处理参数
  25. inArgs := make([]interface{}, 0, len(args))
  26. for _, arg := range args {
  27. inArgs = append(inArgs, arg.Interface())
  28. }
  29. // 连接
  30. cliSession := NewSession(c.conn)
  31. // 编码数据
  32. reqRPC := RPCData{Name: rpcName, Args: inArgs}
  33. b, err := encode(reqRPC)
  34. if err != nil {
  35. panic(err)
  36. }
  37. // 写数据
  38. err = cliSession.Write(b)
  39. if err != nil {
  40. panic(err)
  41. }
  42. // 服务端发过来返回值,此时应该读取和解析
  43. respBytes, err := cliSession.Read()
  44. if err != nil {
  45. panic(err)
  46. }
  47. // 解码
  48. respRPC, err := decode(respBytes)
  49. if err != nil {
  50. panic(err)
  51. }
  52. // 处理服务端返回的数据
  53. outArgs := make([]reflect.Value, 0, len(respRPC.Args))
  54. for i, arg := range respRPC.Args {
  55. // 必须进行nil转换
  56. if arg == nil {
  57. // reflect.Zero()会返回类型的零值的value
  58. // .out()会返回函数输出的参数类型
  59. outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
  60. continue
  61. }
  62. outArgs = append(outArgs, reflect.ValueOf(arg))
  63. }
  64. return outArgs
  65. }
  66. // 完成原型到函数调用的内部转换
  67. // 参数1是reflect.Type
  68. // 参数2 f是函数类型,是对于参数1 fn函数的操作
  69. // fn是定义,f是具体操作
  70. v := reflect.MakeFunc(fn.Type(), f)
  71. // 为函数fPtr赋值,过程
  72. fn.Set(v)
  73. }

实现RPC通信

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

  1. package main
  2. import (
  3. "encoding/gob"
  4. "fmt"
  5. "ginSourceCode/rpc"
  6. "net"
  7. )
  8. // 定义用户对象
  9. type User struct {
  10. Name string
  11. Age int
  12. }
  13. // 用于测试用户查询的方法
  14. func queryUser(uid int) (User, error) {
  15. user := make(map[int]User)
  16. // 假数据
  17. user[0] = User{"zs", 20}
  18. user[1] = User{"ls", 21}
  19. user[2] = User{"ww", 22}
  20. // 模拟查询用户
  21. if u, ok := user[uid]; ok {
  22. return u, nil
  23. }
  24. return User{}, fmt.Errorf("%d err", uid)
  25. }
  26. func main() {
  27. // 编码中有一个字段是interface{}时,要注册一下
  28. gob.Register(User{})
  29. addr := "127.0.0.1:8000"
  30. // 创建服务端
  31. srv := rpc.NewServer(addr)
  32. // 将服务端方法,注册一下
  33. srv.Register("queryUser", queryUser)
  34. // 服务端等待调用
  35. go srv.Run()
  36. // 客户端获取连接
  37. conn, err := net.Dial("tcp", addr)
  38. if err != nil {
  39. fmt.Println("err")
  40. }
  41. // 创建客户端对象
  42. cli := rpc.NewClient(conn)
  43. // 需要声明函数原型
  44. var query func(int) (User, error)
  45. cli.CallRPC("queryUser", &query)
  46. // 得到查询结果
  47. u, err := query(0)
  48. if err != nil {
  49. fmt.Println("err")
  50. }
  51. fmt.Println(u)
  52. }

参考链接

golang中的RPC开发-2的更多相关文章

  1. golang中的rpc开发

    golang中实现RPC非常简单,官方提供了封装好的库,还有一些第三方的库 golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp和http数据传输方式,由于其他语言不支 ...

  2. golang中的rpc包用法

    RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样. 我所在公司的项目是采用基于Restful的微服务架构,随着微服 ...

  3. golang - gob与rpc

    今天和大家聊聊golang中怎么使用rpc,rpc数据传输会涉及到gob编码,所以先讲讲gob,别担心,就算你完全没有接触过gob与rpc,只要知道rpc的中文是远程过程调用,剩下的我都能给你讲明白( ...

  4. springcloud中使用dubbo开发rpc服务及调用

    spring cloud中基于springboot开发的微服务,是基于http的rest接口,也可以开发基于dubbo的rpc接口. 一,创建goodsService模块 1, 在创建的goodsSe ...

  5. golang中的net/rpc包

    本文先介绍RPC,然后go原生对RPC的使用,之后是介绍go语言中有哪些RPC框架以及一些其他常见的框架,最后是探究go语言中rpc的源码. (1)首先介绍下什么RPC? (2)RPC可以做什么? ( ...

  6. google的grpc在golang中的使用

    GRPC是google开源的一个高性能.跨语言的RPC框架,基于HTTP2协议,基于protobuf 3.x,基于Netty 4.x. 前面写过一篇golang标准库的rpc包的用法,这篇文章接着讲一 ...

  7. Visual Studio Code中配置GO开发环境

    在Visual Studio Code中配置GO开发环境 一.GO语言安装 详情查看:GO语言下载.安装.配置 二.GoLang插件介绍 对于Visual Studio Code开发工具,有一款优秀的 ...

  8. Golang中使用lua进行扩展

    前言 最近在项目中需要使用lua进行扩展,发现github上有一个用golang编写的lua虚拟机,名字叫做gopher-lua.使用后发现还不错,借此分享给大家. 数据类型 lua中的数据类型与go ...

  9. golang中Context的使用场景

    golang中Context的使用场景 context在Go1.7之后就进入标准库中了.它主要的用处如果用一句话来说,是在于控制goroutine的生命周期.当一个计算任务被goroutine承接了之 ...

随机推荐

  1. ABP VNext框架中Winform终端的开发和客户端授权信息的处理

    在ABP VNext框架中,即使在它提供的所有案例中,都没有涉及到Winform程序的案例介绍,不过微服务解决方案中提供了一个控制台的程序供了解其IDS4的调用和处理,由于我开发过很多Winform项 ...

  2. SQL Server日志恢复还原数据

    通过日志还原,首先要注意的是: 1,在数据库更新和删除之前有一个完整的备份. 2,在更新和删除之后,做一个日志备份. 3,该日志只能用于还原数据库备份和日志备份时间之间的数据. 下面看整个数据库备份和 ...

  3. 【蓝桥杯】第十二届蓝桥杯砝码称重(Python题解)

    @ 目录 题目 [80分] 思路 知识点 代码 题目 [80分] 你有一架天平和N个砝码,这N个砝码重量依次是W1,W2,--,WN请你计算一共可以称出多少种不同的重量? 注意砝码可以放在天平两边. ...

  4. Python pyecharts绘制折线图

    一.pyecharts绘制折线图line.add()方法简介 line.add()方法简介 add(name,x_axis,y_axis,is_symbol_show=True, is_smooth= ...

  5. 【LeetCode】322. Coin Change 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 动态规划 日期 题目地址:https://leetc ...

  6. 【LeetCode】36. Valid Sudoku 解题报告(Python)

    [LeetCode]36. Valid Sudoku 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址 ...

  7. [算法笔记-题解]问题 C: 例题4-3 比较交换3个实数值,并按序输出

    问题 C: 例题4-3 比较交换3个实数值,并按序输出 [命题人 : 外部导入] 时间限制 : 1.000 sec 内存限制 : 12 MB 题目描述 从键盘输入3个实数a, b, c,通过比较交换, ...

  8. Capstone通用 USB Type-C音视频拓展坞转换芯片

    专业解决视频接口技术Capstone科技在2021年新推出四款低功耗单芯片USB Type-C音视频格式转换器方案──CS5266.CS5267.CS5268与CS5269.将为各种显示屏.外部显示设 ...

  9. Capstone CS5213|HDMI转VGA|CS5213设计参考电路

    Capstone CS5213是一款HDMI到VGA转换器结合了HDMI输入接口和模拟RGB DAC输出且带支持片上音频数模转换器.CS5213芯片设计简单,整体芯片尺寸精悍,外围电路集成优化度较高, ...

  10. shell2-if判断

    1.条件测试类型(判断类型): 将测试结果做为判断依据. 测试类型有以下三种   [ 命令 ] :命令测试法(最常用的)  [[ 命令 ]] : 关键字测试 test 命令 以上是三种都可以,注意单词 ...