Appearance
二进制存储数据
更新: 1/17/2026 字数: 0 字 时长: 0 分钟
二进制数据读写效率更高,体积更小,安全性更高,非常适合作为网络通信中传递数据的媒介。
二进制文件不仅是人眼不可阅读的,如果使用者不清楚这个二进制文件数据是如何存储的,就无法解析这个二进制文件。
数据 <——> 字节
通过BitConverter中的GetBytes方法将各种原始类型转化为字节。
cs
byte[] bytes = BitConverter.GetBytes(256); // [0, 1, 0, 0]同样通过BitConverter中的ToXxx方法将字节转化为原始数据,额外需要一个startIndex索引参数。
cs
int number = BitConverter.ToInt32(bytes, 0); // 256NOTE
基础类型中的decimal和string无法通过该方式转换。
对于字符串,需要采用一种编码方式再转化为字节数组。毋庸置疑,最通用的编码格式是 UTF-8。使用Encoding类来完成这个转换工作:
cs
byte[] bytes = Encoding.UTF8.GetBytes("Explosion!!!");
string s = Encoding.UTF8.GetString(bytes);文件操作
File类
.NET 原生的File类提供了许多有用的文件操作方法:
cs
// 1.判断文件是否存在
bool isValidPath = File.Exists(fileName)
// --- 写入 ---
// 2. 创建文件
FileStream fs = File.Create(fileName);
// 3. 写入字节数组
byte[] bytes = Encoding.UTF8.GetBytes("Explosion!!!");
File.WriteAllBytes(fileName, bytes);
// 4. 将指定的 string 数组一会会写入文件
string[] strArr = {"Black\nRed", "Explosion!!!","Cartethyia"}
File.WriteAllLines(fileName, strArr);
// --- 读取 ---
// 5. 读取字节数据
bytes = File.ReadAllBytes(Application.dataPath + "/LimBoo's Kin.txt");
// 6. 读取所有行信息
strArr = File.ReadAllLines(Application.dataPath + "/LimBoo's Kin.txt");
// 7. 读取所有文本信息
File.ReadAllText(Application.dataPath + "/LimBoo's Kin.txt");
// --- 删除 ---
// 8. 删除文件(文件需要关闭)
File.Delete(Application.dataPath + "/LimBoo's Kin.txt");
// --- 复制 ---
// 9. 复制文件到目标路径,true 代表强制覆盖(默认 false)
File.Copy(Application.dataPath + "/LimBoo's Kin.txt", targetFileName, true);
// --- 替换 ---
// 10. 用一个文件替换另一个文件
// 可填一个备份路径:替换发生前,旧的目标文件会被改名存放在这里。
File.Replace(sourceFileName,
destinationFileName,
destinationBackupFileName);
// --- 流 ---
// 需要关闭
FileStream fs = File.Open(fileName,
FileMode.OpenOrCreate,
FileAccess.ReadWrite);文件流
文件在内存与硬盘文件之间建立了一个数据传输通道。文件流利用缓冲区分批次加载文件,这意味着它会将整个文件放入内存,且可以一步一步地读写数据。适合用于处理大文件,断点续传/随机读写与网络传输。
FileStream只能读写字节 (byte)。
创建一个文件流
通过构造函数:
cs
FileStream fs = new FileStream(fileName,
FileMode.Create,
FileAccess.ReadWrite
);FileMode枚举:CreateNew:创建新文件,如果文件存在,则报错Create:创建文件,如果文件存在,则覆盖Open:打开文件,如果文件不存在,则报错OpenOrCreate:打开文件,如果文件不存在,则创建文件Append:若存在文件,则打开并查找文件尾,或者创建一个新文件Truncate:打开并清空文件内容
FileShare参数(可选):None:谢绝共享Read:允许别的程序读取该文件;Write:允许别的程序写入该文件ReadWrite:允许别的程序读写该文件
通过Create方法:
cs
FileStream fs = File.Create(fileName, 2048, FileOptions.None);- 参数二:缓存大小
- 参数三:描述如何创建或覆盖该文件
通过Open方法:
cs
FileStream fs = File.Open(fileName, FileMode.Create);重要方法
基础方法:
cs
// 获取文本字节长度
int length = fs.Length;
// 是否可读/可写
bool canRead = fs.canRead;
// 将缓冲区立刻写入
fs.Flush();
// 关闭 fs 释放资源,自定调用 Flush() 方法(二者效果相同)
fs.Close();
// fs.Dispose();
// 自动释放资源:
using (FileStream fs = new FileStream("file.txt", FileMode.Open)) {
// ...
}写入:
cs
fs.Write(byteArr, startIndex, length)当你要写入一个字符串时,由于字符串长度是不确定的,为了之后的读取,需要记录字符串的长度。通常的作法是先写入字符串的长度,再写入字符串自身。自己完成这个流程很麻烦,我们可以借助已有的轮子BinaryWriter和BinaryReader ,例如:
cs
// 传统方法
byte[] bytes = Encdoing.UTF8.GetBytes("Explosion!!!");
int length = bytes.Length
// 写入长度
fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
// 写入内容
fs.Write(bytes, 0, bytes.Length);
fs.Flush()
// 更简洁轻松的方法(已有轮子)
using (FileStream fs = new FileStream(path, FileMode.Create)) using (BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8)) {
// BinaryWriter 会自动处理字符串长度前缀
writer.Write("Explosion!!!");
// 你甚至可以顺便写入其他基础类型,比如 int, byte
// writer.Write(100);
}读取:
BinaryReader的ReadXxx()方法可以读取多种基础类型。如果之前写入了一个字节数组,读取时通常使用reader.ReadBytes(int count),参数count是你要读取的字节数量。
cs
using (FileStream fs = new FileStream(path, FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs, Encoding.UTF8))
{
// 指针跳转
// 参数1: 偏移量;参数2: 从哪里开始算 (此处是文件开头)
reader.BaseStream.Seek(500, SeekOrigin.Begin);
// 方法 B: 直接改 Position 属性 (效果同上)
// reader.BaseStream.Position = 500;
// ReadString 会自动读取长度前缀,然后读取对应长度的字符
string s = reader.ReadString();
}WARNING
BinaryWriter和BinaryReader必须成对使用。
文件夹操作
.NET 原生的Directory类提供了许多有用的文件操作方法:
cs
// 1. 断文件夹是否存在
bool isDirValid = Directory.Exists(path);
// 2. 创建文件夹(若文件已存在则直接返回 DirectoryInfo)
DirectoryInfo info = Directory.CreateDirectory(path);
// 3. 删除文件夹
// 参数二:是否删除非空目录
// true:删除整个目录;false:仅当该目录为空时才删除(默认)
Directory.Delete(path);
// 4. 查找文件夹
// 指定路径下所有文件夹名
string[] strs = Directory.GetDirectories(rootPath);
// 指定路径下所有文件名(不含文件夹)
strs = Directory.GetFiles(Application.dataPath);
// 5. 移动文件夹及其内容
// targetPath 已存在时会报错;代码实际实现为重命名
Directory.Move(originPath, targetPath);
// --- DirectoryInfo ---
// 1. 在创建和获取目录时会返回 DirectoryInfo
DirectoryInfo info = Directory.CreateDirectory(path);
dInfo = Directory.GetParent(path);
// 2. 属性
// 全路径
print(dInfo.FullName);
// 文件名
print(dInfo.Name);
// 3. 到所有子文件夹的目录信息
DirectoryInfo[] dInfos = dInfo.GetDirectories();
// --- FileInfo文件信息类 ---
// 通过 DirectoryInfo 得到该文件下的所有文件信息
FileInfo[] fInfos = dInfo.GetFiles();
for (int i = 0; i < fInfos.Length; i++)
{
print(fInfos[i].Name); // 文件名
print(fInfos[i].FullName); // 路径
print(fInfos[i].Length); // 字节长度
print(fInfos[i].Extension); // 后缀名
}二进制序列化
序列化 (Serialization) 就是把内存里复杂的对象,变成一串可以存储或传输的数据。
CAUTION
BinaryFormatter已经被微软官方宣布极度不安全且已弃用 (Obsolete),很多时候你也不必使用[System.Serialize]。
建议使用第三方高性能库,如 Protobuf-net 或 MessagePack。其速度极快,生成的文件极小,而且像写 JSON 一样简单(全是自动的)。
使用 Protobuf-net
这是 Google 开发的一种协议,被适配到了 C#。它能把你的类自动变成极小的二进制流。Protobuf 只是一个打包员。它负责把数据打包成字节,把这些字节运到硬盘上还得靠FileStream。
但注意,Protobuf 只能读取由自己生成的数据:对于任何不遵守 Protobuf 规则的数据(比如 PNG 图片、MP3 音乐、Excel 文件,或者是别的程序员用 BinaryWriter 随便写的文件),Protobuf 拿它们完全没办法。
定义数据类:
cs
// [ProtoContract] 表示这个类可以被序列化
[ProtoContract]
public class GameSaveData
{
// [ProtoMember(1)] 代表这个字段在二进制流里的唯一 id
// 由于 Protobuf 是靠 id 定位数据的,变量可以随意修改,增加
[ProtoMember(1)]
public string PlayerName;
[ProtoMember(2)]
public int Level;
[ProtoMember(3)]
public float Health;
[ProtoMember(4)]
public List<int> ItemIds;
// 没有标签的字段会被忽略
public string TempStatusString;
}序列化和反序列化:
cs
using System.IO;
using ProtoBuf;
public static class SaveSystem
{
// ==========================================
// 场景 A: 本地存储 (存硬盘)
// ==========================================
public static void Save(GameSaveData data, string path)
{
using (var file = File.Create(path)) // 管道通向硬盘
{
Serializer.Serialize(file, data);
}
}
public static GameSaveData Load(string path)
{
if (!File.Exists(path)) return null;
using (var file = File.OpenRead(path)) // 管道来自硬盘
{
return Serializer.Deserialize<GameSaveData>(file);
}
}
// ==========================================
// 场景 B: 网络传输 (存内存 -> byte[])
// ==========================================
/// <summary>
/// 发送端:把对象变成字节数组 (准备塞给 Socket.Send)
/// </summary>
public static byte[] SerializeToNetworkPacket(GameSaveData data)
{
// 创建一个内存流(临时的内存缓冲区)
// MemoryStream 可以接受一个数组
using (var ms = new MemoryStream())
{
// 魔法时刻:Protobuf 以为自己在写文件,其实写进了内存
Serializer.Serialize(ms, data);
// 把内存流里的数据全部倒出来,变成 byte[]
// 可以声明一个类成员数组,减少此处创建/销毁数组的开销
return ms.ToArray();
}
}
/// <summary>
/// 接收端:把收到的字节数组还原成对象 (来自 Socket.Receive)
/// </summary>
public static GameSaveData DeserializeFromNetworkPacket(byte[] receivedBytes)
{
// 把收到的死数据,重新包装成活的流
using (var ms = new MemoryStream(receivedBytes))
{
// 魔法时刻:从内存流里还原对象
return Serializer.Deserialize<GameSaveData>(ms);
}
}
}使用 BinaryWriter
cs
using System.IO;
public class PlayerData
{
public string Name;
public int Level;
public float Hp;
// --- 自己实现序列化逻辑 ---
// 把自己写入流
public void Serialize(BinaryWriter writer)
{
writer.Write(Name);
writer.Write(Level);
writer.Write(Hp);
}
// 从流里读取自己
public void Deserialize(BinaryReader reader)
{
Name = reader.ReadString();
Level = reader.ReadInt32();
Hp = reader.ReadSingle();
}
}
// --- 调用层工具类 ---
public static class PlayerNetworkManager
{
// ==========================================
// 场景 A: 本地存储
// ==========================================
public static void SaveGame(PlayerData data, string path)
{
using (FileStream fs = new FileStream(path, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
data.Serialize(writer);
}
}
// ==========================================
// 场景 B: 网络传输
// ==========================================
/// <summary>
/// 发送端:将 PlayerData 打包成网络包
/// </summary>
public static byte[] CreateNetworkPacket(PlayerData data)
{
// 1. 创建内存管道
using (MemoryStream ms = new MemoryStream())
{
// 2. 创建 Writer,骑在内存管道上
using (BinaryWriter writer = new BinaryWriter(ms))
{
// 3. 调用 PlayerData 自己的逻辑,让它写进 Writer
data.Serialize(writer);
// 4. 这个 bytes 就是你要发给服务器的东西
return ms.ToArray();
}
}
}
/// <summary>
/// 接收端:解析网络包
/// </summary>
public static PlayerData ParseNetworkPacket(byte[] packet)
{
PlayerData data = new PlayerData();
// 1. 把网络包(byte[]) 变成流
using (MemoryStream ms = new MemoryStream(packet))
using (BinaryReader reader = new BinaryReader(ms))
{
// 2. 让对象从流里读取自己
data.Deserialize(reader);
}
return data;
}
}加密
目前理论上除了 One-Time Pad 算法以外,是没有完全安全的算法的。但基于现实考虑,由于 One-Time Pad 算法需要和文件同样大小的密钥,除了非常需要安全性的场合,我们是不可能去使用它的。
简而言之,加密是为了使破解的成本 > 破解的收益。
常见的加密算法:
- AES (对称加密):目前全球通用的标准(美军严选)。速度很快,破解极难
- 简单异或
哈希算法用于验证完整性:
- HMAC(带密钥的哈希)
- SHA-256
- 别用 MD4 和 SHA1
XOR (异或混淆) 做法:
网络数据包,为了速度,非关键的数值,只需简单“防君子不防小人”。CPU 做异或的速度非常快,开销非常小:
cs
// 密钥
private static readonly byte[] Key = Encoding.UTF8.GetBytes("SecretKey");
// 加密和解密是同一个方法:调用一次是加密,再调用一次就是解密。
public static byte[] Process(byte[] data)
{
byte[] result = new byte[data.Length];
for (int i = 0; i < data.Length; i++)
{
// 每一个字节都和密钥的对应字节做异或运算
result[i] ^= Key[i % Key.Length];
}
return result;
}ASE:
- 初始向量 (IV) 的作用是让同样的明文在不同时候加密出不同的密文,就像随机数的 seed
- 初始向量必须是16字节,其可以是公开的
- 明文 -> 明文和 IV 异或 -> key 加密
cs
// 密钥:必须是 16, 24 或 32
// 实际发布时,最好通过简单算法运行时生成
private static readonly byte[] Key = Encoding.UTF8.GetBytes("12345678901234567890123456789012");
/// 加密:自动生成随机 IV,并拼接到结果的最前面
/// 结构:[IV (16 bytes)] + [加密数据]
public static byte[] Encrypt(byte[] data)
{
using (Aes aes = Aes.Create())
{
aes.Key = Key;
// 1. 让 AES 帮我们生成一个随机的 IV
aes.GenerateIV();
using (MemoryStream ms = new MemoryStream())
{
// 2. 【核心步骤】先把这个随机 IV 明文写入流的最开头
ms.Write(aes.IV, 0, aes.IV.Length);
// 3. 然后开始加密实际数据
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
}
return ms.ToArray();
}
}
}
/// 解密:自动从数据流的最前面提取 IV
public static byte[] Decrypt(byte[] encryptedDataWithIv)
{
using (Aes aes = Aes.Create())
{
aes.Key = Key;
using (MemoryStream ms = new MemoryStream(encryptedDataWithIv))
{
// 1. 准备一个 16 字节的数组来存 IV
byte[] ivBuffer = new byte[16];
// 2. 【核心步骤】先从流里读出前 16 个字节
// 如果读不够16字节,说明数据损坏了
int bytesRead = ms.Read(ivBuffer, 0, 16);
if (bytesRead < 16) throw new Exception("数据不完整,丢失 IV");
// 3. 把读出来的 IV 设置给 AES
aes.IV = ivBuffer;
// 4. 用这个 IV 解密剩下的部分
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (MemoryStream resultMs = new MemoryStream())
{
cs.CopyTo(resultMs);
return resultMs.ToArray();
}
}
}
}什么时候加密
- 严格加密
- 机密:用户的真实隐私、财产、凭证
- 重要游戏内容:经济系统、进度、成就
- 轻量加密
- 代码逻辑
- 美术资源
- 无关紧要
- 游戏设置:音量大小、分辨率、语言设置
- UI 文本/日志文件:你自己总得读吧