发布于2026-07-23 阅读(0)
扫一扫,手机访问
数字化和信息化一路高歌猛进,大文件传输这件事,在企业、科研圈乃至个人用户那里,早已不是什么新鲜话题。几十GB甚至上百GB的视频素材、工程数据,说实话,传统的传输方式碰上它们,常常让人头疼——网络一抖,程序一崩,就得从头再来,时间和带宽就这么白白耗掉了。
那有没有什么办法能“断了接着传”?当然有。这里要聊的,就是一个基于WinForm和.NET gRPC技术实现的大文件断点续传工具。它不仅能扛住最大100GB的文件传输,更关键的是,网络断了之后,它能从断点处继续,而不是让你重头来过。效率和稳定性,都能上一个台阶。
简单来说,这是个面向桌面端用户的大文件断点续传工具。前端界面用的是WinForm,后端服务通信则交给了ASP.NET Core gRPC。
它的核心目标很明确:提供一种轻量、可靠、且易于扩展的文件传输方案。适合谁用?那些需要频繁折腾大文件上传的企业或开发者。值得一提的是,整个项目不依赖复杂的第三方组件,完全在.NET生态里打转,跨平台潜力和可维护性都还不错。
从前端到后端,这个项目完整地构建了一套基于WinForm和gRPC的大文件传输系统。这里列一下关键技术栈和实现要点。
使用WinForm (.NET 8)开发图形用户界面;支持多线程处理上传任务,界面不会卡顿;集成了进度条控件和日志输出模块,交互体验更友好。
基于ASP.NET Core gRPC (.NET 8)构建服务端接口;通过.proto文件定义文件上传的数据结构和服务方法;利用gRPC的双向流特性,实现大文件的分块上传和实时响应。
使用Google.Protobuf库完成Protobuf数据的序列化与反序列化;文件分块上传时,每块都携带偏移量和标识符,服务器端据此拼接和恢复断点;最后用MD5 / SHA1做哈希比对,确保文件一致性。
使用SQLite数据库存储每个上传任务的状态信息,包括已上传大小、文件路径、服务器地址等;应用重启或网络中断后,读取本地记录,恢复上传上下文。
Grpc.Net.Client:构建gRPC客户端连接。Google.Protobuf:提供Protobuf数据模型支持。Grpc.Tools:编译.proto文件生成C#代码。
安装命令如下:
Install-Package Grpc.Net.Client Install-Package Google.Protobuf Install-Package Grpc.Tools
////// 初始化数据库表 UploadSessions,用于记录上传会话信息。 /// 如果表不存在,则创建该表。 /// 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(); } ////// 创建一个新的上传会话,并插入数据库中。 /// /// 上传文件的原始名称 /// 文件总大小 /// 文件的哈希值,用于校验完整性 ///生成的会话ID 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; } ////// 根据会话ID获取上传会话的信息。 /// /// 会话ID ///UploadSession 对象,若未找到则返回 null 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; } ////// 根据文件名和哈希查找最近的一次上传会话。 /// 主要用于断点续传时查找已有会话。 /// /// 文件名 /// 文件哈希值 ///最近一次的 UploadSession 对象,若未找到则返回 null 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; } ////// 更新指定会话的已上传字节数。 /// /// 会话ID /// 当前已上传字节数 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(); } ////// 获取指定会话的已上传字节数。 /// /// 会话ID ///已上传字节数 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; } ////// 将指定会话标记为已完成。 /// /// 会话ID 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(); } ////// 终止指定会话并删除临时文件及数据库记录。 /// /// 会话ID 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(); } } ////// 清理过期的上传会话(未完成且超过指定时间)。 /// 同时删除对应的临时文件和数据库记录。 /// /// 会话的过期时间跨度 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(); } }

售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8