.NET 8 gRPC 实现高效100G大文件断点续传工具
前言
随着数字化和信息化的发展,大文件传输在企业、科研以及个人用户中变得越来越常见。传统的文件传输方式在面对大文件(如几十GB甚至上百GB的视频、工程数据)时,常常因网络不稳定、程序崩溃等原因导致传输失败,而重新上传又浪费大量时间和带宽资源。
为了解决这一问题,本文推荐一个基于WinForm 和 .NET gRPC 技术实现的大文件断点续传工具。该工具不仅支持最大100GB文件的高效传输,还具备在网络中断后从中断点继续传输的能力,大大提高了传输效率与稳定性。
项目介绍
项目是一个面向桌面端用户的 大文件断点续传工具,采用 WinForm 构建前端界面,使用 ASP.NET Core gRPC 实现后端服务通信。
其核心目标是提供一种轻量级、可靠且易于扩展的文件传输解决方案,适用于需要频繁进行大文件上传的企业或开发人员。
该项目不依赖复杂的第三方组件,完全基于.NET 生态构建,具有良好的跨平台潜力和可维护性。
项目功能
核心功能
大文件支持:支持最大100GB的单个文件上传。
断点续传机制:在网络中断或客户端异常退出后,能够从中断位置继续上传。
分块传输策略:将大文件切分为多个小块进行传输,提升传输稳定性和并发处理能力。
实时进度显示:在界面上动态展示当前上传进度、速度及剩余时间。
传输管理控制:支持暂停、继续、取消等操作,增强用户体验。
附加功能:
文件校验机制:通过MD5或SHA1算法验证上传前后文件的一致性,确保数据完整性。
传输日志记录:自动记录每次上传的日志信息,便于追踪和故障排查。
本地状态持久化:使用SQLite数据库保存传输状态,保障断点信息不丢失。
项目特点
技术先进:采用最新的 .NET 8 框架,结合 gRPC 协议,实现了高性能的远程调用和流式传输。
架构清晰:前后端分离设计,前端负责交互,后端专注业务逻辑与数据传输,便于后期扩展。
协议高效:基于 HTTP/2 的 gRPC 协议,具备低延迟、高吞吐量的优势,非常适合大文件流式上传。
本地状态管理:使用 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();
}
}
项目效果

项目源码
Gitee:https://gitee.com/sujimin/gRPC
总结
以上仅展示了大文件断点续传工具的部分功能。更多实用特性和详细信息,请大家访问项目源码。
希望通过本文能为.NET 在大文件断点续传工具开发方面提供有价值的参考。感谢您阅读本篇文章,欢迎在评论区留言交流,分享您的宝贵经验和建议。
关键词:WinForm、gRPC、大文件传输、断点续传、分块上传、Protocol Buffers、SQLite、ASP.NET Core、MD5校验、SHA1校验、.NET 8、文件完整性验证、传输日志、开源项目
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

.NET 8 gRPC 实现高效100G大文件断点续传工具的更多相关文章
- iOS开发之网络编程--使用NSURLConnection实现大文件断点续传下载+使用输出流代替文件句柄
前言:本篇讲解,在前篇iOS开发之网络编程--使用NSURLConnection实现大文件断点续传下载的基础上,使用输出流代替文件句柄实现大文件断点续传. 在实际开发中,输入输出流用的比较少,但 ...
- Java:大文件拆分工具
java大文件拆分工具(过滤掉表头) import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File ...
- php实现大文件断点续传下载实例
php实现大文件断点续传下载实例,看完你就知道超过100M以上的大文件如何断点传输了,这个功能还是比较经典实用的,毕竟大文件上传功能经常用得到. require_once('download.clas ...
- HTML5 大文件断点续传完整思路整理
需求: 支持大文件批量上传(20G)和下载,同时需要保证上传期间用户电脑不出现卡死等体验: 内网百兆网络上传速度为12MB/S 服务器内存占用低 支持文件夹上传,文件夹中的文件数量达到1万个以上,且包 ...
- B/S大文件断点续传
一. 功能性需求与非功能性需求 要求操作便利,一次选择多个文件和文件夹进行上传:支持PC端全平台操作系统,Windows,Linux,Mac 支持文件和文件夹的批量下载,断点续传.刷新页面后继续传输. ...
- Java高效读取大文件
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung (http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 ...
- Java高效读取大文件(转)
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung(http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 读 ...
- Python:高效计算大文件中的最长行的长度
在操作某个很多进程都要频繁用到的大文件的时候,应该尽早释放文件资源(f.close()) 前2种方法主要用到了列表解析,性能稍差,而最后一种使用的时候生成器表达式,相比列表解析,更省内存 列表解析和生 ...
- 高效读取大文件,再也不用担心 OOM 了!
内存读取 第一个版本,采用内存读取的方式,所有的数据首先读读取到内存中,程序代码如下: Stopwatch stopwatch = Stopwatch.createStarted(); // 将全部行 ...
- js之大文件断点续传
文件夹上传:从前端到后端 文件上传是 Web 开发肯定会碰到的问题,而文件夹上传则更加难缠.网上关于文件夹上传的资料多集中在前端,缺少对于后端的关注,然后讲某个后端框架文件上传的文章又不会涉及文件夹. ...
随机推荐
- verilog实现32位有符号流水乘法器
verilog实现32位有符号流水乘法器 1.4bit乘法流程 1.无符号X无符号二进制乘法器 以下为4bit乘法器流程(2X6) 0 0 0 0 0 0 1 0 (2) X 0 0 0 0 0 1 ...
- c数组与结构体
数组,存储同类型的复合类型:结构体,存储不同类型的复合类型,用于自定义数据结构. 计算机中,针对存储大量数据的集合,有着两种方式,一种是以块式集中存储数据,这就是数组的存储方式,大量同类型的数据集中放 ...
- Window7搭建Kafka环境总结
1.安装zooeleeper 下载链接:http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.14/ 安装步骤如下: 1)解压zookeepe ...
- 搜索算法1——聊聊dfs与回溯
搜索算法1--聊聊dfs与回溯 目录 1.dfs 的概念 $\ \ \ $1.1 dfs 的概念 2.dfs 的做法 $\ \ \ $2.1 为什么要用 dfs $\ \ \ $2.2 dfs 如何实 ...
- java基础之成员变量和局部变量区别
1:在类中的位置不同 成员变量:类中,方法外 局部变量:方法中,或者方法声明上(形参) 2:作用范围不一样 成员变量:类中 局部变量:方法中 3:初始化值的不同 成员变量:有默认值 局部变量:没有默认 ...
- python,爬取小说网站小说内容,同时每一章存在不同的txt文件中
思路,第一步小说介绍页获取章节地址,第二部访问具体章节,获取章节内容 具体如下:先获取下图章节地址 def stepa(value,headers): lit=[] response = reques ...
- Sentinel源码—6.熔断降级和数据统计的实现
大纲 1.DegradeSlot实现熔断降级的原理与源码 2.Sentinel数据指标统计的滑动窗口算法 1.DegradeSlot实现熔断降级的原理与源码 (1)熔断降级规则DegradeRule的 ...
- Vue3+Ant-design项目启用ts/typescript
Ant-design官方文档提供了js和ts两种案例,按照文档给项目install ant-design后写了个组件编译时发现只要加上`<script lang="ts"&g ...
- DevEco Studio AI辅助开发工具两大升级功能 鸿蒙应用开发效率再提升
随着搭载HarmonyOS 5的Pura X发布,鸿蒙生态进入快车道,各应用正在加速适配开发,越来越多开发者加入到鸿蒙应用开发浪潮中.为提升鸿蒙应用开发效率,华为前不久上线了首款开发HarmonyOS ...
- [Ubuntu 20.04] 修复‘systemd-shutdown[1]: waiting for process: crond’需等待1分半钟的问题
由于在2020-2021年期间下载过Linux版本的Free Download Manager(简称FDM,一款免费但不开源的跨平台下载工具),而该软件的官网被挂了木马,因此在此期间下载安装过FDM的 ...