Ainx的消息封装

news2025/6/18 10:00:20
在这里插入图片描述

📕作者简介: 过去日记,致力于Java、GoLang,Rust等多种编程语言,热爱技术,喜欢游戏的博主。
📗本文收录于Ainx系列,大家有兴趣的可以看一看
📘相关专栏Rust初阶教程、go语言基础系列、spring教程等,大家有兴趣的可以看一看
📙Java并发编程系列,设计模式系列、go web开发框架 系列正在发展中,喜欢Java,GoLang,Rust,的朋友们可以关注一下哦!


📙 本文大部分都是借鉴刘丹冰大佬的zinx框架和文章,更推荐大家去读大佬的原文,本文只是个人学习的记录

文章目录

  • Ainx的消息封装
    • 创建消息封装类型
    • 消息的封包与拆包
      • 创建拆包封包抽象类
      • 实现拆包封包类
    • Ainx-V0.5代码实现
      • Request字段修改
      • 集成拆包过程
      • 提供封包方法
    • 使用Ainx-V0.5完成应用程序

Ainx的消息封装

接下来我们再对Ainx做一个简单的升级,现在我们把服务器的全部数据都放在一个Request里,当前的Request结构如下:

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

很明显,现在是用一个[]byte来接受全部数据,又没有长度,又没有消息类型,这不科学。怎么办呢?我们现在就要自定义一种消息类型,把全部的消息都放在这种消息类型里。

创建消息封装类型

在ainx/ainterface/下创建imessage.go文件

ainx/ainterface/imessage.go

package ainterface

/*
将请求的一个消息封装到message中,定义抽象层接口
*/
type IMessage interface {
	GetDataLen() uint32 //获取消息数据段长度
	GetMsgId() uint32   //获取消息ID
	GetData() []byte    //获取消息内容

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

同时创建实例message类,在ainx/anet/下,创建message.go文件

ainx/anet/message.go

package anet

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

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

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

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

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

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

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

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

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

消息的封包与拆包

我们这里就是采用经典的TLV(Type-Len-Value)封包格式来解决TCP粘包问题吧。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
由于Ainx也是TCP流的形式传播数据,难免会出现消息1和消息2一同发送,那么zinx就需要有能力区分两个消息的边界,所以Ainx此时应该提供一个统一的拆包和封包的方法。在发包之前打包成如上图这种格式的有head和body的两部分的包,在收到数据的时候分两次进行读取,先读取固定长度的head部分,得到后续Data的长度,再根据DataLen读取之后的body。这样就能够解决粘包的问题了。

创建拆包封包抽象类

在ainx/ainterface下,创建idatapack.go文件
ainterface

ainx/ainterface/idatapack.go

package ainterface

/*
封包数据和拆包数据
直接面向TCP链接中的数据流,为传输数据添加头部信息,用于处理TCP粘包问题。
*/
type IDataPack interface {
	GetHeadLen() uint32                //获取包头长度方法
	Pack(msg IMessage) ([]byte, error) //封包方法
	Unpack([]byte) (IMessage, error)   //拆包方法
}

实现拆包封包类

在ainx/anet/下,创建datapack.go文件.

ainx/anet/datapack.go

package anet

import (
	"ainx/ainterface"
	"ainx/utils"
	"bytes"
	"encoding/binary"
	"errors"
)

// 封包拆包实例,暂时不需要成员
type DataPack struct {
}

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

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

// 封包方法(压缩)
func (dp *DataPack) Pack(msg ainterface.IMessage) ([]byte, error) {
	// 创建一个存放bytes字节的缓冲
	dataBuff := bytes.NewBuffer([]byte{})
	写dataLen
	//字节序 就是多字节数据类型 (int, float 等)在内存中的存储顺序。可分为大端序,低地址端存放高位字节;小端序与之相反,低地址端存放低位字节。
	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) (ainterface.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 utils.GlobalSetting.MaxPacketSize > 0 && msg.DataLen > utils.GlobalSetting.MaxPacketSize {
		return nil, errors.New("Too large msg data recieved")
	}

	//这里只需要把head的数据拆包出来就可以了,然后再通过head的长度,再从conn读取一次数据
	return msg, nil
}

Ainx-V0.5代码实现

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

Request字段修改

首先我们要将我们之前的Request中的[]byte类型的data字段改成Message类型.

ainx/anet/request.go

package anet

import "ainx/ainterface"

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

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

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

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

集成拆包过程

接下来我们需要在Connection的StartReader()方法中,修改之前的读取客户端的这段代码:

func (c *Connection) StartReader() {
    
	//...
    
	for  {
		//读取我们最大的数据到buf中
		buf := make([]byte, utils.GlobalSetting.MaxPacketSize)
		_, err := c.Conn.Read(buf)
		if err != nil {
			fmt.Println("recv buf err ", err)
			c.ExitBuffChan <- true
			continue
		}
        
        //...
        
    }
}

改成如下:

ainx/anet/connection.go

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())
		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, //将之前的buf 改成 msg
		}
		//从路由Routers 中找到注册绑定Conn的对应Handle
		go func(request ainterface.IRequest) {
			//执行注册的路由方法
			c.Router.PreHandle(request)
			c.Router.Handle(request)
			c.Router.PostHandle(request)
		}(&req)
	}
}

提供封包方法

现在我们已经将拆包的功能集成到Ainx中了,但是使用Ainx的时候,如果我们希望给用户返回一个TLV格式的数据,总不能每次都经过这么繁琐的过程,所以我们应该给Ainx提供一个封包的接口,供Ainx发包使用。

ainx/ainterface/iconnection.go

新增SendMsg()方法


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

// 定义⼀一个统⼀一处理理链接业务的接⼝口
type HandFunc func(*net.TCPConn, []byte, int) error

ainx/anet/connection.go

SendMsg()方法实现:

// 直接将Message数据发送数据给远程的TCP客户端
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 ")
	}

	//写回客户端
	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
}

使用Ainx-V0.5完成应用程序

现在我们可以基于Ainx框架完成发送msg功能的测试用例了。

Server.go

package main

import (
	"ainx/ainterface"
	"ainx/anet"
	"fmt"
)

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

// Test Handle
func (this *PingRouter) Handle(request ainterface.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() {
	//创建一个server句柄
	s := anet.NewServer("Ainx V0.5")

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

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

Client.go

package main

import (
	"ainx/anet"
	"fmt"
	"io"
	"net"
	"time"
)

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

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

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

	for {
		//发封包message消息
		dp := anet.NewDataPack()
		msg, _ := dp.Pack(anet.NewMsgPackage(0, []byte("Ainx 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.(*anet.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)
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1503672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

300分钟吃透分布式缓存-25讲:Redis是如何处理容易超时的系统调用的?

BIO 线程简介 Redis 在运行过程中&#xff0c;不可避免的会产生一些运行慢的、容易引发阻塞的任务&#xff0c;如将内核中的文件缓冲同步到磁盘中、关闭文件&#xff0c;都会引发短时阻塞&#xff0c;还有一些大 key&#xff0c;如一些元素数高达万级或更多的聚合类元素&#…

CNN中的参数,计算量,FLOPs,Multi-Add(乘加),输出特征图尺寸和通道变化

在阅读论文时&#xff0c;我们会遇到参数量&#xff0c;FLOPS&#xff0c;Multi-add&#xff0c; CNN参数&#xff0c;CNN计算量等概念&#xff0c;通过阅读整理&#xff0c;这篇博客希望以最简洁的解释帮助大家理解这些基本概念。 首先&#xff0c;我们看一下卷积的计算方式&a…

【Linux】编译器-gcc/g++使用

个人主页 &#xff1a; zxctscl 文章封面来自&#xff1a;艺术家–贤海林 如有转载请先通知 文章目录 1. 前言2. 初见gcc和g3. 程序的翻译过程3.1 预处理3.1.1 宏替换 去注释 头文件展开3.1.2 条件编译 3.2 编译3.3 汇编3.4 链接 4. 链接4.1 动态链接4.2 静态链接 1. 前言 在之…

Vue+OpenLayers7入门到实战:OpenLayers7点聚合(聚散点)功能,地图缩小显示聚集数量,点击聚集点散开和地图放大后显示要素图片

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7入门到实战 前言 本章介绍如何使用OpenLayers7在地图上实现地图点聚合(聚散点)功能,实现地图缩小显示聚集数量,点击聚集点和地图放大后显示要素对应icon图片的功能。 二、依赖和使用 "ol": "7.5.2"…

【Linux】CentOS网络故障排查大揭秘: 实战攻略解读

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 检查网络连接状态&#xff1a; 检查网络配置&#xff1a; 重启网络服务&#xff1a; 检查防火墙设置&#xff1a; 查看日志文…

基于极大似然算法的系统参数辨识matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于极大似然算法的系统参数辨识。对系统的参数a1&#xff0c;b1&#xff0c;a2&#xff0c;b2分别进行估计&#xff0c;计算估计误差以及估计收敛曲线&#xff0…

【java】22:try-catch 异常处理

try-catch 方式处理异常说明 public static void main(String[] args) { int num1 10; int num2 0; try { int res num1 / num2; } catch (Exception e) { System.out.println(e.getMessage()); } } 注意事项 1)如果异常发生了&#xff0c;则异常发生后面的代码不会执行&…

SpringCloudGateway全局过滤器

文章目录 全局过滤器的作用自定义全局过滤器过滤器执行的顺序 上一篇 Gateway理论与实践 介绍的过滤器&#xff0c;网关提供了31种&#xff0c;但每一种过滤器的作用都是固定的。如果我们希望拦截请求&#xff0c;做自己的业务逻辑则没办法实现。 全局过滤器的作用 全局过滤器的…

户用光伏创新技术,引领光伏时代进步

户用光伏近几年由于国家政策支持力度加大&#xff0c;技术也在快速发展&#xff0c;成功引领我国光伏时代的进步&#xff0c;掌握核心技术必将在新能源市场中抢占主导地位&#xff01; 一、制造方面 1.高效低成本晶硅太阳能电池表界面制造技术 这项技术主要涉及晶硅太阳能电池…

作业 字符数组-统计和加密

字串中数字个数 描述 输入一行字符&#xff0c;统计出其中数字字符的个数。 输入 一行字符串&#xff0c;总长度不超过255。 输出 输出为1行&#xff0c;输出字符串里面数字字符的个数。 样例 #include <iostream> #include<string.h> using namespace std; int m…

2024 ssh连接linux ,包括连接被拒的解决方案

这里以windows系统 连接 linux&#xff08;centOS&#xff09;为例&#xff1a; 一、如果windows 连接时出现&#xff1a; Permission denied, please try again. 连接被拒绝&#xff0c;做出以下修改&#xff1a; 打开linux - Terminal 输入&#xff1a;cat /etc/ssh/sshd_c…

什么是ElasticSearch的深度分页问题?如何解决?

在ElasticSearch中进行分页查询通常使用from和size参数。当我们对ElasticSearch发起一个带有分页参数的查询(如使用from和size参数)时,ElasticSearch需要遍历所以匹配的文档直到达到指定的起始点(from),然后返回从这一点开始的size个文档 在这个例子中: 1.from 参数定义…

算法设计与分析(超详解!) 第一节 算法概述

1.算法的定义 算法的非形式化定义&#xff1a;算法是规则的有限集合&#xff0c;是为解决特定问题而规定的一系列操作。 可以理解为&#xff1a;算法&#xff08;algorithm&#xff09;是指在解决问题时&#xff0c;按照某种机械的步骤一定可以得到问题的结果&#xff08;有的…

【C/C++】常量指针与指针常量的深入解析与区分(什么是const int * 与 int * const ?)

目录 一、前言 二、const 的简单介绍 三、常量指针 &#x1f50d;介绍与分析 &#x1f4f0;小结与记忆口诀 四、指针常量 &#x1f50d;介绍与分析 &#x1f4f0;小结与记忆口诀 五、总结与提炼 六、共勉 一、前言 在【C/C】的编程中&#xff0c;指针与const关键字的组合…

Java实现从本地读取CSV文件数据

一、前言 最近项目中需要实现这样一个功能&#xff0c;就是从本地读取CSV文件&#xff0c;并以指定行作为标题行&#xff0c;指定行开始作为数据读取行&#xff0c;读取数据并返回给前端&#xff0c;下面具体说下是如何通过java实现。 二、如何实现&#xff1f; 1.引入相关mav…

【vue.js】文档解读【day 4】 | 事件处理

如果阅读有疑问的话&#xff0c;欢迎评论或私信&#xff01;&#xff01; 文章目录 事件处理前言监听事件内联事件处理器方法事件处理器方法与内联事件判断在内联处理器中调用方法在内联事件处理器中访问事件参数修饰符事件修饰符按键修饰符常规按键别名系统按键别名组合按键ex…

C++篇 语 句

到目前为止&#xff0c;我们只见过两种语句&#xff1a; return 语句和表达式语句。根据语句对执行顺 序的影响&#xff0c;C 语言其余语句大多属于以下 3 大类。 选择语句&#xff1a; if 语句和 switch 语句。循环语句&#xff1a; while 语句&#xff0c; do...while 语句和…

EF类和E/F类功率放大器(能量转换器)的波形推导和理想仿真--基于Matlab和ADS

EF类和E/F类功率放大器&#xff08;能量转换器&#xff09;的波形推导和理想仿真–基于Matlab和ADS 参考论文&#xff1a;Modeling and Analysis of Class EF and Class E/F Inverters With Series-Tuned Resonant Networks(2016) 这篇文章的思路和MTT的文章A Generalized Hi…

Angular基础---HelloWorld---Day2

文章目录 1.循环语句&#xff1a; *ngfor2.循环语句&#xff1a;ngSwitch4.事件的绑定:click5.事件的绑定:input6.模版引用变量7.数据双向绑定ngModel8.动态表单控件9.动态表单空间组 文末附有代码仓库地址&#xff01;&#xff01;&#xff01; 1.循环语句&#xff1a; *ngfor…

变换,动画

面试题——需求&#xff1a;在不知道父元素与子元素的宽高时 如何让子元素在父元素内居中&#xff1f; 1.定位 父相子绝 2.子元素 top&#xff1a;50% left:50% 3.子元素 transform: translate(-50%,-50%) .parent{height: 500px;background-color: red;position: relative;}.c…