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

前言

随着数字化和信息化的发展,大文件传输在企业、科研以及个人用户中变得越来越常见。传统的文件传输方式在面对大文件(如几十GB甚至上百GB的视频、工程数据)时,常常因网络不稳定、程序崩溃等原因导致传输失败,而重新上传又浪费大量时间和带宽资源。

为了解决这一问题,本文推荐一个基于WinForm.NET gRPC 技术实现的大文件断点续传工具。该工具不仅支持最大100GB文件的高效传输,还具备在网络中断后从中断点继续传输的能力,大大提高了传输效率与稳定性。

项目介绍

项目是一个面向桌面端用户的 大文件断点续传工具,采用 WinForm 构建前端界面,使用 ASP.NET Core gRPC 实现后端服务通信。

其核心目标是提供一种轻量级、可靠且易于扩展的文件传输解决方案,适用于需要频繁进行大文件上传的企业或开发人员。

该项目不依赖复杂的第三方组件,完全基于.NET 生态构建,具有良好的跨平台潜力和可维护性。

项目功能

核心功能

  • 大文件支持:支持最大100GB的单个文件上传。

  • 断点续传机制:在网络中断或客户端异常退出后,能够从中断位置继续上传。

  • 分块传输策略:将大文件切分为多个小块进行传输,提升传输稳定性和并发处理能力。

  • 实时进度显示:在界面上动态展示当前上传进度、速度及剩余时间。

  • 传输管理控制:支持暂停、继续、取消等操作,增强用户体验。

附加功能:

  • 文件校验机制:通过MD5或SHA1算法验证上传前后文件的一致性,确保数据完整性。

  • 传输日志记录:自动记录每次上传的日志信息,便于追踪和故障排查。

  • 本地状态持久化:使用SQLite数据库保存传输状态,保障断点信息不丢失。

项目特点

  • 技术先进:采用最新的 .NET 8 框架,结合 gRPC 协议,实现了高性能的远程调用和流式传输。

  • 架构清晰:前后端分离设计,前端负责交互,后端专注业务逻辑与数据传输,便于后期扩展。

  • 协议高效:基于 HTTP/2gRPC 协议,具备低延迟、高吞吐量的优势,非常适合大文件流式上传。

  • 本地状态管理:使用 SQLite 存储上传状态,实现断点信息的持久化。

  • 序列化统一:采用 Protocol Buffers (Protobuf) 进行数据结构定义和序列化,保证数据传输的安全与高效。

项目技术

本项目从前端到后端完整地构建了一个基于 WinForm 和 gRPC 的大文件传输系统。

以下是关键技术栈和实现要点:

前端技术

使用 WinForm (.NET 8) 开发图形用户界面;

支持多线程处理上传任务,避免界面卡顿;

集成进度条控件和日志输出模块,提升交互体验。

后端通信

基于 ASP.NET Core gRPC (.NET 8) 构建服务端接口;

定义 .proto 文件描述文件上传的数据结构和服务方法;

利用 gRPC 的双向流特性 实现大文件的分块上传和实时响应。

数据处理

使用 Google.Protobuf 库完成 Protobuf 数据的序列化与反序列化;

文件分块上传过程中,每一块都携带偏移量和标识符,用于服务器端拼接和断点恢复;

使用 MD5 / SHA1 对原始文件与接收后的文件进行哈希比对,确保一致性。

本地存储

使用 SQLite 数据库存储每个上传任务的状态信息,包括已上传大小、文件路径、服务器地址等;

在应用重启或网络中断后,读取本地记录恢复上传上下文。

NuGet 包依赖

Grpc.Net.Client:用于构建 gRPC 客户端连接;
Google.Protobuf:提供 Protobuf 数据模型支持;

Grpc.Tools:编译 .proto 文件生成 C# 代码。

安装命令如下:

Install-Package Grpc.Net.Client Install-Package Google.Protobuf Install-Package Grpc.Tools 

项目代码

/// <summary> /// 初始化数据库表 UploadSessions,用于记录上传会话信息。 /// 如果表不存在,则创建该表。 /// </summary> 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(); }  /// <summary> /// 创建一个新的上传会话,并插入数据库中。 /// </summary> /// <param name="fileName">上传文件的原始名称</param> /// <param name="fileSize">文件总大小</param> /// <param name="fileHash">文件的哈希值,用于校验完整性</param> /// <returns>生成的会话ID</returns> 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; }  /// <summary> /// 根据会话ID获取上传会话的信息。 /// </summary> /// <param name="sessionId">会话ID</param> /// <returns>UploadSession 对象,若未找到则返回 null</returns> 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; }  /// <summary> /// 根据文件名和哈希查找最近的一次上传会话。 /// 主要用于断点续传时查找已有会话。 /// </summary> /// <param name="fileName">文件名</param> /// <param name="fileHash">文件哈希值</param> /// <returns>最近一次的 UploadSession 对象,若未找到则返回 null</returns> 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; }  /// <summary> /// 更新指定会话的已上传字节数。 /// </summary> /// <param name="sessionId">会话ID</param> /// <param name="uploadedBytes">当前已上传字节数</param> 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(); }  /// <summary> /// 获取指定会话的已上传字节数。 /// </summary> /// <param name="sessionId">会话ID</param> /// <returns>已上传字节数</returns> 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; }  /// <summary> /// 将指定会话标记为已完成。 /// </summary> /// <param name="sessionId">会话ID</param> 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(); }  /// <summary> /// 终止指定会话并删除临时文件及数据库记录。 /// </summary> /// <param name="sessionId">会话ID</param> 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();     } }  /// <summary> /// 清理过期的上传会话(未完成且超过指定时间)。 /// 同时删除对应的临时文件和数据库记录。 /// </summary> /// <param name="expirationTime">会话的过期时间跨度</param> 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 < @Cutoff AND (CompletedAt IS NULL OR CompletedAt < @Cutoff)";     selectCommand.Parameters.AddWithValue("@Cutoff", cutoff.ToString("o"));      var sessionsToDelete = new List<(string SessionId, string TempFilePath)>();      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();     } } 

项目效果

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

项目源码

Gitee:https://gitee.com/sujimin/gRPC

总结

以上仅展示了大文件断点续传工具的部分功能。更多实用特性和详细信息,请大家访问项目源码。

希望通过本文能为.NET 在大文件断点续传工具开发方面提供有价值的参考。感谢您阅读本篇文章,欢迎在评论区留言交流,分享您的宝贵经验和建议。

关键词:WinForm、gRPC、大文件传输、断点续传、分块上传、Protocol Buffers、SQLite、ASP.NET Core、MD5校验、SHA1校验、.NET 8、文件完整性验证、传输日志、开源项目

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

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

发表评论

评论已关闭。

相关文章