【go】binary包,大小端理解,read,write使用,自实现TCP封包拆包案例

binary.LittleEndian 是 Go 语言 encoding/binary 包中的一个常量,用于指定字节序(Byte Order)。字节序是指多字节数据在内存中存储的顺序,有两种主要方式:

  • 小端序(Little Endian) :低字节存于内存低地址,高字节存于高地址。例如,整数 0x12345678 在小端序中存储为 78 56 34 12
  • 大端序(Big Endian) :高字节存于内存低地址,低字节存于高地址。同样的整数 0x12345678 在大端序中存储为 12 34 56 78

encoding/binary 包概述

encoding/binary 包提供了字节序相关的编码和解码功能,常用于网络协议、文件格式或其他二进制数据的处理。主要功能包括:

  1. 字节序常量

    • binary.LittleEndian:小端序
    • binary.BigEndian:大端序
    • binary.NativeEndian:系统原生字节序(Go 1.19+)
  2. 核心函数

    • binary.Write(w io.Writer, order ByteOrder, data interface{}) error:将数据按指定字节序写入 io.Writer
    • binary.Read(r io.Reader, order ByteOrder, data interface{}) error:从 io.Reader 按指定字节序读取数据。
    • order.PutUint16/32/64(b []byte, v uint64):将整数按指定字节序写入字节切片。
    • order.Uint16/32/64(b []byte):从字节切片按指定字节序解析整数。

代码分析

go 复制代码
if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMsgId()); err != nil {
    return nil, err
}
  • dataBuff 是一个 io.Writer(如 bytes.Buffer),用于存储二进制数据。
  • binary.LittleEndian 指定使用小端序编码。
  • msg.GetMsgId() 返回一个整数(如 uint16),表示消息ID。

这段代码的作用是将消息ID按小端序写入 dataBuff。例如,如果 msg.GetMsgId() 返回 0x1234,则在 dataBuff 中存储为 34 12

选择字节序的建议

  • 网络协议:通常使用大端序(如 TCP/IP 协议)。
  • 文件格式:取决于具体规范(如 PNG 使用大端序,Windows PE 文件使用小端序)。
  • 系统内部 :建议与平台原生字节序一致(可用 binary.NativeEndian)。

示例代码

下面是一个简单示例,展示如何使用 binary.Writebinary.Read

go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

func main() {
	// 创建一个缓冲区
	var buf bytes.Buffer

	// 写入一个 uint32 整数(值为 0x12345678),使用小端序
	err := binary.Write(&buf, binary.LittleEndian, uint32(0x12345678))
	if err != nil {
		fmt.Println("写入错误:", err)
		return
	}

	// 输出缓冲区内容(小端序:78 56 34 12)
	fmt.Printf("小端序编码结果: %x\n", buf.Bytes())

	// 重置缓冲区以读取数据
	buf.Reset()
	buf.Write([]byte{0x78, 0x56, 0x34, 0x12}) // 手动写入小端序数据

	// 读取并解析为 uint32
	var result uint32
	err = binary.Read(&buf, binary.LittleEndian, &result)
	if err != nil {
		fmt.Println("读取错误:", err)
		return
	}

	fmt.Printf("解析结果: 0x%x\n", result) // 输出: 0x12345678
}

输出结果:

复制代码
小端序编码结果: 78563412
解析结果: 0x12345678

这个示例展示了如何使用 binary.Write 将整数按小端序编码,以及如何使用 binary.Read 解码。如果将 binary.LittleEndian 替换为 binary.BigEndian,则编码结果会变为 12345678


TCP封包拆包案例

  • 为了解决自实现tcp读取时发生粘包问题,而封装长度字段来识别tcp数据长度,避免多读其他数据段

转自刘丹冰大佬 https://www.bilibili.com/video/BV1wE411d7th/?p=5

datapack.go

go 复制代码
package znet

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

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

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

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

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

// 封包拆包类实例,暂时不需要成员
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 ziface.IMessage) ([]byte, error) {
	//创建一个存放bytes字节的缓冲
	dataBuff := bytes.NewBuffer([]byte{})

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

	//写dataLen
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetDataLen()); 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{}

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

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

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

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

datapack_test.go

go 复制代码
package znet

import (
	"fmt"
	"io"
	"net"
	"testing"
)

//只是负责测试datapack拆包,封包功能
func TestDataPack(t *testing.T) {
	//创建socket TCP Server
	listener, err := net.Listen("tcp", "127.0.0.1:7777")
	if err != nil{
		fmt.Println("server listen err:", err)
		return
	}

	//创建服务器gotoutine,负责从客户端goroutine读取粘包的数据,然后进行解析
	go func (){
		for{
			conn, err := listener.Accept()
			if err != nil{
				fmt.Println("server accept err:", err)
			}

			//处理客户端请求
			go func(conn net.Conn){
				//创建封包拆包对象dp
				dp := NewDataPack()
				for{
					//1 先读出流中的head部分
					headData := make([]byte, dp.GetHeadLen())
					_, err := io.ReadFull(conn, headData)  //ReadFull 会把msg填充满为止
					if err != nil {
						fmt.Println("read head error")
					}
					//将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.(*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))
					}
				}
			}(conn)

		}
	}()

	//客户端goroutine,负责模拟粘包的数据,然后进行发送
	conn, err := net.Dial("tcp", "127.0.0.1:7777")
	if err != nil{
		fmt.Println("client dial err:", err)
		return
	}

	//创建一个封包对象 dp
	dp := NewDataPack()

	//封装一个msg1包
	msg1 := &Message{
		Id:0,
		DataLen:5,
		Data:[]byte{'h', 'e', 'l', 'l', 'o'},
	}

	sendData1, err := dp.Pack(msg1)
	if err!= nil{
		fmt.Println("client pack msg1 err:", err)
		return
	}

	msg2 := &Message{
		Id:1,
		DataLen:7,
		Data:[]byte{'w', 'o', 'r', 'l', 'd', '!', '!'},
	}
	sendData2, err := dp.Pack(msg2)
	if err!= nil{
		fmt.Println("client temp msg2 err:", err)
		return
	}

	//将sendData1,和 sendData2 拼接一起,组成粘包
	sendData1 = append(sendData1, sendData2...)

	//向服务器端写数据
	conn.Write(sendData1)

	//客户端阻塞
	select{}
}

https://github.com/0voice

相关推荐
树码小子8 小时前
Java网络编程:(socket API编程:TCP协议的 socket API -- 回显程序的服务器端程序的编写)
java·网络·tcp/ip
路由侠内网穿透11 小时前
本地部署 GPS 跟踪系统 Traccar 并实现外部访问
运维·服务器·网络·windows·tcp/ip
酷飞飞17 小时前
Python网络与多任务编程:TCP/UDP实战指南
网络·python·tcp/ip
一起搞IT吧18 小时前
嵌入式ARM SOC开发中文专题分享一:ARM SOC外围资源介绍
arm开发·嵌入式硬件
研华嵌入式18 小时前
如何在高通跃龙QCS6490 Arm架构上使用Windows 11 IoT企业版?
arm开发·windows·嵌入式硬件
优雅鹅21 小时前
ARM、AArch64、amd64、x86_64、x86有什么区别?
arm开发·学习
o0o_-_1 天前
【go/gopls/mcp】官方gopls内置mcp server使用
开发语言·后端·golang
风_峰1 天前
【ZYNQ开发篇】Petalinux和电脑端的静态ip地址配置
网络·嵌入式硬件·tcp/ip·ubuntu·fpga开发
老六ip加速器1 天前
手机ip隔离方法
tcp/ip·智能手机·php
sheepwjl1 天前
《嵌入式硬件(十二):基于IMX6ULL的时钟操作》
汇编·arm开发·单片机·嵌入式硬件·时钟·.s编译