温嘉玲 發表於 2025-6-23 10:51:18

.NET8 gRPC实现高效100G大文件断点续传工具

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">前言</a></li><li><a href="#_label1">项目介绍</a></li><li><a href="#_label2">项目功能</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_0">核心功能</a></li><li><a href="#_lab2_2_1">附加功能</a></li></ul><li><a href="#_label3">项目特点</a></li><ul class="second_class_ul"></ul><li><a href="#_label4">项目技术</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_2">前端技术</a></li><li><a href="#_lab2_4_3">后端通信</a></li><li><a href="#_lab2_4_4">数据处理</a></li><li><a href="#_lab2_4_5">本地存储</a></li><li><a href="#_lab2_4_6">NuGet 包依赖</a></li></ul><li><a href="#_label5">项目代码</a></li><ul class="second_class_ul"></ul><li><a href="#_label6">项目效果</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2>
<p>随着数字化和信息化的发展,大文件传输在企业、科研以及个人用户中变得越来越常见。传统的文件传输方式在面对大文件(如几十GB甚至上百GB的视频、工程数据)时,常常因网络不稳定、程序崩溃等原因导致传输失败,而重新上传又浪费大量时间和带宽资源。</p>
<p>为了解决这一问题,本文推荐一个基于<strong>WinForm</strong>&nbsp;和&nbsp;<strong>.NET gRPC</strong>&nbsp;技术实现的大文件断点续传工具。该工具不仅支持最大100GB文件的高效传输,还具备在网络中断后从中断点继续传输的能力,大大提高了传输效率与稳定性。</p>
<p class="maodian"><a name="_label1"></a></p><h2>项目介绍</h2>
<p>项目是一个面向桌面端用户的&nbsp;<strong>大文件断点续传工具</strong>,采用&nbsp;<strong>WinForm</strong>&nbsp;构建前端界面,使用&nbsp;<strong>ASP.NET Core gRPC</strong>&nbsp;实现后端服务通信。</p>
<p>其核心目标是提供一种轻量级、可靠且易于扩展的文件传输解决方案,适用于需要频繁进行大文件上传的企业或开发人员。</p>
<p>该项目不依赖复杂的第三方组件,完全基于.NET 生态构建,具有良好的跨平台潜力和可维护性。</p>
<p class="maodian"><a name="_label2"></a></p><h2>项目功能</h2>
<p class="maodian"><a name="_lab2_2_0"></a></p><h3>核心功能</h3>
<ul><li>大文件支持:支持最大100GB的单个文件上传。</li><li>断点续传机制:在网络中断或客户端异常退出后,能够从中断位置继续上传。</li><li>分块传输策略:将大文件切分为多个小块进行传输,提升传输稳定性和并发处理能力。</li><li>实时进度显示:在界面上动态展示当前上传进度、速度及剩余时间。</li><li>传输管理控制:支持暂停、继续、取消等操作,增强用户体验。</li></ul>
<p class="maodian"><a name="_lab2_2_1"></a></p><h3>附加功能</h3>
<ul><li>文件校验机制:通过MD5或SHA1算法验证上传前后文件的一致性,确保数据完整性。</li><li>传输日志记录:自动记录每次上传的日志信息,便于追踪和故障排查。</li><li>本地状态持久化:使用SQLite数据库保存传输状态,保障断点信息不丢失。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>项目特点</h2>
<ul><li>技术先进:采用最新的&nbsp;<strong>.NET 8</strong>&nbsp;框架,结合&nbsp;<strong>gRPC</strong>&nbsp;协议,实现了高性能的远程调用和流式传输。</li><li>架构清晰:前后端分离设计,前端负责交互,后端专注业务逻辑与数据传输,便于后期扩展。</li><li>协议高效:基于&nbsp;<strong>HTTP/2</strong>&nbsp;的&nbsp;<strong>gRPC</strong>&nbsp;协议,具备低延迟、高吞吐量的优势,非常适合大文件流式上传。</li><li>本地状态管理:使用 SQLite 存储上传状态,实现断点信息的持久化。</li><li>序列化统一:采用&nbsp;<strong>Protocol Buffers (Protobuf)</strong>&nbsp;进行数据结构定义和序列化,保证数据传输的安全与高效。</li></ul>
<p class="maodian"><a name="_label4"></a></p><h2>项目技术</h2>
<p>本项目从前端到后端完整地构建了一个基于 WinForm 和 gRPC 的大文件传输系统。</p>
<p>以下是关键技术栈和实现要点:</p>
<p class="maodian"><a name="_lab2_4_2"></a></p><h3>前端技术</h3>
<p>使用&nbsp;<strong>WinForm (.NET 8)</strong>&nbsp;开发图形用户界面;</p>
<p>支持多线程处理上传任务,避免界面卡顿;</p>
<p>集成进度条控件和日志输出模块,提升交互体验。</p>
<p class="maodian"><a name="_lab2_4_3"></a></p><h3>后端通信</h3>
<p>基于&nbsp;<strong>ASP.NET Core gRPC (.NET 8)</strong>&nbsp;构建服务端接口;</p>
<p>定义&nbsp;<code>.proto</code>&nbsp;文件描述文件上传的数据结构和服务方法;</p>
<p>利用&nbsp;<strong>gRPC 的双向流特性</strong>&nbsp;实现大文件的分块上传和实时响应。</p>
<p class="maodian"><a name="_lab2_4_4"></a></p><h3>数据处理</h3>
<p>使用&nbsp;<strong>Google.Protobuf</strong>&nbsp;库完成 Protobuf 数据的序列化与反序列化;</p>
<p>文件分块上传过程中,每一块都携带偏移量和标识符,用于服务器端拼接和断点恢复;</p>
<p>使用&nbsp;<strong>MD5 / SHA1</strong>&nbsp;对原始文件与接收后的文件进行哈希比对,确保一致性。</p>
<p class="maodian"><a name="_lab2_4_5"></a></p><h3>本地存储</h3>
<p>使用&nbsp;<strong>SQLite</strong>&nbsp;数据库存储每个上传任务的状态信息,包括已上传大小、文件路径、服务器地址等;</p>
<p>在应用重启或网络中断后,读取本地记录恢复上传上下文。</p>
<p class="maodian"><a name="_lab2_4_6"></a></p><h3>NuGet 包依赖</h3>
<ul><li><code>Grpc.Net.Client</code>:用于构建 gRPC 客户端连接;</li><li><code>Google.Protobuf</code>:提供 Protobuf 数据模型支持;</li></ul>
<p><code>Grpc.Tools</code>:编译&nbsp;<code>.proto</code>&nbsp;文件生成 C# 代码。</p>
<p>安装命令如下:</p>
<div class="jb51code"><pre class="brush:bash;">Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools
</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>项目代码</h2>
<div class="jb51code"><pre class="brush:csharp;">/// &lt;summary&gt;
/// 初始化数据库表 UploadSessions,用于记录上传会话信息。
/// 如果表不存在,则创建该表。
/// &lt;/summary&gt;
private void InitializeDatabase()
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
      CREATE TABLE IF NOT EXISTS UploadSessions (
            SessionId TEXT PRIMARY KEY,       -- 会话唯一标识符(GUID)
            FileName TEXT NOT NULL,         -- 文件名
            FileSize INTEGER NOT NULL,      -- 文件总大小(字节)
            FileHash TEXT NOT NULL,         -- 文件哈希值(用于断点续传校验)
            UploadedBytes INTEGER NOT NULL,   -- 已上传字节数(初始为0)
            TempFilePath TEXT NOT NULL,       -- 临时文件路径
            CreatedAt TEXT NOT NULL,          -- 创建时间(UTC格式字符串)
            CompletedAt TEXT                  -- 完成时间(可为空)
      )";
    command.ExecuteNonQuery();
}

/// &lt;summary&gt;
/// 创建一个新的上传会话,并插入数据库中。
/// &lt;/summary&gt;
/// &lt;param name="fileName"&gt;上传文件的原始名称&lt;/param&gt;
/// &lt;param name="fileSize"&gt;文件总大小&lt;/param&gt;
/// &lt;param name="fileHash"&gt;文件的哈希值,用于校验完整性&lt;/param&gt;
/// &lt;returns&gt;生成的会话ID&lt;/returns&gt;
public string CreateSession(string fileName, long fileSize, string fileHash)
{
    var sessionId = Guid.NewGuid().ToString(); // 生成唯一会话ID
    var tempFilePath = Path.Combine(_tempStoragePath, $"temp_{sessionId}_{Path.GetFileName(fileName)}");

    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
      INSERT INTO UploadSessions
      (SessionId, FileName, FileSize, FileHash, UploadedBytes, TempFilePath, CreatedAt)
      VALUES
      (@SessionId, @FileName, @FileSize, @FileHash, 0, @TempFilePath, @CreatedAt)";

    command.Parameters.AddWithValue("@SessionId", sessionId);
    command.Parameters.AddWithValue("@FileName", fileName);
    command.Parameters.AddWithValue("@FileSize", fileSize);
    command.Parameters.AddWithValue("@FileHash", fileHash);
    command.Parameters.AddWithValue("@TempFilePath", tempFilePath);
    command.Parameters.AddWithValue("@CreatedAt", DateTime.UtcNow.ToString("o")); // ISO8601 格式时间

    command.ExecuteNonQuery();

    return sessionId;
}

/// &lt;summary&gt;
/// 根据会话ID获取上传会话的信息。
/// &lt;/summary&gt;
/// &lt;param name="sessionId"&gt;会话ID&lt;/param&gt;
/// &lt;returns&gt;UploadSession 对象,若未找到则返回 null&lt;/returns&gt;
public UploadSession GetSession(string sessionId)
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = "SELECT * FROM UploadSessions WHERE SessionId = @SessionId";
    command.Parameters.AddWithValue("@SessionId", sessionId);

    using var reader = command.ExecuteReader();
    if (reader.Read())
    {
      return new UploadSession
      {
            SessionId = reader.GetString(0),
            FileName = reader.GetString(1),
            FileSize = reader.GetInt64(2),
            FileHash = reader.GetString(3),
            UploadedBytes = reader.GetInt64(4),
            TempFilePath = reader.GetString(5),
            CreatedAt = DateTime.Parse(reader.GetString(6)),
            CompletedAt = reader.IsDBNull(7) ? null : DateTime.Parse(reader.GetString(7))
      };
    }

    return null;
}

/// &lt;summary&gt;
/// 根据文件名和哈希查找最近的一次上传会话。
/// 主要用于断点续传时查找已有会话。
/// &lt;/summary&gt;
/// &lt;param name="fileName"&gt;文件名&lt;/param&gt;
/// &lt;param name="fileHash"&gt;文件哈希值&lt;/param&gt;
/// &lt;returns&gt;最近一次的 UploadSession 对象,若未找到则返回 null&lt;/returns&gt;
public UploadSession FindSession(string fileName, string fileHash)
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
      SELECT * FROM UploadSessions
      WHERE FileName = @FileName AND FileHash = @FileHash
      ORDER BY CreatedAt DESC
      LIMIT 1";

    command.Parameters.AddWithValue("@FileName", fileName);
    command.Parameters.AddWithValue("@FileHash", fileHash);

    using var reader = command.ExecuteReader();
    if (reader.Read())
    {
      return new UploadSession
      {
            SessionId = reader.GetString(0),
            FileName = reader.GetString(1),
            FileSize = reader.GetInt64(2),
            FileHash = reader.GetString(3),
            UploadedBytes = reader.GetInt64(4),
            TempFilePath = reader.GetString(5),
            CreatedAt = DateTime.Parse(reader.GetString(6)),
            CompletedAt = reader.IsDBNull(7) ? null : DateTime.Parse(reader.GetString(7))
      };
    }

    return null;
}

/// &lt;summary&gt;
/// 更新指定会话的已上传字节数。
/// &lt;/summary&gt;
/// &lt;param name="sessionId"&gt;会话ID&lt;/param&gt;
/// &lt;param name="uploadedBytes"&gt;当前已上传字节数&lt;/param&gt;
public void UpdateSessionProgress(string sessionId, long uploadedBytes)
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
      UPDATE UploadSessions
      SET UploadedBytes = @UploadedBytes
      WHERE SessionId = @SessionId";

    command.Parameters.AddWithValue("@SessionId", sessionId);
    command.Parameters.AddWithValue("@UploadedBytes", uploadedBytes);

    command.ExecuteNonQuery();
}

/// &lt;summary&gt;
/// 获取指定会话的已上传字节数。
/// &lt;/summary&gt;
/// &lt;param name="sessionId"&gt;会话ID&lt;/param&gt;
/// &lt;returns&gt;已上传字节数&lt;/returns&gt;
public long GetUploadedBytes(string sessionId)
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = "SELECT UploadedBytes FROM UploadSessions WHERE SessionId = @SessionId";
    command.Parameters.AddWithValue("@SessionId", sessionId);

    var result = command.ExecuteScalar();
    return result != null ? Convert.ToInt64(result) : 0;
}

/// &lt;summary&gt;
/// 将指定会话标记为已完成。
/// &lt;/summary&gt;
/// &lt;param name="sessionId"&gt;会话ID&lt;/param&gt;
public void CompleteSession(string sessionId)
{
    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
      UPDATE UploadSessions
      SET CompletedAt = @CompletedAt
      WHERE SessionId = @SessionId";

    command.Parameters.AddWithValue("@SessionId", sessionId);
    command.Parameters.AddWithValue("@CompletedAt", DateTime.UtcNow.ToString("o"));

    command.ExecuteNonQuery();
}

/// &lt;summary&gt;
/// 终止指定会话并删除临时文件及数据库记录。
/// &lt;/summary&gt;
/// &lt;param name="sessionId"&gt;会话ID&lt;/param&gt;
public void AbortSession(string sessionId)
{
    var session = GetSession(sessionId);
    if (session != null)
    {
      try
      {
            if (File.Exists(session.TempFilePath))
            {
                File.Delete(session.TempFilePath); // 删除临时文件
            }
      }
      catch
      {
            // 可选:记录日志或处理异常
      }

      using var connection = new SqliteConnection(_connectionString);
      connection.Open();

      var command = connection.CreateCommand();
      command.CommandText = "DELETE FROM UploadSessions WHERE SessionId = @SessionId";
      command.Parameters.AddWithValue("@SessionId", sessionId);
      command.ExecuteNonQuery();
    }
}

/// &lt;summary&gt;
/// 清理过期的上传会话(未完成且超过指定时间)。
/// 同时删除对应的临时文件和数据库记录。
/// &lt;/summary&gt;
/// &lt;param name="expirationTime"&gt;会话的过期时间跨度&lt;/param&gt;
public void CleanupExpiredSessions(TimeSpan expirationTime)
{
    var cutoff = DateTime.UtcNow - expirationTime;

    using var connection = new SqliteConnection(_connectionString);
    connection.Open();

    // 首先查询所有过期会话
    var selectCommand = connection.CreateCommand();
    selectCommand.CommandText = @"
      SELECT SessionId, TempFilePath FROM UploadSessions
      WHERE CreatedAt &lt; @Cutoff AND (CompletedAt IS NULL OR CompletedAt &lt; @Cutoff)";
    selectCommand.Parameters.AddWithValue("@Cutoff", cutoff.ToString("o"));

    var sessionsToDelete = new List&lt;(string SessionId, string TempFilePath)&gt;();

    using (var reader = selectCommand.ExecuteReader())
    {
      while (reader.Read())
      {
            sessionsToDelete.Add((reader.GetString(0), reader.GetString(1)));
      }
    }

    // 然后依次删除临时文件和数据库记录
    foreach (var (sessionId, tempFilePath) in sessionsToDelete)
    {
      try
      {
            if (File.Exists(tempFilePath))
            {
                File.Delete(tempFilePath);
            }
      }
      catch
      {
            // 可选:记录日志或处理异常
      }

      var deleteCommand = connection.CreateCommand();
      deleteCommand.CommandText = "DELETE FROM UploadSessions WHERE SessionId = @SessionId";
      deleteCommand.Parameters.AddWithValue("@SessionId", sessionId);
      deleteCommand.ExecuteNonQuery();
    }
}
</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>项目效果</h2>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025623104953978.png" /></p>
頁: [1]
查看完整版本: .NET8 gRPC实现高效100G大文件断点续传工具