从零到一: 用Go语言搭建简易RPC框架并实践 (一)

🐉大家好,我是gopher_looklook,现任某独角兽企业Go语言工程师,喜欢钻研Go源码,发掘各项技术在大型Go微服务项目中的最佳实践,期待与各位小伙伴多多交流,共同进步!

前言

最近在读Go语言的net/rpc源码,感觉源码涉及到了太多细节和错误处理,层层嵌套的函数调用关系错综复杂,难以窥见RPC框架的本质。

其实对于一个RPC (R emote P rocedure Call 远程过程调用) 框架,我认为最核心的功能有2点:

  1. 客户端与服务端通信。
  2. 服务端注册服务,并处理客户端请求。

举个例子,这段代码定义了一个简单的SortServer 服务,并提供了一个简单的排序功能Sort 。当我们希望其他程序可以跨服务调用我们写的Sort方法时,需要实现的最核心的功能就是实现不同应用程序间的通信,并且将服务端的SortServer服务注册到一个地方,以随时响应客户端调用服务方法获取执行结果的请求。

这个系列将分成两篇文章。

第一篇实现服务端与客户端通信。服务端与客户端协商,约定使用固定的JSON数据格式进行通信。客户端将消息发送给服务端,服务端简单将得到的消息简单组装,返回给客户端。

第二篇在服务端实现服务注册功能 ,并修改解析客户端请求 的函数逻辑,将客户端发送过来的消息转化为具体的服务调用请求 ,在注册的服务列表中找到对应的服务,利用反射实现服务功能调用,将执行结果返回给客户端。

话不多说,下面让我们开始动手搭建RPC框架吧!

服务端客户端通信

为了方便演示效果,新创建一个Go项目gopher-rpc。

消息格式约定

  • 服务端与客户端消息格式
go 复制代码
type Request struct {
    Method string      `json:"method"`
    Args   interface{} `json:"args"`
}

Method 表示要调用的服务名方法,比如"SortServer.Sort "表示想要调用的是SortServer服务的Sort方法 ;"ArithServer.Add "则表示想要调用的是ArithServer服务的Add方法 。由于不知道具体请求参数的个数和类型,因此Args暂时用空切片interface{}类型表示即可。

  • 消息结束分隔符

约定好以\n作为消息结束的标志。

项目结构

其中server.goclient.go用于编写RPC框架的代码,在main文件夹下建一个basic包,用于演示基本的服务端与客户端的通信功能。

框架源码1.0

  • server.go
go 复制代码
package gopher_rpc

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io"
    "strings"
)

type Server struct{}

func NewServer() *Server {
    return &Server{}
}

var DefaultServer = NewServer()

type Request struct {
    Method string      `json:"method"`
    Args   interface{} `json:"args"`
}

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
       message, err := reader.ReadString('\n')
       if err != nil {
          if err == io.EOF {
             break
          }
          fmt.Printf("读取数据时出错: %v\n", err)
          return
       }

       var request Request
       err = json.Unmarshal([]byte(message), &request)
       if err != nil {
          fmt.Printf("反序列化 JSON 数据时出错: %v\n", err)
          return
       }
       serviceMethodName := request.Method
       dot := strings.LastIndexByte(serviceMethodName, '.')
       if dot < 0 {
          fmt.Printf("无效的方法名: %s\n", serviceMethodName)
       }
       serviceName := serviceMethodName[:dot]
       methodName := serviceMethodName[dot+1:]

       // 约定好以\n作为消息结尾
       response := fmt.Sprintf("serviceName:%s, methodName:%s, request.Arg:%#v \n ", serviceName, methodName, request.Args)
       _, err = conn.Write([]byte(response))
       if err != nil {
          fmt.Printf("发送响应时出错: %v\n", err)
          return
       }
    }
}

func ServerConn(conn io.ReadWriteCloser) {
    DefaultServer.ServeConn(conn)
}

在server.go中,首先定义了一个Server结构体 ,用来承载服务端提供的功能。接着定义了一个默认Server---------DefaultServer 。Server指针实现了一个处理连接请求conn的函数ServerConn 。在ServerConn函数中,利用bufio包提供的相关功能,从conn中读取到客户端发送过来的请求消息message ,接着使用JSON反序列化成约定好的消息体格式。解析出客户端消息中请求的MethodArgs 。之后把消息中的服务名、方法名、参数重新组装成一条消息响应response 。利用conn.Write函数向网络连接中写入请求,等待客户端接收。同时提供了一个全局的ServerConn函数 ,调用DefaultServer.ServeConn方法,供服务端使用。

  • client.go
go 复制代码
package gopher_rpc

import (
    "bufio"
    "fmt"
    "net"
)

type Client struct {
    conn     net.Conn
    reader   *bufio.Reader
    done     chan struct{}
    errChan  chan error
    Response string
}

// Dail 用于建立与服务器的连接
func Dial(network, address string) (*Client, error) {
    conn, err := net.Dial(network, address)
    if err != nil {
       return nil, fmt.Errorf("无法连接到服务器 %s: %v", address, err)
    }
    client := &Client{
       conn:    conn,
       reader:  bufio.NewReader(conn),
       done:    make(chan struct{}), // 无缓冲channel
       errChan: make(chan error, 1),
    }
    // 开一个协程接受服务端返回
    go client.receive()
    return client, nil
}

func (c *Client) receive() {
    defer close(c.done)
    defer close(c.errChan)
    response, err := c.reader.ReadString('\n')
    if err != nil {
       c.errChan <- fmt.Errorf("接收服务器响应时出错:%v", err.Error())
       return
    }
    c.Response = response
    c.done <- struct{}{}
}

// Call 方法用于向服务端发送消息并等待响应(创建Client时开了个协程去接受服务端响应)
func (c *Client) Call(serviceMethod string) error {
    doneChan := c.Go(serviceMethod)
    select {
    case <-doneChan:
       return nil
    case err := <-c.errChan:
       return err
    }
}

func (c *Client) Go(serviceMethod string) chan struct{} {
    _, err := c.conn.Write([]byte(serviceMethod + "\n"))
    if err != nil {
       c.errChan <- fmt.Errorf("发送消息时出错: %v", err)
       close(c.done)
       close(c.errChan)
       return nil
    }
    return c.done
}

// Close 方法用于关闭客户端连接
func (c *Client) Close() error {
    if c.conn != nil {
       return c.conn.Close()
    }
    return nil
}

在client.go中,首先定义了一个Client结构体 ,封装了一些跟网络请求与处理相关的字段。接着提供了一个对外暴露的Dial方法。用于连接到服务端。在Dial 方法返回Client之前,还开了一个Go协程 ,用于监听和接收服务端发送过来的消息;若成功读取到消息,会往无缓冲channel 字段done 中发送一个空结构体,这样做的目的是为了当receive协程 接收到服务端消息时,可以利用无缓冲通道done 通知给主程序。Client结构体指针还提供了一个Call方法 ,Call方法调用了Go方法 ,用于往conn中写入消息发送给服务端,并返回无缓冲通道done。Client.Call方法调用完Go方法后,使用select 监听两个通道client.doneclient.errChan 。若发生错误,则返回相应的error。如果没有错误,则监听client.done 通道。若之前的receive协程成功读取到了服务端响应。<-doneChan 将不再阻塞,Call函数正常退出,服务端返回的响应数据存储在Client结构体的Response字段当中。

实战服务端客户端通信

  • 服务端监听请求

server_main.go

go 复制代码
package main

import (
    "fmt"
    "gopher_rpc"
    "net"
)

func main() {
    listener, err := net.Listen("tcp", ":8888")
    if err != nil {
       fmt.Printf("监听端口时出错:%v\n", err)
       return
    }
    defer listener.Close()
    fmt.Println("Server is listening on port 8888....")
    for {
       conn, err := listener.Accept()
       if err != nil {
          fmt.Printf("接收连接时出错:%v\n", err)
          continue
       }
       // 开一个go协程,异步处理连接请求
       go gopher_rpc.ServerConn(conn)
    }
}
  • 客户端发送数据并打印响应结果

client_main.go

go 复制代码
package main

import (
    "encoding/json"
    "fmt"
    "gopher_rpc"
)

type ServiceMethod struct {
    Method string `json:"method"`
    Args   Args   `json:"args"`
}

type Args struct {
    Num1 int64 `json:"num_1"`
    Num2 int64 `json:"num_2"`
}

func main() {
    // 连接到服务端
    client, err := gopher_rpc.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
       fmt.Println(err)
       return
    }
    defer client.Close()

    param := &ServiceMethod{
       Method: "ArithServer.Add", // 可以表示想调用ArithServer服务的Add方法
       Args: Args{
          Num1: 10,
          Num2: 20,
       }, // 可以表示两个参数,一个是10,一个是20
    }
    bs, _ := json.Marshal(param)
    if err = client.Call(string(bs)); err != nil {
       fmt.Println(err)
       return
    }
    fmt.Printf("客户端接收的数据: %s", client.Response)
}

程序运行

  • 服务端
  • 客户端

从运行结果可以看到,服务端正常解析出了客户端请求的服务名、方法名和参数,并重新组装消息,简单地返回给了客户端。说明我们实现了服务端与客户端的通信。

未完待续

在下一篇文章中,我们将修改server.go实现服务注册处理客户端请求的功能。

下一篇传送门: 从零到一: 用Go语言搭建简易RPC框架并实践 (二)

相关推荐
zZeal8 分钟前
Django ORM解决Oracle表多主键的问题
后端·python·oracle·django
七灵微2 小时前
【后端】Flask
后端·python·flask
SomeB1oody2 小时前
【Rust自学】17.2. 使用trait对象来存储不同值的类型
开发语言·后端·rust
Asthenia04123 小时前
深入解析 Canal 组件:EventParser,EventProcessorFactory和Glue
后端
慕璃嫣4 小时前
Haskell语言的安全开发
开发语言·后端·golang
qq_544329175 小时前
CRM项目的开发与调试整体策略
前端·后端·bug
黄同学real9 小时前
使用.NET 8构建高效的时间日期帮助类
后端·c#·.net
ChinaRainbowSea10 小时前
四.4 Redis 五大数据类型/结构的详细说明/详细使用( zset 有序集合数据类型详解和使用)
java·javascript·数据库·redis·后端·nosql
eybk13 小时前
Qpython+Flask监控添加发送语音中文信息功能
后端·python·flask