【Zinx】Day3:Zinx 的消息封装

Day3:Zinx 的消息封装

现在我们来对 Zinx 做一个简单的升级。现阶段,当服务器接收数据时,我们是将全部数据都放在一个 Request 当中的,当前的 Request 结构如下:

type Request struct {
	conn ziface.IConnection // 已经和客户端建立好的连接
	data []byte             // 客户端请求的数据
}

显然,现在我们是用 []byte 来接收全部数据,这个结构过于简单,如果我们能从 Request 结构当中得知消息的类型、长度,那就更好了,所以现在我们对 Request 当中的 data 进行修改,将它抽象为一种消息类型,并将全部消息放在具体的消息类型当中。

创建消息封装类型

首先,我们在 ziface 下创建 imessage.go 类型,封装消息接口:

package ziface

type IMessage interface {
	GetDataLen() uint32 // 获取消息数据段的长度
	GetMsgId() uint32   // 获取消息 ID
	GetData() []byte    // 获取消息内容

	SetMsgId(uint32)   // 设置消息 ID
	SetData([]byte)    // 设置消息内容
	SetDataLen(uint32) // 设置消息数据段的长度
}

同时在 znet 下新建 message.go 并创建 message 结构,让它实现 imessage 接口:

package znet

import "zinx/ziface"

type Message struct {
	Id      uint32 // 消息的 ID
	DataLen uint32 // 消息的长度
	Data    []byte // 消息的内容
}

var _ ziface.IMessage = (*Message)(nil)

// NewMsgPackage 创建一个 Message 消息
func NewMsgPackage(id uint32, data []byte) *Message {
	return &Message{
		Id:      id,
		DataLen: uint32(len(data)),
		Data:    data,
	}
}

// GetDataLen 获取消息数据段的长度
func (msg *Message) GetDataLen() uint32 {
	return msg.DataLen
}

// GetMsgId 获取消息 Id
func (msg *Message) GetMsgId() uint32 {
	return msg.Id
}

// GetData 获取消息内容
func (msg *Message) GetData() []byte {
	return msg.Data
}

// SetDataLen 设置数据段长度
func (msg *Message) SetDataLen(len uint32) {
	msg.DataLen = len
}

// SetMsgId 设置消息 Id
func (msg *Message) SetMsgId(msgId uint32) {
	msg.Id = msgId
}

// SetData 设置消息内容
func (msg *Message) SetData(data []byte) {
	msg.Data = data
}

一个基础的 Message 包含消息 ID、数据、数据长度三个成员,提供基本的 setter 和 getter 方法,目的是为了后续的封装做优化。同时也提供了一个创建 message 包的初始化方法 NewMegPackage

消息的封包与拆包

Zinx 采用经典的 TLV(Type-Len-Value)封包格式来解决 TCP 粘包问题。
请添加图片描述
由于 Zinx 采用 TCP 流的形式传播数据,难免会出现消息 1 和 消息 2 一同发送的情况【粘包:接收端一次读取到多个数据包,原因可能是接收方缓冲区积压了多个包后统一读取,或是发送方开启了数据合并优化】,那么 Zinx 就需要有能力区分两个消息的边界,所以 Zinx 采用统一的封包和拆包方法。

在发包之前,将数据打包成上图所示的格式,即同时包括 head 和 body 两部分,收到数据的时候分两次进行读取,先读取固定长度的 head 部分,得到后续 data 的长度,然后再根据 dataLen 读取之后的 body,这样就可以解决粘包的问题了,原因是读取头部之后,就可以得到后续数据的长度,这样就知道了这个包的数据的边界,不会读到下一个包的数据。同时,应确保 head 结构的长度固定。

创建拆包封包的抽象类

首先我们在 ziface 下创建 idatapack.go,在其中创建用于数据封包和拆包的接口:

package ziface

type IDataPack interface {
	GetHeadLen() uint32                // 获取包头长度方法
	Pack(msg IMessage) ([]byte, error) // 封包方法
	Unpack([]byte) (IMessage, error)   // 拆包方法
}

实现拆包与封包类

我们进一步在 znet 下创建 datapack.go,在其中定义 DataPack 来实例化接口:

package znet

import (
	"bytes"
	"encoding/binary"
	"errors"
	"zinx/settings"
	"zinx/ziface"
)

// DataPack 为用于封包和拆包的类, 暂时不需要成员
type DataPack struct{}

var _ ziface.IDataPack = (*DataPack)(nil)

// NewDataPack 封包拆包实例的初始化方法
func NewDataPack() *DataPack {
	return &DataPack{}
}

// GetHeadLen 获取包头长度
func (dp *DataPack) GetHeadLen() uint32 {
	// Id uint32(4 bytes) + DataLen uint32(4 bytes)
	return 8
}

// Pack 为封包方法
func (dp *DataPack) Pack(msg ziface.IMessage) ([]byte, error) {
	// 创建一个存放 byte 字节的缓冲
	dataBuff := bytes.NewBuffer([]byte{})

	// 写 dataLen
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetDataLen()); err != nil {
		return nil, err
	}

	// 写 msgID
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMsgId()); err != nil {
		return nil, err
	}

	// 写 data 数据
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetData()); err != nil {
		return nil, err
	}

	return dataBuff.Bytes(), nil
}

func (dp *DataPack) Unpack(binaryData []byte) (ziface.IMessage, error) {
	// 创建一个用于输入二进制数据的 ioReader
	dataBuff := bytes.NewReader(binaryData)

	// 只解压 head 信息, 得到 dataLen 和 msgID
	msg := &Message{}

	// 读 dataLen
	if err := binary.Read(dataBuff, binary.LittleEndian, &msg.DataLen); err != nil {
		return nil, err
	}

	// 读 msgID
	if err := binary.Read(dataBuff, binary.LittleEndian, &msg.Id); err != nil {
		return nil, err
	}

	// 判断 dataLen 的长度是否超过了我们允许的最大包长度
	if settings.Conf.MaxPacketSize > 0 && msg.DataLen > settings.Conf.MaxPacketSize {
		return nil, errors.New("Too large msg data received")
	}

	// 此处只需要把 head 的数据拆包出来即可, 再通过 head 的长度, 从 conn 读取一次数据
	return msg, nil
}

Zinx-v0.5 代码实现

现在我们需要把封包和拆包的功能集成到 Zinx 当中,并测试 Zinx 当中的该功能是否有效。

修改 Request 字段

为 IRequest 接口新增 GetMsgID 方法:

// IRequest 为接口, Request 打包客户端请求的连接信息以及请求数据
type IRequest interface {
	GetConnection() IConnection // 获取请求连接信息
	GetData() []byte            // 获取请求消息的数据
	GetMsgID() uint32           // 获取请求的 id
}

将 Request 中的 []byte 类型的 data 字段修改为 Message 类型,并实现 GetMsgID 方法。

package znet

import "zinx/ziface"

type Request struct {
	conn ziface.IConnection // 已经和客户端建立好的连接
	msg  ziface.IMessage    // 客户端请求的数据
}

var _ ziface.IRequest = (*Request)(nil)

// GetConnection 获取请求连接信息
func (r *Request) GetConnection() ziface.IConnection {
	return r.conn
}

// GetData 获取请求消息的数据
func (r *Request) GetData() []byte {
	return r.msg.GetData()
}

// GetMsgID 获取请求的消息ID
func (r *Request) GetMsgID() uint32 {
	return r.msg.GetMsgId()
}

集成拆包的过程

实际上我们要修改的就是我们在 connection.go 当中定义的 StartReader。在 StartReader 中,我们实现了从连接读取数据并执行相应业务(所绑定业务)的逻辑,在读取数据流的时候,我们需要先对客户端发来的包进行拆解,使用我们新定义的 DataPack 和 Message 来完成:

// StartReader 开启处理 conn 读数据的 goroutine
func (c *Connection) StartReader() {
	fmt.Println("Reader Goroutine is running")
	defer fmt.Println(c.RemoteAddr().String(), " conn reader exit !")
	defer c.Stop()

	for {
		// 创建封包拆包的对象
		dp := NewDataPack()
		
		// 读取客户端的 msg head
		headData := make([]byte, dp.GetHeadLen())	// 注意 GetHeadLen() 返回常量 8, 因为包的头部长度固定
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head error", err)
			c.ExitBuffChan <- true
			continue
		}
		
		// 拆包, 得到 msgid 和 datalen, 并放在 msg 中
		msg, err := dp.Unpack(headData)
		if err != nil {
			fmt.Println("unpack error", err)
			c.ExitBuffChan <- true
			continue
		}
		
		// 根据 dataLen 读取 data, 放在 msg.Data 中
		var data []byte
		if msg.GetDataLen() > 0 {
			data = make([]byte, msg.GetDataLen())
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data error", err)
				c.ExitBuffChan <- true
				continue
			}
		}
		msg.SetData(data)
		
		// 得到当前客户端请求的 Request 数据
		req := Request {
			conn: c,
			msg: msg,
		}
		
		// 从路由 Routers 中找到注册绑定 Conn 的对应 Handle
		go func(request ziface.IRequest) {
			// 执行路由中的方法
			c.Router.PreHandle(request)
			c.Router.Handle(request)
			c.Router.PostHandle(request)
		}(&req)
	}
}

需要重点强调的是,我们一定要理解 DataPack 的 Unpack 方法的作用,它的目的就是将包的头部 head 拆解出来,根据头部我们可以获取数据的长度,我们直接根据数据的长度创建一个对应大小的字节缓冲区(data := make([]byte, msg.GetDataLen()))来从字节流中读取 body 的数据(io.ReadFull(c.GetTCPConnection(), data))即可。

提供封包的方法

我们已经成功把拆包的功能集成到 Zinx 中了,但在使用 Zinx 的过程中,我们希望给用户返回一个 TLV 格式的数据,所以我们应该给 Zinx 定义一个封包的接口,供 Zinx 发包使用。

对 iconnection 进行修改,新增 RemoteAddr 和 SendMsg 方法(注意,之前已经为 Connection 实现了 RemoteAddr 方法,但是没将这个方法添加到接口当中,此处做一个补充):

type IConnection interface {
	Start()                                  // 启动连接
	Stop()                                   // 停止连接
	GetConnID() uint32                       // 获取远程客户端地址信息
	GetTCPConnection() *net.TCPConn          // 从当前连接获取原始的 socket TCPConn
	RemoteAddr() net.Addr                    // 获取远程客户端地址信息
	SendMsg(msgId uint32, data []byte) error // 直接将 Message 数据发给远程的 TCP 客户端
}

实现 SendMsg 方法,实际上就是将业务处理完毕的结果封包回发给 Client:

func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed == true {
		return errors.New("Connection closed when send msg")
	}
	
	// 将 data 封包
	dp := NewDataPack()
	msg, err := dp.Pack(NewMsgPackage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id = ", msgId)
		return errors.New("Pack error msg ")
	}
	
	// 将 data 发送
	if _, err := c.Conn.Write(msg); err != nil {
		fmt.Println("Write msg id ", msgId, " error ")
		c.ExitBuffChan <- true
		return errors.New("conn Write error")
	}
	return nil
}

使用 Zinx-v0.5 完成应用程序

现在我们使用 Zinx-v0.5 完成应用程序,在 Server 端绑定一个 Ping Handler 用户作为业务处理函数:

package main

import (
	"fmt"
	"zinx/settings"
	"zinx/ziface"
	"zinx/znet"
)

// ping test 自定义路由
type PingRouter struct {
	znet.BaseRouter
}

// Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
	fmt.Println("Call PingRouter Handle")
	//先读取客户端的数据,再回写ping...ping...ping
	fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

	//回写数据
	err := request.GetConnection().SendMsg(1, []byte("ping...ping...ping"))
	if err != nil {
		fmt.Println(err)
	}
}

func main() {
	// 首先初始化
	settings.Init()
	// 创建一个server句柄
	s := znet.NewServer()

	//配置路由
	s.AddRouter(&PingRouter{})

	//开启服务
	s.Serve()
}

在 Client 端发送消息,并接收来自 Server 的消息:

package main

import (
	"fmt"
	"io"
	"net"
	"time"
	"zinx/znet"
)

/*
模拟客户端
*/
func main() {

	fmt.Println("Client Test ... start")
	// 3 秒之后发起测试请求,给服务端开启服务的机会
	time.Sleep(3 * time.Second)

	conn, err := net.Dial("tcp", "127.0.0.1:7777")
	if err != nil {
		fmt.Println("client start err, exit!")
		return
	}

	for {
		//发封包message消息
		dp := znet.NewDataPack()
		msg, _ := dp.Pack(znet.NewMsgPackage(0, []byte("Zinx V0.5 Client Test Message")))
		_, err := conn.Write(msg)
		if err != nil {
			fmt.Println("write error err ", err)
			return
		}

		//先读出流中的head部分
		headData := make([]byte, dp.GetHeadLen())
		_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
		if err != nil {
			fmt.Println("read head error")
			break
		}
		//将headData字节流 拆包到msg中
		msgHead, err := dp.Unpack(headData)
		if err != nil {
			fmt.Println("server unpack err:", err)
			return
		}

		if msgHead.GetDataLen() > 0 {
			//msg 是有data数据的,需要再次读取data数据
			msg := msgHead.(*znet.Message)
			msg.Data = make([]byte, msg.GetDataLen())

			//根据dataLen从io中读取字节流
			_, err := io.ReadFull(conn, msg.Data)
			if err != nil {
				fmt.Println("server unpack data err:", err)
				return
			}

			fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
		}

		time.Sleep(1 * time.Second)
	}
}

启动服务端和客户端,在 Terminal 当中,Server 显示的信息如下:

Add Router succ! 
[START] Server listenner at IP: 127.0.0.1, Port 7777, is starting
start Zinx server   zinx server  succ, now listenning...
Reader Goroutine is running
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
... ... ...

Client 显示的信息如下:

Client Test ... start
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
... ... ...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值