11.Binary基础知识总结

11.总结


11.1 知识点

总结主要内容

学习内容回顾

优点

缺点

主要用处


11.2 核心要点速览

数据类型 ↔ 字节数组转换

  • 写到文件或发出去之前,内存里的东西最后都要变成 byte[]。整型、浮点、boolBitConverter;字符串用 System.Text.Encoding,不要拿 BitConverter 去硬转中文。
  • sizeof 是编译期运算符,用来扫一眼值类型占几个字节;string 不能 sizeof,长度运行期才知道。
  • 字符串、数组这种长度会变的字段,协议里通常前面跟一个固定宽度的长度(本系列练习里用 4 字节 int),读端才知道接下来 Read 多少字节。
操作 类/方法 代码示例 说明
数值转字节 BitConverter.GetBytes byte[] b = BitConverter.GetBytes(999); PC 上常见为小端;bool 一般 1 字节
字节转数值 BitConverter.ToInt32 int i = BitConverter.ToInt32(b, 0); 第二参是起始下标;后面凑不满 4 字节会异常
字符串转字节 Encoding.UTF8.GetBytes byte[] s = Encoding.UTF8.GetBytes("林文韬"); 项目里默认 UTF-8 最省心;和 GBK、系统 ANSI 混接容易乱码
字节转字符串 Encoding.UTF8.GetString string t = Encoding.UTF8.GetString(s, 0, s.Length); 可按偏移和长度从大缓冲区里切一段再解码
易混点 小端 / 长度头 Write 4 字节长度,再 Write UTF-8 字节 对端是大端或网络序时,要么约定清楚,要么自己 Reverse / 手写转换

文件操作(File 类)

  • 小文件、逻辑简单时,用 File.ReadAllBytes / WriteAllBytes 一类 API 一把读完或整包覆盖,代码最短。
  • 课程示例里经常写 Application.dataPath,方便在工程里看到文件;发给玩家的存档应落在 Application.persistentDataPath。个别平台路径大小写敏感,拼路径时别混。
  • 文件还被 FileStream 占着的时候去 File.Delete,往往会失败,先关流或让 using 结束。
操作 方法 说明
是否存在 File.Exists 避免对不存在路径直接读写
创建空文件 File.Create 返回 FileStream,可接着写
字节整包写/读 WriteAllBytes / ReadAllBytes 覆盖写;体量不大时够用
文本写/读 WriteAllText / ReadAllText 可指定 Encoding;注意 \n 等转义
按行写/读 WriteAllLines / ReadAllLines string[] 与多行文本互转
删除 / 复制 Delete / Copy Copy 目标已存在时,第三个参数传 true 才覆盖
替换 File.Replace 第三个参数可做备份路径
打开为流 File.Open 拿到 FileStream 后按字节精细读写

文件流操作(FileStream)

  • 文件按字节顺序读写:写的时候按协议一段段 Write,读的时候必须严格同顺序 Read。中间少读或多读一个字节,后面的字段全部错位。
  • Read 返回本次读到的字节数;流里有一个当前读写位置,连续调用会自动往后挪。
  • FileMode.Open 要求文件已在;Create 会清空重建;存档常用 OpenOrCreateAppend 从文件末尾接着写。
  • using (FileStream fs = …) 结束时调用 Dispose,比到处手写 Close 稳一些。
操作 用法 说明
打开 new FileStream(path, mode, access)File.Open 需要时加 FileShare,避免多进程抢锁
写入 Write(byte[] buf, int offset, int count) 同一个 byte[] 可以分段写多个逻辑字段
刷新 Flush() 把缓冲区里的数据推下去,异常退出前少丢一段
读一段 Read(buffer, offset, count) 典型套路:先读 4 字节得到长度,再按长度读字符串
一次读完 Length 分配 byte[]Read 只合适小文件;大文件应分块,避免一次吃掉太多内存

文件夹与信息类(Directory / DirectoryInfo / FileInfo)

  • Directory 的静态方法直接吃路径字符串:Exists、建目录、删目录、枚举。DirectoryInfo 绑在某一个目录实例上,方便拿 FullName、列子目录、列文件。
  • CreateDirectory 在目录已存在时不会把里面东西删掉,只是返回对应信息。
  • Move 会把整个文件夹挪走;目标位置如果已经有同名文件夹,调用会失败。
能力 API 说明
判断 / 创建 / 删目录 Exists / CreateDirectory / Delete Delete(path, false) 只删空目录;true 递归删除
列子目录 GetDirectories 返回子文件夹路径,不含普通文件
列文件 GetFiles 可配通配符和是否递归子目录
目录详情 DirectoryInfoFullNameNameGetDirectoriesGetFiles GetFiles 返回 FileInfo[],可看 LengthExtension
上级目录 Directory.GetParent 得到父目录的 DirectoryInfo

手写二进制协议的小例子(Student 存档)

  • 写入顺序与 Student 练习一致:age 四个字节 → name 的 UTF-8 长度(四个字节)→ name 的字节 → number 四个字节 → sex 一个 bool。读的时候用 index 记录当前偏移:读完 ageindex += 4,读完长度字段后再 index += 4 + nameBytesLength,其余字段同理。
  • 存档目录没有就先 CreateDirectoryFileStream 放在 using 里,Write 完记得 Flush
  • 读之前 File.Exists;小练习里可以把整文件读进一个 byte[],再按上面的顺序拆字段。正式项目变大后要改成流式或加分块,这里不展开。

BinaryFormatter 与可序列化类型

  • 需要交给 BinaryFormatter 的类型要标 [System.Serializable]。里面再嵌别的引用类型,那些类型也要能序列化,否则运行时要么丢字段要么直接异常。
  • 教程示例覆盖基本类型、数组、List<T>Dictionary<,>、自定义类/结构;与 Unity / .NET 版本相关的细节以你当前工程实测为准。
  • BinaryFormatter 在新版 .NET 里处境很差:标记过时、部分环境直接没有,而且不能用来吃来路不明的二进制。这套笔记用它,是为了把「对象 → 字节 → 文件」跑通;真项目存档请按组里规范换 Json、MessagePack、自研格式等。
[System.Serializable]
public class Person { … }

[System.Serializable]
public struct StructTest { … }

序列化 vs 反序列化 对比

流程 流对象 使用类 关键方法 说明
序列化(内存) MemoryStream BinaryFormatter SerializeToArray() 取字节 只要已写入的那一段;别拿 GetBuffer() 整数组去写盘或 XOR,尾部常有废字节
序列化(文件) FileStream BinaryFormatter Serialize(stream, obj) 对象图直接进文件
反序列化(文件) FileStream BinaryFormatter Deserialize(stream) → 对象 从磁盘文件还原
反序列化(内存) MemoryStream BinaryFormatter Deserialize(stream) → 对象 byte[] 包一层 MemoryStream,和「收到网络包再反序列化」同一套路

序列化流程要点

  • 内存流方式

    • new MemoryStream()
    • BinaryFormatter.Serialize(memoryStream, obj)
    • memoryStream.ToArray() 得到要保存或发送的字节
  • 文件流方式

    • new FileStream(..., FileMode.OpenOrCreate, FileAccess.Write)
    • 同一个 BinaryFormatter 往文件流里 Serialize
    • Flush(),流在 using 里关掉即可

反序列化流程要点

  • 文件流方式

    • File.OpenFileStream 只读打开
    • Deserialize 成目标类型,示例代码里用 as Person
  • 内存流方式

    • File.ReadAllBytes 先拉到内存(课上用来假装网络收包)
    • new MemoryStream(bytes)
    • Deserialize

加密与异或示例(演示向)

  • 序列化得到 byte[] 之后,再按你的规则变换;读盘回来先逆运算,再交给 Deserialize。他人拿到文件但不知道规则时,难以还原成对象。
  • 单字节 key 异或两次回到原值,用来理解「对称、可逆」够用了,谈不上密码学安全。真要加密,上 AES,密钥别写死在客户端明文里当唯一防线。
  • 正文里点到 MD5、SHA1、HMAC、AES,主要是认名字:谁是哈希、谁做消息认证、谁是对称加密。真要接库,直接用维护中的方案,别自己造轮子。

二进制数据管理器(单例封装)

  • 练习题里的写法:静态单例持有一个 SAVE_PATH,指向 persistentDataPath 下面的子目录。Save 里若目录不存在就 CreateDirectory
  • Save(object, fileName)FileStream 打开或创建,只负责写;BinaryFormatter 把对象写进去。
  • Load<T>(string fileName) where T : class:没有文件就 return default(T),一般是 null,外面要判断。有文件则只读打开,Deserialize 之后 as T

核心注意事项

场景 陷阱 应对
字节顺序 BitConverter 在 PC 上常见小端,和 Java 或部分网络字节序相反 协议写清楚;必要时对字段做 Reverse
变长字符串 只写 UTF-8 内容,没有长度 前面写 int 长度,再写 body
MemoryStream GetBuffer() 拿到的数组往往比实际写入长 ToArray(),或只处理 stream.Length 那么长
流未释放 文件被占用,Delete / 覆盖失败 using + 需要时 Flush
编码混用 读写两端编码不一致 全链路 UTF-8
非空目录 Delete(path, false) 对非空目录抛异常 true 递归删,或先手动清空
路径拼接 手写 \ / 在换平台时踩坑 Path.Combine;玩家数据走 persistentDataPath
短写长留 OpenOrCreate 复写更短的数据,文件尾部留着旧垃圾 写版本号、Create 截断、或删了重建
BinaryFormatter 来路不明的包、长期演进 课内练手;线上换团队认可的序列化方案

11.3 面试题精选

基础题

1. BitConverter 的字节序与 ToInt32 起始下标

题目

BitConverter.GetBytes 得到的字节有什么 endian 特点?BitConverter.ToInt32(byte[] bytes, int startIndex)startIndex 用错会怎样?

深入解析
  • 常见桌面环境上 BitConverter小端排字节;跨语言或网络要核对协议。可用 BitConverter.IsLittleEndian 看当前运行时是否小端。
  • startIndex 表示从数组哪一格开始凑 4 字节;后续长度不够会抛异常。
  • 多字段共用一个 byte[] 时,读写偏移必须成对一致。
答题示例

一般是小端,低位字节在前。

startIndex 决定从哪开始读 4 字节,算错或越界就异常或读出垃圾数。

参考文章
  • 2.各数据类型转字节数据

2. 字符串写进二进制为什么要带长度

题目

把字符串用 UTF-8 写成二进制时,为什么常先写一段长度?

深入解析
  • UTF-8 变长,读端不知道一次该 Read 多少字节。
  • 典型做法:先 int 存 UTF-8 字节数,再写内容;读时先 4 字节再读 body。
  • FileStream 顺序读写规则一致,错序整档报废。
答题示例

UTF-8 每个字符占的字节数不固定。

先写长度再写内容,读的时候先读长度再读那么多字节,协议才闭合。

参考文章
  • 4.文件操作相关-文件流相关
  • 6.文件操作相关-综合练习题

3. File 整包读写和 FileStream 的选型

题目

File.ReadAllBytes / WriteAllBytesFileStream 各适合什么场景?

深入解析
  • FileReadAllXX / WriteAllXX:代码短,适合小文件、配置、体积不大的快照。
  • FileStream:按块顺序读写,适合自定义协议、大文件分块。
  • Unity 里注意 persistentDataPath,并用 using 释放流。
答题示例

小文件一次读完或整包写,用 File 就行。

要自己控制字段顺序或文件很大,用 FileStream 分段读写。

参考文章
  • 3.文件操作相关-文件相关
  • 4.文件操作相关-文件流相关

进阶题

1. FileStream.Read 的返回值

题目

Stream.Read(byte[] buffer, int offset, int count) 返回值含义是什么?设计协议时要注意什么?

深入解析
  • 返回本次读到的字节数,某些流可能小于 count;文件流多数能读满,仍建议按返回值写健壮逻辑。
  • 流有当前位置,连续 Read 自动后移。
  • 定长字段与变长字段交错时,偏移要手算清楚。
答题示例

返回值是这次实际读了多少字节。

协议里 int、带长度字符串要按写的顺序读,offset 跟着挪。

参考文章
  • 4.文件操作相关-文件流相关

2. Directory.Delete 的两个参数

题目

Directory.Delete(string path, bool recursive)recursivefalsetrue 分别会怎样?

深入解析
  • false:仅空目录可删。
  • true:整棵子树删掉,危险,生产环境要确认或备份。
答题示例

false 目录里一有东西就删不掉。

true 递归全删,用之前要想清楚有没有重要文件。

参考文章
  • 5.文件操作相关-文件夹相关

3. MemoryStream:ToArray 与 GetBuffer

题目

序列化进 MemoryStream 后,ToArray()GetBuffer() 有何区别?为何和异或加密有关?

深入解析
  • ToArray() 只拷贝当前流里已写入的那一段,长度与流的逻辑长度一致。
  • GetBuffer() 拿到的是内部缓冲区引用,buffer.Length 往往大于 MemoryStream 已写入的字节数,尾部可能是未初始化的槽位。
  • GetBuffer() 整数组做 XOR 或 WriteAllBytes 会把尾部脏字节带进去,解密或反序列化失败;应用 ToArray() 或只处理前 stream.Length 字节。
答题示例

ToArray() 长度刚好是写入的数据。

GetBuffer() 可能后面多一截空的,拿来加密或写入会把脏数据弄进去。

参考文章
  • 7.CSharp类对象相关-序列化
  • 9.CSharp类对象相关-二进制数据加密

深度题

1. BinaryFormatter 的风险与替代

题目

为何不建议对不可信二进制做 BinaryFormatter 反序列化?Unity 项目里存档你会怎么选型?

深入解析
  • 属于典型不安全反序列化:可构造类型链(gadget)在反序列化时执行非预期逻辑;新版本 .NET 限制多或已移除。
  • 教学理解对象图可以;线上常见 JSON/MessagePack + 版本号、自研紧凑格式或 ScriptableObject 等。
  • 迁移要设计旧档兼容
答题示例

不可信二进制用 BinaryFormatter 反序列化,可能触发恶意 gadget 链执行代码。

真项目我更倾向带版本头的文本或二进制格式,或用团队定的序列化方案。

参考文章
  • 7.CSharp类对象相关-序列化
  • 8.CSharp类对象相关-反序列化

2. 手写协议与 BinaryFormatter

题目

像 Student 练习那样手写字段顺序,和直接 BinaryFormatter 序列化对象,各有什么利弊?

深入解析
  • 手写:体积、行为可控,易加魔数与版本;改字段要同步改读写。
  • BinaryFormatter:快但不透明、演进难、安全与跨语言差。
答题示例

手写协议透明、省空间,改结构麻烦。

BinaryFormatter 写得快,长期维护和安全性不如显式格式。

参考文章
  • 6.文件操作相关-综合练习题
  • 7.CSharp类对象相关-序列化

3. 本地加密的现实边界

题目

客户端存档做异或混淆能防什么?密钥放包里会怎样?

深入解析
  • 单层异或极易被逆向,只能挡随便看看文件的人。
  • 密钥硬编码终会被提取;防作弊常要服务端参与或校验。
  • 加密是提高成本,不是绝对安全。
答题示例

异或只能算演示,防不了会调试的人。

密钥在客户端总会被抠;真要保护得服务端和整套方案一起上。

参考文章
  • 9.CSharp类对象相关-二进制数据加密


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏