Golang实现RPC-《GO开发知识笔记》

admin 2025-11-04 00:59:55 编程 来源:ZONE.CI 全球网 0 阅读模式
  • RPC
    • 一、简介
    • 二、Golang中实现RPC
      • 示例
    • 三、RPC调用流程
    • 四、网络传输数据格式
    • 六、实现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程序,实现求矩形面积和周长。
    • 服务端 ```java 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) } }

    1. - 客户端
    2. ```java
    3. package main
    4. import (
    5. "fmt"
    6. "log"
    7. "net/rpc"
    8. )
    9. type Params struct{
    10. Width,Height int
    11. }
    12. // 调用服务
    13. func main() {
    14. // 连接远程rpc服务
    15. client, err := rpc.DialHTTP("tcp","127.0.0.1:8080")
    16. if err != nil {
    17. fmt.Println(err)
    18. }
    19. // 调用远程的方法
    20. // 定义接受服务端传回来的计算结果变量
    21. ret := 0
    22. // 求面积
    23. if err = client.Call("Rect.Area",Params{50,100},&ret); err != nil {
    24. log.Fatal(err)
    25. }
    26. fmt.Println("面积:", ret)
    27. // 求周长
    28. if err = client.Call("Rect.Perimeter",Params{50,100},&ret);err !=nil{
    29. log.Fatal(err)
    30. }
    31. fmt.Println("周长:",ret)
    32. }

    2 、服务端接收两个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下: 9 * 2 =18 9 / 2, 商 = 4, 余数 = 1

    • 服务端 ```java 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) } }

    1. - 客户端
    2. ```java
    3. package main
    4. import (
    5. "fmt"
    6. "log"
    7. "net/rpc/jsonrpc"
    8. )
    9. // 声明请求参数的结构体
    10. type ArithRequest struct{
    11. A, B int
    12. }
    13. // 声明响应的结构体
    14. type ArithResponse struct{
    15. Pro int
    16. Quo int
    17. Rem int
    18. }
    19. func main() {
    20. // 连接远程的rpc
    21. conn, err := jsonrpc.Dial("tcp","127.0.0.1:8080")
    22. if err != nil{
    23. log.Fatal(err)
    24. }
    25. req := ArithRequest{
    26. A: 9,
    27. B: 2,
    28. }
    29. resp := ArithResponse{}
    30. // 商和余数
    31. if err = conn.Call("Arith.Divide",req,&resp); err != nil {
    32. log.Fatal(err)
    33. }
    34. // 乘法
    35. if err = conn.Call("Arith.Multiply", req, &resp); err != nil {
    36. log.Fatal(err)
    37. }
    38. fmt.Printf("%d 和 %d, 积=%d, 商=%d,余数=%d\n",req.A,req.B,resp.Pro,resp.Quo,resp.Rem)
    39. }

    三、RPC调用流程

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

    四、网络传输数据格式

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

    • 编写连接会话 ```java 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 }

    1. - 测试会话读写
    2. ```java
    3. package rpc
    4. import (
    5. "fmt"
    6. "net"
    7. "sync"
    8. "testing"
    9. )
    10. // 测试读写
    11. func TestSession_ReadWrite(t *testing.T) {
    12. // 定义监听的ip和端口
    13. addr := "127.0.0.1:8000"
    14. // 定义传输的数据
    15. my_data := "hello"
    16. // 等待组
    17. wg := sync.WaitGroup{}
    18. // 协程 一个读,一个写
    19. wg.Add(2)
    20. // 写数据
    21. go func(){
    22. defer wg.Done()
    23. // 创建tcp连接
    24. lis, err := net.Listen("tcp",addr)
    25. if err != nil{
    26. t.Fatal(err)
    27. }
    28. conn, _ := lis.Accept()
    29. s := Session{conn: conn}
    30. // 写数据
    31. if err := s.Write([]byte(my_data)); err != nil {
    32. t.Fatal(err)
    33. }
    34. }()
    35. // 读数据
    36. go func() {
    37. defer wg.Done()
    38. conn, err := net.Dial("tcp",addr)
    39. if err != nil {
    40. t.Fatal(err)
    41. }
    42. s := Session{conn: conn}
    43. // 读数据
    44. data, err := s.Read()
    45. if err != nil{
    46. t.Fatal(err)
    47. }
    48. if string(data) != my_data{
    49. t.Fatal(err)
    50. }
    51. fmt.Println(string(data))
    52. }()
    53. wg.Wait()
    54. }
    • 编写编解码 ```java 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 }

    1. <a name="CurKm"></a>
    2. ### 五、实现RPC服务端
    3. <a name="Vob3h"></a>
    4. #### 1、服务端接收到的数据需要包括什么?
    5. - 调用的函数名,参数列表
    6. - 一般会约定函数的第二个返回值是error类型
    7. - 通过反射实现
    8. <a name="fzlEb"></a>
    9. #### 2、服务端需要解决的问题是什么?
    10. - Client调用时只传过来函数名,需要维护函数名到函数之间的map映射
    11. <a name="jpuch"></a>
    12. #### 3、服务端的核心功能有哪些?
    13. - 维护函数名导函数反射值的map
    14. - client端传函数名,参数列表后,服务端要解析为反反射值,调用执行
    15. - 函数的返回值打包,并通过网络返回个客户端
    16. ```java
    17. package rpc
    18. import (
    19. "fmt"
    20. "net"
    21. "reflect"
    22. )
    23. // 声明服务端
    24. type Server struct {
    25. // 地址
    26. addr string
    27. // 服务端维护的函数名到函数反射值的map
    28. funcs map[string]reflect.Value
    29. }
    30. // 创建服务端对象
    31. func NewServer(addr string)*Server{
    32. return &Server{
    33. addr: addr,
    34. funcs: make(map[string]reflect.Value),
    35. }
    36. }
    37. // 服务端绑定注册方法
    38. // 将函数名与函数真正实现对应起来
    39. // 第一个参数为函数名,第二个传入真正的函数
    40. func (s *Server) Register (rpcName string, f interface{}){
    41. if _, ok := s.funcs[rpcName]; ok {
    42. return
    43. }
    44. // map 中没有值,则将映射添加到map,便于调用
    45. fVal := reflect.ValueOf(f)
    46. s.funcs[rpcName] = fVal
    47. }
    48. // 服务端等待调用
    49. func (s *Server) Run(){
    50. // 监听
    51. lis, err := net.Listen("tcp",s.addr)
    52. if err != nil {
    53. fmt.Printf("监听 %s err:%v\n",s.addr,err)
    54. return
    55. }
    56. for {
    57. // 拿到连接
    58. conn ,err := lis.Accept()
    59. if err != nil {
    60. fmt.Printf("accept err :%v\n",err)
    61. return
    62. }
    63. // 创建会话
    64. srvSession := NewSession(conn)
    65. // RPC读取数据
    66. b,err := srvSession.Read()
    67. if err != nil {
    68. fmt.Printf("read err:%v\n",err)
    69. return
    70. }
    71. // 对数据进行解码
    72. rpcData, err := decode(b)
    73. if err != nil {
    74. fmt.Printf("decode err:%v\n", err)
    75. return
    76. }
    77. // 根据读取到数据的Name,得到调用的函数名
    78. f, ok := s.funcs[rpcData.Name]
    79. if !ok{
    80. fmt.Printf("函数 %s 不存在", rpcData.Name)
    81. return
    82. }
    83. // 解析遍历客户端出来的参数,放到一个数组中
    84. inArgs := make([]reflect.Value,0,len(rpcData.Args))
    85. for _, arg := range rpcData.Args{
    86. inArgs = append(inArgs, reflect.ValueOf(arg))
    87. }
    88. // 反射调用方法, 传入参数
    89. out := f.Call(inArgs)
    90. // 解析遍历执行结果,放到一个数组中
    91. outArgs := make([]interface{},0,len(out))
    92. for _, o := range out{
    93. outArgs = append(outArgs, o.Interface())
    94. }
    95. // 包装数据,返回给客户端
    96. respRPCData := RPCData{rpcData.Name,outArgs}
    97. // 编码
    98. respBytes, err := encode(respRPCData)
    99. if err != nil {
    100. fmt.Printf("encode err:%v\n",err)
    101. return
    102. }
    103. // 使用rpc写出数据
    104. err = srvSession.Write(respBytes)
    105. if err != nil {
    106. fmt.Printf("session write err :%v\n",err)
    107. return
    108. }
    109. }
    110. }

    六、实现RPC客户端

    • 客户端只有函数原型,使用reflect.MakeFunc()可以完成原型到函数的调用
    • reflect.MakeFunc()是Client从函数原型到网络调度的关键 ```java 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) }

    1. // 处理服务端返回的数据
    2. outArgs := make([]reflect.Value,0,len(respRPC.Args))
    3. for i, arg := range respRPC.Args{
    4. // 必须对nil进行处理
    5. if arg == nil {
    6. // 必须填充一个真正的类型,不能是nil
    7. outArgs = append(outArgs,reflect.Zero(fn.Type().Out(i)))
    8. continue
    9. }
    10. outArgs = append(outArgs,reflect.ValueOf(arg))
    11. }
    12. return outArgs
    13. }
    14. // 参数1: 一个未初始化函数的方法值,类型时reflect.Type
    15. // 参数2: 另一个函数,作用是对第一个函数参数操作
    16. // 返回reflect.Value 类型
    17. // MakeFunc 使用传入的函数原型,创建一个绑定 参数2 的新函数
    18. v := reflect.MakeFunc(fn.Type(), f)
    19. // 为函数fPtr赋值
    20. fn.Set(v)

    }

    1. <a name="OjV8Y"></a>
    2. ### 七、实现RPC通信测试
    3. - 给服务端注册一个查询用户的方法,客户端去RPC调用。
    4. ```java
    5. package rpc
    6. import (
    7. "encoding/gob"
    8. "fmt"
    9. "net"
    10. "testing"
    11. )
    12. // 用户查询
    13. // 用于测试的结构体
    14. type User struct{
    15. Name string
    16. Age int
    17. }
    18. // 用于测试的查询用户的方法
    19. func queryUser(uid int)(User, error){
    20. user := make(map[int]User)
    21. user[0] = User{"zs",20}
    22. user[1] = User{"ls",20}
    23. user[2] = User{"ws",20}
    24. // 模拟查询用户
    25. if u, ok := user[uid]; ok {
    26. return u, nil
    27. }
    28. return User{}, fmt.Errorf("ud %d not in user db", uid)
    29. }
    30. // 测试方法
    31. func TestRPC(t *testing.T){
    32. // 需要对interface{}可能产生的类型进行注册
    33. gob.Register(User{})
    34. addr := "127.0.0.1:8000"
    35. // 创建服务端
    36. srv := NewServer(addr)
    37. // 将方法注册到服务端
    38. srv.Register("queryUser", queryUser)
    39. // 服务端等待调用
    40. go srv.Run()
    41. // 客户端获取连接
    42. conn, err := net.Dial("tcp", addr)
    43. if err != nil {
    44. t.Error(err)
    45. }
    46. // 创建客户端
    47. cli := NewClient(conn)
    48. // 声明函数原型
    49. var query func(int)(User,error)
    50. cli.callRPC("queryUser", &query)
    51. // 得到查询结果
    52. u, err := query(1)
    53. if err != nil {
    54. t.Fatal(err)
    55. }
    56. fmt.Println(u)
    57. }
    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  15