MeteorCat / 小端字节序和大端网络序

Created Sat, 14 Oct 2023 14:34:24 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
1370 Words

小端字节序和大端网络序

端序列基本上是网络开发必须要了解的知识点, 类似已知单个字节为如下:

[              int32                    ]
[   8          8         8        8     ] 每个 8 代表 1 字节, 所以 int32 占用 4 字节  
[11111111 |00000000 |00000000 |00000000 ] 每 1|0 代表二进制占位, 所以这里合计总共 32 位

由多个 字(word) 组成为 字节(bytes), 内部的多个 word 排列顺序就是字节序, 在日常很多都会用到连续字节传输储存:

假设 0x1234567 占用 4 字节, 内部空了 4 个字节放置该变量
[0x100] [0x101] [0x102] [0x103]

日常如果这个顺序应该按照上面顺序排列, 这种就是大端序列:
[ 01(0x100) ] [ 23(0x101) ] [ 45(0x102) ] [ 67(0x103) ]

但是除了大端序列还有另外的小端序列:
[ 67(0x103) ] [ 45(0x102) ] [ 23(0x101) ] [ 01(0x100) ]

可以看到完全反转的序列, 这就是小端字节序.

明明有大端序列更加符合直觉, 为什么需要小端字节序列? 因为对于计算机电路来说优先处理低位效率更高, 所以一般是从低位开始装载.

低位高位实际上是低电平和高电平, 属于硬件计算机方面学科, 这里所说的高位低位实际上代表正序和倒叙字节位顺序.

一般来说, 计算机内部处理器都是采用 小端字节序, 其他场合如网络和文件存储IO流则是以大端字节序.

所以网络传输默认都是以大端字节序处理, 这里通过 CPU 采用的默认字节序:

#include <stdio.h>

int main(){
    // 联合体对象
    union{
        int n;
        char ch;
    } data;
    data.n = 0x00000001;  // 也可以直接写作 data.n = 1;
    
    // 这里如果CPU是小端, 按照排序原则会将 n 的值( 0x1 )和 ch( 0x0 ) 做交换
    // 如果小端会导致 { n:0x1, ch:0x0 } 转变为 { n:0x0, ch:0x1 }
    if(data.ch == 1){
        printf("Little-endian\n");
    }else{
        printf("Big-endian\n");
    }
    return 0;
}

这里就是怎么识别 CPU 默认采用大端序还是小端序去处理

Unity 传输处理

如果你开发网络服务端, 那么和客户端做数据交互的清空优先会遇到大小端传输问题, 游戏服务端常见的 Unity 当中默认二进制数据库传输采用小端存储, 在传输的过程当中需要转化下.

这里先用 Rust 写个简单的推送服务推送二进制数据给 Unity:

use std::io::{Read, Write};

// 监听服务, 无需引入其他依赖就能跑的TCP服务
fn main() -> Result<(), Box<dyn std::error::Error>> {

    // 服务监听地址
    let address = "127.0.0.1:8088";
    let listener = std::net::TcpListener::bind(address)?;


    // 如果有请求就返回 `hello.world` 的二进制
    let message = "hello.world";
    let message_bytes = message.as_bytes();
    println!("Send Message Bytes = {:x?}", message_bytes);// 打印十六进制数据
    let mut buffer = [0; 65535];
    for stream in listener.incoming() {
        match stream {
            Ok(mut tcp) => {
                if let Ok(n) = tcp.read(&mut buffer) {
                    println!("Read Message By Client: {:x?}", &buffer[0..n]);
                    if let Err(e) = tcp.write_all(message_bytes) {
                        eprintln!("Error! {:?}", e);
                    };
                };
            }
            Err(e) => {
                eprintln!("ERROR! {:?}", e)
            }
        }
    }
    Ok(())
}

启动后监听了 8088 端口, 而且能够看到大端网络序列打印出来的 hello.world 十六进制字节序内容:

# 这里就是会推送给 Unity 的正确十六进制大字节序列, 牢记这里的字节序这就是大端网络字节序
Send Message Bytes = [68, 65, 6c, 6c, 6f, 2e, 77, 6f, 72, 6c, 64]

之后就是在 Unity 当中编写脚本测试挂载跑下传输和打印:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

public class MainScript : MonoBehaviour
{
    /// <summary>
    /// 访问地址
    /// </summary>
    public string hostname = "127.0.0.1";

    /// <summary>
    /// 访问端口
    /// </summary>
    public int port = 8088;


    /// <summary>
    /// 客户端句柄
    /// </summary>
    private Socket socket;

    /// <summary>
    /// 消息内容
    /// </summary>
    public string message = "hello.world";


    /// <summary>
    /// 缓冲区
    /// </summary>
    private byte[] buffer = new byte[65535];


    /// <summary>
    /// 初始化请求
    /// </summary>
    void Start()
    {
        if(hostname.Trim().Length == 0)
        {
            throw new System.Exception("Failed by socket address");
        }

        // 链接 Tcp 句柄
        socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
        socket.Connect(new IPEndPoint(IPAddress.Parse(hostname),port));

        // 推送消息
        SendMsgBytes();
    }


    /// <summary>
    /// 推送消息
    /// </summary>
    private void SendMsgBytes()
    {
        if(socket != null && socket.Connected && message.Trim().Length != 0)
        {
            // 确定默认编码: 注意这里会加载系统默认编码, Unity 较新版本默认采用UTF-8
            // 但是千万注意, 不要使用 Encoding.Default, 因为你无法确定你平台 default 永远默认 utf-8
            Debug.LogFormat("Encoding = {0}", Encoding.Default);

            // 推送二进制消息注意观察这里的数据
            // 为了测试推送小端, 可以手动指定小端序列二进制
            byte[] bytes = Encoding.Unicode.GetBytes(message);
            socket.Send(bytes);

            // 获取返回的字节数据, 这里直接采用 UTF-8 加载
            int sz = socket.Receive(buffer);
            string receiveMsg = Encoding.UTF8.GetString(buffer, 0,sz);
            Debug.LogFormat("Receive = {0}",receiveMsg);
        }
    }

    /// <summary>
    /// 退出时候关闭会话
    /// </summary>
    private void OnApplicationQuit()
    {
        if(socket != null) { 
            socket.Close();
        }
    }
}

直接执行之后在服务端获取拿到的数据, 注意新版本 Unity 环节可能默认采用 UTF8 编码导致数据都是以大端网络序处理; 这里其实作为样板示例知道就行, 后续对协议了解有这回事即可, 在最新版本 Unity 基本上传输数据都能匹配上.