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
包提供了字节序相关的编码和解码功能,常用于网络协议、文件格式或其他二进制数据的处理。主要功能包括:
-
字节序常量:
binary.LittleEndian
:小端序binary.BigEndian
:大端序binary.NativeEndian
:系统原生字节序(Go 1.19+)
-
核心函数:
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)
:从字节切片按指定字节序解析整数。
代码分析
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.Write
和 binary.Read
:
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数据长度,避免多读其他数据段
datapack.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
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{}
}