对 Steam 下载的一次猜想

时间:2025-03-02 17:18:53

Steam 下载每次下次分配的硬盘空间通常是很小的,对比一些古董一般的游戏下载,需要将所有的游戏文件下载到本地,之后才能进行解压,这通常需要一倍以上的硬盘空间才能完成。而现代的游戏下载是一边下载一边解压,甚至下载到一部分还进行部分游戏

在这对游戏的分块下载进行一定的猜想和复现。

实现分块压缩

要实现分块下载,就需要将游戏文件进行分割成各种大小不一的压缩包,每个压缩包可以单独解压。为了尽可能的将个个压缩包的大小接近一致,就需要设定一个阈值当将一部分的文件整合后的大小接近该阈值就将这部分的文件进行压缩成一个压缩包。

使用 C# 进行编码,获取文件夹下所有的文件并进行分组,当每个组的大小接近基准值的时候就返回该组。进行编组时有的单体文件本身就大于基准值这时候将该文件直接进行返回,让他一个文件作为一个组,如果对其进行分割的话就达不到每个压缩包可以单独解压的效果了

/// <summary>
/// 将文件分块(每个块总大小尽可能接近指定基准值)
/// </summary>
/// <param name="rootPath">根目录路径</param>
/// <param name="size">基准块大小(默认100MB)</param>
/// <returns>分块结果集合</returns>
public static IEnumerable<Chunk> CreateChunks(string rootPath, long size = 100 * 1024 * 1024) {
	// 遍历文件夹下所有的文件夹并进行封装为 Entity
   var entities = Entity.Entities(rootPath)
       .OrderByDescending(e => e.GetFileInfo().Length) // 大文件优先处理
       .ToList();

   var chunks = new List<Chunk>();
   var currentChunk = new List<Entity>();
   long currentSize = 0;
   var id = 1;

   foreach (var entity in entities) {
       // 处理单个文件超过基准大小的情况
       if (entity.FileSize > size) {
           // next chunk
           if (currentChunk.Count != 0) {
               chunks.Add(new Chunk(rootPath, currentChunk, id++));
               currentChunk =[];
               currentSize = 0;
           }

           // add entity to chunk
           chunks.Add(new Chunk(rootPath, [entity], id++));
           continue;
       }

       // 常规分块逻辑
       if (currentSize + entity.FileSize <= size) {
           currentChunk.Add(entity);
           currentSize += entity.FileSize;
       } else {
           // 判断最小与最大哪个离基准线近
           if (size - currentSize < currentSize + entity.FileSize - size) {
               // 最小的块离基准线近
               chunks.Add(new Chunk(rootPath, currentChunk, id++));
               currentChunk =[entity];
               currentSize = entity.FileSize;
           } else {
               // 最大的块离基准线近
               currentChunk.Add(entity);
               chunks.Add(new Chunk(rootPath, currentChunk, id++));
               currentChunk =[];
               currentSize = 0;
           }
       }
   }

   // 添加最后剩余的块
   if (currentChunk.Count != 0) {
       chunks.Add(new Chunk(rootPath, currentChunk, id));
   }

   return chunks;
}

处理好分组后,就可以对每个组进行压缩了。压缩后会产生多个压缩包,每个压缩包的大小都会尽可能的贴近基准值。


/// <summary>
/// 创建分块并压缩为zip文件
/// </summary>
/// <param name="rootDirPath">要压缩的文件夹路径</param>
/// <param name="outputDirPath">输出目录路径</param>
/// <param name="baseFileName">压缩文件名</param>
/// <param name="size">块大小(最低压缩基准线,如果单体文件大于基准线则单独压缩)</param>
/// <returns>json 结果</returns>
public static string CreateChunksZipFile(string rootDirPath, string? outputDirPath = null, string? baseFileName = null, long size = 512 * 1024 * 1024) {
   if (!Directory.Exists(rootDirPath)) throw new DirectoryNotFoundException("Directory not found.");
   outputDirPath ??= Path.Combine(rootDirPath, "fuck_zip_output");
   if (!Directory.Exists(outputDirPath)) Directory.CreateDirectory(outputDirPath);

   var start = DateTime.Now;
   var chunks = Chunk.CreateChunks(rootDirPath, size);
   var rootJson = new JsonObject();
   var rootJsonArray = new JsonArray();
   var entityCount = 0;
   var totalSize = 0L;
   foreach (var chunk in chunks) {
       if (baseFileName != null) chunk.BaseFileName = baseFileName;
       Console.WriteLine(chunk);
       entityCount += chunk.Entities.Count();
       totalSize += chunk.TotalSize;
       // 性能严重消耗处
       var zipArchive = chunk.ZipArchive(outputDirPath);

       var json = new JsonObject{
           { "id", chunk.Id },
           { "fileName", chunk.GetFileName() },
           { "entityCount", entityCount },
           { "size", chunk.TotalSize }
       };
       var entityJsonArray = new JsonArray();
       foreach (var entity in chunk.Entities) {
           var json2 = new JsonObject{
               { "path", entity.RelativeFilePath },
               { "hashcode", entity.GetHashcode() } // 性能严重消耗处
           };
           entityJsonArray.Add(json2);
       }

       json.Add("entities", entityJsonArray);
       // 性能严重消耗处
       json.Add("hashcode", HashCodeUtil.CalculateFileHash(zipArchive));
       rootJsonArray.Add(json);
   }

   rootJson.Add("chunks", rootJsonArray);
   rootJson.Add("count", rootJsonArray.Count);
   rootJson.Add("totalSize", totalSize);
   rootJson.Add("entityCount", entityCount);
   rootJson.Add("date", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
   rootJson.Add("_output", outputDirPath);
   rootJson.Add("_timeConsuming", DateTime.Now.Subtract(start).TotalSeconds);
   rootJson.Add("_input", rootDirPath);
   // 生成最终 JSON 字符串(带缩进)
   var finalJson = rootJson.ToJsonString(new JsonSerializerOptions{
       WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
   });

   // 写入文件
   var outputPath = Path.Combine(outputDirPath, "result.json");
   File.WriteAllText(outputPath, finalJson);
   Console.WriteLine($"{outputPath} created.");
   return finalJson;
}