go rpc

236 阅读5分钟

rpc-远程过程调用 : 是一种计算机通信协议,允许调用不同进程空间的程序。

http的Restful与rpc协议相比的缺点:

  • Restful 接口需要额外的定义,无论是客户端还是服务端,都需要额外的代码来处理,而 RPC 调用则更接近于直接调用。
  • 基于 HTTP 协议的 Restful 报文冗余,承载了过多的无效信息,而 RPC 通常使用自定义的协议格式,减少冗余报文。
  • RPC 可以采用更高效的序列化协议,将文本转为二进制传输,获得更高的性能。
  • 因为 RPC 的灵活性,所以更容易扩展和集成诸如注册中心、负载均衡等功能

一个rpc框架需要解决的问题:

  1. 传输协议
  2. 报文编码 3.可用性问题:连接超时、是否支持异步和并发 4.注册中心、负载均衡
消息的序列化与反序列化

典型的rpc调用:

err = client.Call("Arith.Multiply", args, &reply)

其中Arith.Multiply和args为请求的参数,reply和err为服务器的响应。可以把请求的参数和响应的回复抽象为body,剩余的抽象为请求头。这样就抽象出来了一个消息体。请求头的数据接口可以这样定义。

//请求头
type Header struct {
   ServiceMethod string //服务名和方法名
   Seq           uint64 //请求序列号或请求id
   Error         string //错误信息
}

这时还需要实现对消息体进行编解码的方法。此时考虑定义一个接口,因为可以采取不同的方式对消息体进行编解码。

//对消息编码的接口,以便实现一个不同的编码方式
type Codec interface {
   io.Closer //用于包装基本的关闭方法
   ReadHeader(*Header) error
   ReadBody(interface{}) error
   Write(*Header, interface{}) error
}
type NewCodecFunc func(io.ReadWriteCloser) Codec

//定义消息序列化类型
type Type string

const (
   GobType  Type = "application/gob"
   JsonType Type = "application/json" // not implemented
)

var NewCodecFuncMap map[Type]NewCodecFunc

//初始化服务器中消息序列化类型
func init() {
   NewCodecFuncMap = make(map[Type]NewCodecFunc)
   NewCodecFuncMap[GobType] = NewGobCodec
}

上述定义了两种消息编解码方式,这里实现gob方式。

type GobCodec struct {
   conn io.ReadWriteCloser //链接实例  通常是通过TCP或者Unix建立的socket连接实例
   buf  *bufio.Writer      //缓冲writer
   dec  *gob.Decoder       //管理从远端读取的类型和数据信息的解释
   enc  *gob.Encoder       //管理将数据和类型信息编码后发送到远端的操作
}

var _ Codec = (*GobCodec)(nil)

func NewGobCodec(conn io.ReadWriteCloser) Codec {
   buf := bufio.NewWriter(conn)
   return &GobCodec{
      conn: conn,
      buf:  buf,
      dec:  gob.NewDecoder(conn),
      enc:  gob.NewEncoder(buf),
   }
}

//读取请求header
func (c *GobCodec) ReadHeader(h *Header) error {
   return c.dec.Decode(h)
}

//读取请求body
func (c *GobCodec) ReadBody(body interface{}) error {
   return c.dec.Decode(body)
}

//向返回中写入header和body
func (c *GobCodec) Write(h *Header, body interface{}) (err error) {
   defer func() {
      _ = c.buf.Flush()
      if err != nil {
         _ = c.Close()
      }
   }()
   if err = c.enc.Encode(h); err != nil {
      log.Println("rpc: gob error encoding header:", err)
      return
   }
   if err = c.enc.Encode(body); err != nil {
      log.Println("rpc: gob error encoding body:", err)
      return
   }
   return
}

//关闭连接
func (c *GobCodec) Close() error {
   return c.conn.Close()
}

上述定义了消息体和消息的编解码,还需要实现消息通信

通信过程: 一般来说客户端与服务器之间实现通信需要协商内容,例如http报文分位header和body两部分,服务器通过读取header中的content-type和content-length就知道怎么从body中解析数据。但是对于我们目前实现的rpc框架来说需要协商的唯一内容就是消息编解码的方式。我们将这部分信息放在option中进行承载。

const MagicNumber = 0x3bef5c

type Option struct {
   MagicNumber int        // 标识为一个rpc请求
   CodecType   codec.Type // 客户端选择不同的方式编码传递的信息
}

var DefaultOption = &Option{
   MagicNumber: MagicNumber,
   CodecType:   codec.GobType,
}

一般设计协商的部分信息应该要用固定字节来传输,但是为了简单,目前的传递的消息格式为:

| Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} |
| <------      固定 JSON 编码      ------>  | <-------   编码方式由 CodeType 决定   ------->|

通信过程已经清楚,那么就可以实现一个服务端 server

type Server struct{}

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

//默认server实例
var DefaultServer = NewServer()
// 服务器接监听接口 等待连接进入 并开启新协程处理连接
func (server *Server) Accept(lis net.Listener) {
   for {
      conn, err := lis.Accept()
      if err != nil {
         log.Println("rpc server: accept error:", err)
         return
      }
      go server.ServeConn(conn)
   }
}

// 用defaultserver处理连接
func Accept(lis net.Listener) { DefaultServer.Accept(lis) }

处理连接: 验证是否为rpc请求,并得到对应编解码实例,进行进一步处理。

// 处理连接
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
   defer func() { _ = conn.Close() }()
   //获得option实例
   var opt Option
   if err := json.NewDecoder(conn).Decode(&opt); err != nil {
      log.Println("rpc server: options error: ", err)
      return
   }
   if opt.MagicNumber != MagicNumber {
      log.Printf("rpc server: invalid magic number %x", opt.MagicNumber)
      return
   }
   //得到对应消息解码器
   f := codec.NewCodecFuncMap[opt.CodecType]
   if f == nil {
      log.Printf("rpc server: invalid codec type %s", opt.CodecType)
      return
   }

   server.serveCodec(f(conn))
}

进一步处理:

// 三步: 1、读取请求 2、处理请求 3、回复请求
func (server *Server) serveCodec(cc codec.Codec) {
   sending := new(sync.Mutex) // 加锁确保回复逐个发送
   wg := new(sync.WaitGroup)  // 确保处理所有请求
   for {
      req, err := server.readRequest(cc)
      if err != nil {
         if req == nil {
            break // 解析请求失败时,结束循环
         }
         req.h.Error = err.Error()
         server.sendResponse(cc, req.h, invalidRequest, sending)
         continue
      }
      wg.Add(1)
      //并发处理请求
      go server.handleRequest(cc, req, sending, wg)
   }
   wg.Wait()
   _ = cc.Close()
}

解析对应请求:readRequest方法。

func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) {
   var h codec.Header
   if err := cc.ReadHeader(&h); err != nil {
      if err != io.EOF && err != io.ErrUnexpectedEOF {
         log.Println("rpc server: read header error:", err)
      }
      return nil, err
   }
   return &h, nil
}

func (server *Server) readRequest(cc codec.Codec) (*request, error) {
   h, err := server.readRequestHeader(cc)
   if err != nil {
      return nil, err
   }
   req := &request{h: h}
   // TODO: now we don't know the type of request argv
   // day 1, just suppose it's string
   req.argv = reflect.New(reflect.TypeOf(""))
   if err = cc.ReadBody(req.argv.Interface()); err != nil {
      log.Println("rpc server: read argv err:", err)
   }
   return req, nil
}

解析请求失败时回复:

func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) {
   sending.Lock()
   defer sending.Unlock()
   if err := cc.Write(h, body); err != nil {
      log.Println("rpc server: write response error:", err)
   }
}

处理请求:

func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) {
   // TODO, should call registered rpc methods to get the right replyv
   // day 1, just print argv and send a hello message
   defer wg.Done()
   log.Println(req.h, req.argv.Elem())
   req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq))
   server.sendResponse(cc, req.h, req.replyv.Interface(), sending)
}

上述就实现了一个服务器的雏形。对上述内容进行简单应用:

func startServer(addr chan string) {
   l, err := net.Listen("tcp", ":6060")
   if err != nil {
      log.Fatal("network err:", err)
   }
   log.Println("start rpc server on", l.Addr())
   addr <- l.Addr().String()
   rpcdemo.Accept(l)
}

func main() {
   addr := make(chan string)
   // 启动服务器、监听地址:6060
   go startServer(addr)

   conn, _ := net.Dial("tcp", <-addr) //网络上连接地址address 并返回一个conn接口 建立连接
   defer func() { _ = conn.Close() }()

   time.Sleep(time.Second)

   //发送option进行协议交换
   _ = json.NewEncoder(conn).Encode(rpcdemo.DefaultOption)
   cc := codec.NewGobCodec(conn)

   //发送五次请求
   for i := 0; i < 5; i++ {
      h := &codec.Header{
         ServiceMethod: "Foo.Sum",
         Seq:           uint64(i),
      }
      _ = cc.Write(h, fmt.Sprintf("rpc req %d", h.Seq))
      _ = cc.ReadHeader(h)
      var reply string
      _ = cc.ReadBody(&reply)
      log.Println("reply:", reply)
   }
}

总结:上述内容实现了一个rpc框架的消息解编码器-消息的序列化与反序列化,客户端与服务器端简单的协议交换,和服务器的一个简单雏形。