【GeeRPC】【Day1】【Golang】服务端与消息编码

  1. 使用encoding/gob 实现消息的编解码(序列化与反序列化)
  2. 实现一个简易的服务端,仅接受消息,不处理
  3. 待学习
    • 工厂模式
    • TCP协议

一、 消息的序列化与反序列化

1. 典型的RPC调用

  • 客户端发送的请求:服务名、方法名、方法参数
  • 服务端响应的请求:错误error、返回值reply
  • 将请求和响应中的参数和返回值抽象为body,剩余的信息放在header中
    type Header struct{
    	ServiceMethod   string  // 服务名和方法名,通常与go语言中的结构体和方法相映射
    	Seq 			uint64  // 请求的序号,也可以认为是某个请求的ID,用来区分不同的请求
    	Error 			string  // 错误信息,客户端置为空,服务端如果发送错误,将错误信息置于Error中
    }
    

2. 目录规划

  • 消息的编解码相关的代码,都放到codec子目录
  • 抽象对消息体进行编解码的接口,目的是为了实现不同的Codec实例
    type Codec interface{
    	io.Closer
    	ReadHeader(*Header) error
    	ReadBody(interface{}) error
    	Write(*Header, interface{}) error
    }
    
  • 抽象出Codec的构造函数
    type GobCodec struct{
    	conn io.ReadWriteCloser  // 由构建函数传入,通常是通过TCP或者Unix建立socket时得到的链接实例
    	buf *bufio.Writer  // 防止阻塞而创建的带缓冲的Writer,一般这么做能提升性能
    	dec *gob.Decoder  // Decoder解码
    	enc *gob.Encoder  // Encoder编码
    }
    
    var _ Codec = (*GobCodec)(nil)   // 确保GobCodec实现了Codec接口
    
    func NewGobCodec(conn io.ReadWriteCloser) Codec{
    	buf := bufio.NewWriter(conn)
    	return &GobCodec{
    		conn: conn,
    		buf: buf,
    		dec: gob.NewDecoder(conn),
    		enc: gob.NewEncoder(buf),
    	}
    }
    

二、 通信过程

1. 简要说明

  • 客户端与服务端通信需要协商一些内容
  • HTTP报文,分为header和body两部分
    • body的格式和长度通过header中的Content-Type和Content-Length指定,服务端通过解析header就能够知道如何从body中读取需要的信息
  • 对于RPC来说这部分协商是需要自主设计的
    • 为了提升性能,一般在报文的最开始会规划固定的字节,来协商相关的信息
    • 比如第1个字节用来表示序列化方式,第2个字节表示压缩方式,第3-6字节表示header长度,第7-10字节表示body的长度

2. GeeRPC实践

  • 目前需要协商的唯一一项内容,消息的编解码方式
    type Type string
    const(
    	GobType Type = "application/gob"
    	MagicNumber = 0x3bef5c
    )
    
    type Option struct{
    	MagicNumber int
    	CodecType codec.GobType
    }
    var DefaultOption = &Option{
    	MagicNumber: MagicNumber,
    	CodecType: codec.GobType,
    }
    
  • 服务端的实现
    type Server struct{}
    
    func NewServer() *Server{
    	return &Server
    }
    
    var DefaultServer = NewServer()  // 默认的Server实例,目的是方便用户使用
    
    func (server *Server) Accept(lis net.Listener){
    	// 等待socket连接建立
    	for {
    		conn, err := lis.Accept()
    		if err != nil{
    			log.Println("rpc server: accept error: ", err)
    			return
    		}
    		// 开启子协程处理
    		go server.ServeConn(conn)	
    	}
    }
    
    func (server *Server) ServeConn(conn io.ReadWriteCloser){
    	defer func(){_ = conn.Close()}()
    	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))
    }
    
    var invalidRequest = struct{}{}
    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
    		}
    		// 处理请求,协程并发执行请求
    		// 回复请求的报文必须是逐个发送的,容易导致多个回复报文交织在一起,客户端无法解析,所以使用锁(sending)保证
    		wg.Add(1)
    		go server.handleRequest(cc, req, sending, wg)
    	}	
    }
    
    func Accept(lis net.Listener){DefaultServer.Accept(lis)}
    
    // 使用
    lis, _ := net.Listen("tcp", ":9999")
    Accept(lis)
    

三、 举例1-TCP粘包

1. 简要说明

  • 粘包原因
    • tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发
    • 粘包可发生在发送端也可发生在接收端
    • 发送端粘包:当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
    • 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
  • 解决粘包
    • 出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作
    • 封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)
    • 包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包
    • 我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度

2. 粘包实例

  • 服务端代码
    func process(conn net.Conn){
    	defer conn.Close()
    	reader := bufio.NewReader(conn)
    	var buf [1024]byte
    	for {
    		n, err := reader.Read(buf[:])
    		if err == io.EOF{
    			break	
    		}	
    		if err != nil{
    			fmt.Println("read from client failed, err: ", err)
    			break	
    		}
    		recvStr := string(buf[:n])
    		fmt.Println("收到client发来的数据:", recvStr)
    	}	
    }
    
    func main(){
    	listen, err := net.Listen("tcp", "127.0.0.1:30000")
    	if err != nil{
    		fmt.Println("listen failed, err: ", err)
    		return	
    	}	
    	defer listen.Close()
    	for{
    		conn, err := listen.Accept()
    		if err != nil{
    			fmt.Println("accept failed, err: ", err)
    			continue	
    		}
    		go process(conn)
    	}
    }
    
  • 客户端代码
    func main(){
    	conn, err := net.Dial("tcp", "127.0.0.1:30000")
    	if err != nil{
    		fmt.Println("dial failed, err: ", err)
    		return	
    	}	
    	defer conn.Close()
    	for i := 0; i < 20; i++{
    		msg := `Hello, Hello, how ard you?`
    		conn.Write([]byte(msg))	
    	}
    }
    

3. 解决粘包

  • proto.go

    // Encode将消息编码
    func Encode(message string)([]byte, error){
    	// 读取消息的长度,转换成int32类型(占4个字节)
    	var length = int32(len(message))
    	var pkg = new(bytes.Buffer)
    	
    	// 写入消息头
    	err := binary.Write(pkg, binart.LittleEndian, length)
    	if err != nil{
    		return nil, err	
    	}	
    	
    	// 写入消息实体
    	err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    	if err != nil{
    		return nil, err	
    	}
    	return pkg.Bytes(), nil
    }
    
    // Decode 解码消息
    func Decode(reader *bufio.Reader)(string, error){
    	// 读取消息的长度
    	lengthByte, _ := reader.Peek(4)  // 读取前4个字节的数据
    	lengthBuff := bytes.NewBuffer(lengthByte)
    	var length int32
    	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    	if err != nil{
    		return "", err
    	}
    	// Buffered返回缓冲中现有的可读取的字节数
    	if int32(reader.Buffered()) < length + 4{
    		return "", err	
    	}
    	
    	// 读取真正的消息数据
    	pack := make([]byte, int(4 + length))
    	_, err = reader.Read(pack)
    	if err != nil{
    		return "", err	
    	}
    	return string(pack[4:]), nil
    }
    
    
  • 服务端 server.go

    func process(conn net.Conn){
    		defer conn.Close()
    		reader := bufio.NewReader(conn)
    		for {
    			msg, err := proto.Decode(reader)
    			if err == io.EOF{
    				return
    			}
    			if err != nil{
    				fmt.Println("decode msg failed, err: ", err)
    				return	
    			}
    			fmt.Println("收到client发来的数据: ", msg)
    		}	
    	}
    	
    	func main(){
    		listen, err := net.Listen("tcp", "127.0.0.1:30000")
    		if err != nil{
    			fmt.Println("listen failed, err: ", err)
    			return	
    		}	
    		defer listen.Close()
    		for {
    			conn, err := listen.Accept()
    			if err != nil{
    				fmt.Println("accept failed, err: ", err)
    				continue
    			}
    			go process(conn)	
    		}
    	}
    
  • 客户端 client.go

    func main(){
    	conn, err := net.Dial("tcp", "127.0.0.1:30000")
    	if err != nil{
    		fmt.Println("dial failed, err: ", err)
    		return	
    	}
    	defer conn.Close()
    	for i := 0; i < 20; i++{
    		msg := `Hello, Hello, How are you?`
    		data, err := proto.Encode(msg)	
    		if err != nil{
    			fmt.Println("encode msg failed, err: ", err)
    			return	
    		}
    		conn.Write(data)
    	}
    }
    

三、 举例2 - TCP消息通信

1. 简要说明

  • 自定义消息格式
    • 自定义消息格式就是服务端和客户端约定好消息的格式,比如消息头多少字节、消息长度多少字节、消息内容两边按照约定的规则去读取解析数据
    • 包头[字节] + 包内容长度[字节] + 包体[字节]
  • 使用了自定义的消息格式,很容易判断出客户端发过来的内容是否合法,扩展性也很强
  • 相对来说,只需要这个格式实现服务就行了,也节省带宽消耗
  • 创建两个文件,分别写服务端和客户端的代码
    【GeeRPC】【Day1】【Golang】服务端与消息编码_第1张图片【GeeRPC】【Day1】【Golang】服务端与消息编码_第2张图片

2. 服务端 server.go

  • 协议结构体

    type protocol struct{
    	Length uint32   // 内容长度
    	Content []byte	// 内容
    }
    
  • 装包解包

    const HEADER_LEN = 4
    
    func Packet(content string) []byte{
    	buffer := make([]byte, HEADER_LEN + len(content))
    	// 将buffer前面四个字节设置为包长度,大端序
    	binary.BigEndian.PutUint32(buffer[0:4], uint32(len(content)))
    	copy(buffer[4:], content)
    	return buffer	
    }
    
    // 解包,先读取4个字节转换成整型,再读包长度字节
    func UnPacket(c net.Conn) (*Protocol, error){
    	var (
    		p = &Protocol{}
    		header = make([]byte, HEADER_LEN)
    	)	
    	_, err := io.ReadFull(c, header)
    	if err != nil{
    		return p, err	
    	}
    	p.Length = binary.BigEndian.Uint32(header)  // 转换成10进制的数字
    	contentByte := make([]byte, p.Length)
    	_, e := io.ReadFull(c, contentByte)  // 读取内容
    	if e != nil{
    		return p, e
    	}
    	p.Content = contentByte
    	return p, nil
    }
    
  • 解析包体内容

    func (p *Protocol) parseContent() (map[string]interface{}, error){
    	var object map[string]interface{}
    	unmarshal := json.Unmarshal(p.Content, &object)
    	if unmarshal != nil{
    		return object, unmarshal
    	}
    	return object, nil	
    }
    
  • 主函数

    func main(){
    	l, err := net.Listen("tcp", ":9527")
    	if err != nil{
    		log.Fatal(err)	
    	}
    	defer l.Close()
    	for {
    		conn, err := l.Accept()
    		if err != nil{
    			log.Fatal(err)	
    		}
    		go func(c net.Conn){
    			protocol, _ := UnPacket(c)
    			parseContent, err := protocol.parseContent()
    			if (err != nil){	
    			}
    			s := parseContent["serviceId"].(string)
    			cstr := parseContent["data"].(string)
    			if s == "Hello World"{
    				fmt.Printf("serviceId: %s, content: %s", s, cstr)
    				writeByte := []byte(cstr)
    				c.Write(writeByte)	
    			}	
    			c.Close
    		}(conn)
    	}
    }
    

3. 客户端 client.go

  • 生成包体
    
    const HEADER_LEN = 4
    
    type Content struct{
    	ServiceId string `json:"serviceId"`
    	Data interface{} `json: "data"`	
    }
    
    func Packet(serviceId string, content string)[]byte{
    	bytes, _ := json.Marshal(Content{ServiceId: serviceId, Data: content})
    	buffer := make([]byte, HEADER_LEN + len(bytes))
    	// 将buffer前面四个字节设置为包长度,大端序
    	binary.BigEndian.PutUint32(buffer[0:4], uint32(len(bytes)))
    	copy(buffer[4:], bytes)
    	return buffer	
    }
    
  • 主函数
    func main(){
    	conn, e := net.Dial("tcp", ":9527")
    	if e != nil{
    		log.Fatal(e)	
    	}
    	reader := bufio.NewReader(os.Stdin)
    	fmt.Print("Text to send:")
    	text, _ := reader.ReadString('\n')
    	
    	buffer := Packet("Hello World", text)
    	conn.Write(buffer)
    	
    	message, _ := bufio.NewReader(conn).ReadString('\n')
    	fmt.Print("Message from server:" + message)
    	
    	defer conn.Close()
    }
    

参考

  1. https://geektutu.com/post/geerpc-day1.html
  2. https://loocode.com/post/go-zi-ding-yi-tcp-xiao-xi-tong-xin
  3. https://www.topgoer.com/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/socket%E7%BC%96%E7%A8%8B/TCP%E9%BB%8F%E5%8C%85.html

你可能感兴趣的:(GeeRPC,golang,开发语言,后端)