本文代码地址:https://gitee.com/lymgoforIT/gee-rpc/tree/master/day1-codec
本文是7天用Go从零实现RPC框架GeeRPC
的第一篇。
- 使用
encoding/gob
实现消息的编解码(序列化与反序列化) - 实现一个简易的服务端,仅接受消息,不处理,代码约
200
行
消息的序列化与反序列化
一个典型的 RPC
调用如下:
err = client.Call("Arith.Multiply", args, &reply)
客户端发送的请求包括服务名 Arith
,方法名 Multiply
,参数 args
三个,服务端的响应包括错误 error
,返回值 reply
2
个。我们将请求的参数和响应中的返回值抽象为 body
,剩余的信息放在 header
中,那么就可以抽象出数据结构 Header
:
day1-codec/codec/codec.go
package codec
import "io"
type Header struct {
ServiceMethod string // 请求的服务和方法 Service.Method
Seq uint64 // 客户端请求的序列号ID,用于区分不同的请求
Error string // 错误信息
}
ServiceMethod
是服务名和方法名,通常与Go
语言中的结构体和方法相映射。Seq
是请求的序号,也可以认为是某个请求的ID
,用来区分不同的请求。Error
是错误信息,客户端置为空,服务端如果如果发生错误,将错误信息置于Error
中。
我们将和消息编解码以及从请求读取数据和将响应结果写回客户端相关的代码都放到 codec
子目录中,在此之前,还需要在根目录下使用 go mod init geerpc
初始化项目,方便后续子 package
之间的引用。
进一步,抽象出对消息体进行编解码的接口 Codec
,抽象出接口是为了实现不同的 Codec
实例:
day1-codec/codec/codec.go
type Codec interface {
io.Closer
ReadHeader(h *Header) error
ReadBody(body interface{}) error
Write(h *Header, body interface{}) error
}
- 继承
io.Closer
,从而拥有Close
方法,用于关闭客户端与服务端的连接 ReadHeader
从连接流中读取Header
信息,放入参数h
中ReadBody
从连接流中读取body
信息,放入参数body
中Write
将响应的h
和body
写入客户端与服务端的连接conn
中,即将服务端的响应信息发送回客户端
紧接着,抽象出 Codec
的构造函数,客户端和服务端可以通过 Codec
的 Type
得到构造函数,从而创建 Codec
实例。这部分代码和工厂模式类似,与工厂模式不同的是,返回的是构造函数,而非实例。
day1-codec/codec/codec.go
type NewCodecFunc func(io.ReadWriteCloser) Codec
type Type string
const (
GobType Type = "application/gob"
JsonType Type = "application/json" // not implemented
)
var newCodecFuncMap = make(map[Type]NewCodecFunc)
// RegisterNewCodecFunc 注册对应Type获取Codec的函数
func RegisterNewCodecFunc(t Type ,f NewCodecFunc) error {
if _,ok := newCodecFuncMap[t];ok {
return fmt.Errorf("register newCodecFunc repeat,type:%v,func:%T",t,f)
}
newCodecFuncMap[t] = f
return nil
}
// GetNewCodecFunc 根据Type获取Codec的函数
func GetNewCodecFunc(t Type) NewCodecFunc {
return newCodecFuncMap[t]
}
我们定义了 2
种 Codec
,Gob
和 Json
,但是实际代码中只用到了 Gob
一种,且本文也只介绍了Gob
一种,事实上,2
者的实现非常接近,甚至只需要把 gob
换成 json
即可。
首先定义 GobCodec
结构体,这个结构体由四部分构成,conn
是由构建函数传入,通常是通过 TCP
或者 Unix
建立 socket
时得到的链接实例,dec
和 enc
对应 gob
的 Decoder
和 Encoder
,buf
是为了防止阻塞而创建的带缓冲的 Writer
,一般这么做能提升性能。
day1-codec/codec/gob.go
package codec
import (
"bufio"
"encoding/gob"
"io"
"log"
)
var _ Codec = (*GobCodec)(nil)
func init() {
// 注册对应Type获取Codec的函数
err := RegisterNewCodecFunc(GobType,NewGobCodec)
if err != nil {
panic(err)
}
}
type GobCodec struct {
conn io.ReadWriteCloser // 用于保存客户端与服务端的连接
buf *bufio.Writer
dec *gob.Decoder
enc *gob.Encoder
}
func NewGobCodec(conn io.ReadWriteCloser) Codec {
buf := bufio.NewWriter(conn) // 装饰器模式,给conn增加缓冲能力
return &GobCodec{
conn: conn,
buf: buf,
dec: gob.NewDecoder(conn),// 从conn流中解码数据
enc: gob.NewEncoder(buf),// 将数据编码到buf中,因为buf只是装饰了conn,所以实际是到了conn中,即响应客户端
}
}
在
Go
语言中,json.NewDecoder
和json.Unmarshal
都用于将JSON
数据解析为Go
中的数据结构,但它们有一些区别:
json.NewDecoder
是通过创建一个Decoder
对象,从一个io.Reader
(如os.Stdin
、文件、网络连接等)中读取JSON
数据并进行解码。json.Unmarshal
则是直接将JSON
数据(以字节切片[]byte
或者字符串的形式)解析并映射到指定的数据结构。使用场景上,如果数据是从一个输入流中读取,通常使用
json.NewDecoder
;如果已经有了JSON
数据的字节切片或字符串,使用json.Unmarshal
会更方便。json.NewEncoder
和json.Marshal
同理。
接着实现 ReadHeader
、ReadBody
、Write
和 Close
方法。
func (g *GobCodec) Close() error {
return g.conn.Close()
}
func (g *GobCodec) ReadHeader(h *Header) error {
return g.dec.Decode(h)
}
func (g *GobCodec) ReadBody(body interface{}) error {
return g.dec.Decode(body)
}
func (g *GobCodec) Write(h *Header, body interface{}) (err error) {
defer func() {
_ = g.buf.Flush()
if err != nil {
_ = g.Close()
}
}()
if err = g.enc.Encode(h); err != nil {
log.Println("rpc codec:gob error encoding header:", err)
return err
}
if err = g.enc.Encode(body); err != nil {
log.Println("rpc codec: gob error encoding body:", err)
return err
}
return nil
}
通信过程
客户端与服务端的通信需要协商一些内容,例如 HTTP
报文,分为header
和 body
2
部分,body
的格式和长度通过 header
中的 Content-Type
和 Content-Length
指定,服务端通过解析 header
就能够知道如何从 body
中读取需要的信息。对于 RPC
协议来说,这部分协商是需要自主设计的。为了提升性能,一般在报文的最开始会规划固定的字节,来协商相关的信息。比如第1
个字节用来表示序列化方式,第2
个字节表示压缩方式,第3-6
字节表示 header
的长度,7-10
字节表示 body
的长度。
对于 GeeRPC
来说,目前需要协商的唯一一项内容是消息的编解码方式。我们将这部分信息,放到结构体 Option
中承载。目前,已经进入到服务端的实现阶段了。
day1-codec/server.go
package geerpc
import "geerpc/day1-codec/codec"
// MagicNumber 协议一般都会用一个魔数开头,标志当前的请求符合本协议定义的格式
const MagicNumber = 0x3bef5c
type Option struct {
MagicNumber int
CodecType codec.Type // 客户端请求的编码格式
}
var DefaultOption = &Option{
MagicNumber: MagicNumber,
CodecType: codec.GobType,
}
一般来说,涉及协议协商的这部分信息,需要设计固定的字节来传输的。但是为了实现上更简单,GeeRPC
客户端固定采用 JSON
编码 Option
,后续的 header
和 body
的编码方式由 Option
中的 CodeType
指定,服务端首先使用 JSON
解码 Option
,然后通过 Option
的 CodeType
解码剩余的内容。即报文将以这样的形式发送:
| Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} |
| <------ 固定 JSON 编码 ------> | <------- 编码方式由 CodeType 决定 ------->|
在一次连接中,Option
固定在报文的最开始,Header
和 Body
可以有多个(因为tcp
协议,连接后,只要没有断开连接,客户端可以通过同一个连接多次发起请求),即报文可能是这样的。
| Option | Header1 | Body1 | Header2 | Body2 | ...
服务端的实现
通信过程已经定义清楚了,那么服务端的实现就比较直接了。
day1-codec/server.go
// Server represents an RPC Server.
type Server struct{}
// NewServer returns a new Server.
func NewServer() *Server {
return &Server{}
}
// DefaultServer is the default instance of *Server.
var DefaultServer = NewServer()
// Accept accepts connections on the listener and serves requests
// for each incoming connection.
func (server *Server) Accept(lis net.Listener) {
for {
conn, err := lis.Accept()
if err != nil {
log.Println("rpc server: accept error:", err)
continue // 单个请求建立连接失败,可跳过,继续服务后续请求
}
go server.ServeConn(conn)
}
}
// Accept accepts connections on the listener and serves requests
// for each incoming connection.
func Accept(lis net.Listener) { DefaultServer.Accept(lis) }
- 首先定义了结构体
Server
,没有任何的成员字段。 - 实现了
Accept
方式,net.Listener
作为参数,for
循环等待socket
连接建立,并开启子协程处理,处理过程交给了ServerConn
方法。 DefaultServer
是一个默认的Server
实例,主要为了用户使用方便。
如果想启动服务,过程是非常简单的,传入 listener
即可,tcp
协议和 unix
协议都支持。
lis, _ := net.Listen("tcp", ":9999")
geerpc.Accept(lis)
ServeConn
的实现就和之前讨论的通信过程紧密相关了,首先使用 json.NewDecoder
反序列化得到 Option
实例,检查 MagicNumber
和 CodeType
的值是否正确。然后根据 CodeType
得到对应的消息编解码器,接下来的处理交给 serverCodec
。
// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
// 当前连接服务结束后,关闭连接,并且由于该ServeConn方法是子协程开的,所以必须要做好捕获panic的处理
defer func() {
_ = conn.Close()
if err := recover();err != nil {
log.Println("recover panic... ")
}
}()
// 从连接流中解码出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))
}
// invalidRequest is a placeholder for response argv when error occurs
var invalidRequest = struct{}{}
func (server *Server) serveCodec(cc codec.Codec) {
sending := new(sync.Mutex) // make sure to send a complete response
wg := new(sync.WaitGroup) // wait until all request are handled
for {
req, err := server.readRequest(cc)
if err != nil {
if req == nil {
break // it's not possible to recover, so close the connection
}
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()
}
serveCodec
的过程非常简单。主要包含三个阶段
- 读取请求
readRequest
- 处理请求
handleRequest
- 回复请求
sendResponse
之前提到过,在一次连接中,允许接收多个请求,即多个 request header
和 request body
,因此这里使用了for
无限制地等待请求的到来,直到发生错误(例如连接被关闭,接收到的报文有问题等),这里需要注意的点有三个:
handleRequest
使用了协程并发执行请求。- 处理请求是并发的,但是回复请求的报文必须是逐个发送的,并发容易导致多个回复报文交织在一起,客户端无法解析。在这里使用锁(
sending
)保证。 - 尽力而为,只有在
header
解析失败时,才终止循环。
// request stores all information of a call
type request struct {
h *codec.Header // header of request
argv, replyv reflect.Value // argv and replyv of request
}
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(""))
// req是指针类型,所以ReadBody中会将解码的内容赋值到req.argv上的
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()
// Write中将h和body写入了conn,实际就是将响应内容返回给了客户端
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 func() {
wg.Done()
if err := recover();err != nil {
log.Println("recover panic... ")
}
}()
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)
}
目前还不能判断 body
的类型,因此在readRequest
和 handleRequest
中,day1
将 body
作为字符串处理。接收到请求,打印 header
,并回复 geerpc resp ${req.h.Seq}
。这一部分后续再实现。
main 函数(一个简易的客户端)
day1
的内容就到此为止了,在这里我们已经实现了一个消息的编解码器 GobCodec
,并且客户端与服务端实现了简单的协议交换(protocol exchange
),即允许客户端使用不同的编码方式。同时实现了服务端的雏形,建立连接,读取、处理并回复客户端的请求。
接下来,我们就在 main
函数中看看如何使用刚实现的 GeeRPC
吧。
day1-codec/main/main.go
package main
import (
"encoding/json"
"fmt"
"geerpc"
"geerpc/codec"
"log"
"net"
"time"
)
func startServer(addr chan string) {
// pick a free port
l, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatal("network error:", err)
}
log.Println("start rpc server on", l.Addr())
addr <- l.Addr().String()
geerpc.Accept(l)
}
func main() {
addr := make(chan string)
go startServer(addr)
// in fact, following code is like a simple geerpc client
// 如下代码就是一个简单的客户端
conn, _ := net.Dial("tcp", <-addr)
defer func() { _ = conn.Close() }()
time.Sleep(time.Second)
// 发送JSON包 option
_ = json.NewEncoder(conn).Encode(geerpc.DefaultOption)
// 发完JSON包后等一秒,避免JSON包吞掉部分后面的gob包的header内容(粘包和拆包)
time.Sleep(1 * time.Second)
// 使用客户端与服务端的连接conn构造一个Codec,用于收发信息
cc := codec.NewGobCodec(conn)
// send request & receive response
for i := 0; i < 5; i++ {
h := &codec.Header{
ServiceMethod: "Foo.Sum",
Seq: uint64(i),
}
// 客户端往连接中写入数据就是向服务端发送请求
_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
// 读取服务端的响应头
_ = cc.ReadHeader(h)
// 读取服务端的响应体
var reply string
_ = cc.ReadBody(&reply)
log.Println("reply:", reply)
}
}
- 在
startServer
中使用了信道addr
,确保服务端端口监听成功,客户端再发起请求。 - 客户端首先发送
Option
进行协议交换,接下来发送消息头h := &codec.Header{}
,和消息体geerpc req ${h.Seq}
。 - 最后解析服务端的响应
reply
,并打印出来。
执行结果如下:
start rpc server on [::]:63662
&{Foo.Sum 0 } geerpc req 0
reply: geerpc resp 0
&{Foo.Sum 1 } geerpc req 1
reply: geerpc resp 1
&{Foo.Sum 2 } geerpc req 2
reply: geerpc resp 2
&{Foo.Sum 3 } geerpc req 3
reply: geerpc resp 3
&{Foo.Sum 4 } geerpc req 4
reply: geerpc resp 4
QA :
- 我在写demo时,向一个网络连接中先发送一个json的数据,再发送一个gob的数据。
然后使用json解码器进行解码,json解码器这时会把两个数据都从网络连接中读出来,存在json解码器解码后,造成gob解码器无法获取到对应的数据,作者的rpc框架也是用的json解码器和gob解码器,并且也是先发送json数据,再发送gob数据,在测试中没有发现这个问题,请问是如何避免的?我的情况是运行会阻塞在readRequestHeader,这可能是什么原因呢?
答:作者是先发送option(json包),发送完后sleep 1 秒,再发送header+body的。_ = json.NewEncoder(conn).Encode(geerpc.DefaultOption) 这句话执行完,sleep 1 秒后conn就已经把json格式的option数据发到服务端了,服务端处理之后,客户端才发送header和body,然后服务端继续处理。
正常测试是刚刚说的这个流程,但是发完JSON包后如果不sleep 1秒的话,就会出现上面的情况。
这块知识属于 tcp粘包 问题,另外还有 拆包 问题,你可以去了解下。
更好的解决办法:
可以给json包添加长度header,比如约定连接第一次发起请求时的前两个字节存储JSON包的长度,解决json Decoder吞掉部分gob包的问题
client代码
func NewClient(conn net.Conn, opt *Option) (*Client, error) {
f := codec.NewCodecFuncMap[opt.CodecType]
if f == nil {
err := fmt.Errorf("invalid codec type %s", opt.CodecType)
log.Println("rpc client: codec error:", err)
return nil, err
}
jsonBytes, err := json.Marshal(opt)
if err != nil {
log.Println("rpc client: options err:", err)
_ = conn.Close()
return nil, err
}
lenJsonBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lenJsonBytes, uint16(len(jsonBytes)))
conn.Write(lenJsonBytes)
conn.Write(jsonBytes)
return newClientCodec(f(conn), opt), nil
}
server代码
func (server *Server) ServeConn(conn net.Conn) {
defer func() { _ = conn.Close() }()
var opt Option
// time.Sleep(time.Second)
lenJsonBytes := make([]byte, 2)
conn.Read(lenJsonBytes)
jsonLength := binary.BigEndian.Uint16(lenJsonBytes)
log.Println("rpc server: json length:", jsonLength)
jsonBytes := make([]byte, jsonLength)
_, err := conn.Read(jsonBytes)
if err != nil {
log.Println("rpc server: read json bytes error:", err)
return
}
if err := json.Unmarshal(jsonBytes, &opt); err != nil {
log.Println("rpc server: options error:", err)
return
}
if opt.MagicNumber != MagicNumber {
log.Printf("rpc server: invalid magic number: %x\n", opt.MagicNumber)
return
}
f := codec.NewCodecFuncMap[opt.CodecType]
if f == nil {
log.Println("rpc server: invalid codec type: ", opt.CodecType)
return
}
log.Println("rpc server: communicate start with codec type: ", opt.CodecType)
server.serveCodec(f(conn), &opt)
}
原文链接:https://geektutu.com/post/geerpc-day1.html