小端字节序和大端网络序
端序列基本上是网络开发必须要了解的知识点, 类似已知单个字节为如下:
[ 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 基本上传输数据都能匹配上.