.NET8 gRPC實(shí)現(xiàn)高效100G大文件斷點(diǎn)續(xù)傳工具
前言
隨著數(shù)字化和信息化的發(fā)展,大文件傳輸在企業(yè)、科研以及個(gè)人用戶(hù)中變得越來(lái)越常見(jiàn)。傳統(tǒng)的文件傳輸方式在面對(duì)大文件(如幾十GB甚至上百GB的視頻、工程數(shù)據(jù))時(shí),常常因網(wǎng)絡(luò)不穩(wěn)定、程序崩潰等原因?qū)е聜鬏斒?,而重新上傳又浪費(fèi)大量時(shí)間和帶寬資源。
為了解決這一問(wèn)題,本文推薦一個(gè)基于WinForm 和 .NET gRPC 技術(shù)實(shí)現(xiàn)的大文件斷點(diǎn)續(xù)傳工具。該工具不僅支持最大100GB文件的高效傳輸,還具備在網(wǎng)絡(luò)中斷后從中斷點(diǎn)繼續(xù)傳輸?shù)哪芰?,大大提高了傳輸效率與穩(wěn)定性。
項(xiàng)目介紹
項(xiàng)目是一個(gè)面向桌面端用戶(hù)的 大文件斷點(diǎn)續(xù)傳工具,采用 WinForm 構(gòu)建前端界面,使用 ASP.NET Core gRPC 實(shí)現(xiàn)后端服務(wù)通信。
其核心目標(biāo)是提供一種輕量級(jí)、可靠且易于擴(kuò)展的文件傳輸解決方案,適用于需要頻繁進(jìn)行大文件上傳的企業(yè)或開(kāi)發(fā)人員。
該項(xiàng)目不依賴(lài)復(fù)雜的第三方組件,完全基于.NET 生態(tài)構(gòu)建,具有良好的跨平臺(tái)潛力和可維護(hù)性。
項(xiàng)目功能
核心功能
- 大文件支持:支持最大100GB的單個(gè)文件上傳。
- 斷點(diǎn)續(xù)傳機(jī)制:在網(wǎng)絡(luò)中斷或客戶(hù)端異常退出后,能夠從中斷位置繼續(xù)上傳。
- 分塊傳輸策略:將大文件切分為多個(gè)小塊進(jìn)行傳輸,提升傳輸穩(wěn)定性和并發(fā)處理能力。
- 實(shí)時(shí)進(jìn)度顯示:在界面上動(dòng)態(tài)展示當(dāng)前上傳進(jìn)度、速度及剩余時(shí)間。
- 傳輸管理控制:支持暫停、繼續(xù)、取消等操作,增強(qiáng)用戶(hù)體驗(yàn)。
附加功能
- 文件校驗(yàn)機(jī)制:通過(guò)MD5或SHA1算法驗(yàn)證上傳前后文件的一致性,確保數(shù)據(jù)完整性。
- 傳輸日志記錄:自動(dòng)記錄每次上傳的日志信息,便于追蹤和故障排查。
- 本地狀態(tài)持久化:使用SQLite數(shù)據(jù)庫(kù)保存?zhèn)鬏敔顟B(tài),保障斷點(diǎn)信息不丟失。
項(xiàng)目特點(diǎn)
- 技術(shù)先進(jìn):采用最新的 .NET 8 框架,結(jié)合 gRPC 協(xié)議,實(shí)現(xiàn)了高性能的遠(yuǎn)程調(diào)用和流式傳輸。
- 架構(gòu)清晰:前后端分離設(shè)計(jì),前端負(fù)責(zé)交互,后端專(zhuān)注業(yè)務(wù)邏輯與數(shù)據(jù)傳輸,便于后期擴(kuò)展。
- 協(xié)議高效:基于 HTTP/2 的 gRPC 協(xié)議,具備低延遲、高吞吐量的優(yōu)勢(shì),非常適合大文件流式上傳。
- 本地狀態(tài)管理:使用 SQLite 存儲(chǔ)上傳狀態(tài),實(shí)現(xiàn)斷點(diǎn)信息的持久化。
- 序列化統(tǒng)一:采用 Protocol Buffers (Protobuf) 進(jìn)行數(shù)據(jù)結(jié)構(gòu)定義和序列化,保證數(shù)據(jù)傳輸?shù)陌踩c高效。
項(xiàng)目技術(shù)
本項(xiàng)目從前端到后端完整地構(gòu)建了一個(gè)基于 WinForm 和 gRPC 的大文件傳輸系統(tǒng)。
以下是關(guān)鍵技術(shù)棧和實(shí)現(xiàn)要點(diǎn):
前端技術(shù)
使用 WinForm (.NET 8) 開(kāi)發(fā)圖形用戶(hù)界面;
支持多線(xiàn)程處理上傳任務(wù),避免界面卡頓;
集成進(jìn)度條控件和日志輸出模塊,提升交互體驗(yàn)。
后端通信
基于 ASP.NET Core gRPC (.NET 8) 構(gòu)建服務(wù)端接口;
定義 .proto 文件描述文件上傳的數(shù)據(jù)結(jié)構(gòu)和服務(wù)方法;
利用 gRPC 的雙向流特性 實(shí)現(xiàn)大文件的分塊上傳和實(shí)時(shí)響應(yīng)。
數(shù)據(jù)處理
使用 Google.Protobuf 庫(kù)完成 Protobuf 數(shù)據(jù)的序列化與反序列化;
文件分塊上傳過(guò)程中,每一塊都攜帶偏移量和標(biāo)識(shí)符,用于服務(wù)器端拼接和斷點(diǎn)恢復(fù);
使用 MD5 / SHA1 對(duì)原始文件與接收后的文件進(jìn)行哈希比對(duì),確保一致性。
本地存儲(chǔ)
使用 SQLite 數(shù)據(jù)庫(kù)存儲(chǔ)每個(gè)上傳任務(wù)的狀態(tài)信息,包括已上傳大小、文件路徑、服務(wù)器地址等;
在應(yīng)用重啟或網(wǎng)絡(luò)中斷后,讀取本地記錄恢復(fù)上傳上下文。
NuGet 包依賴(lài)
Grpc.Net.Client:用于構(gòu)建 gRPC 客戶(hù)端連接;Google.Protobuf:提供 Protobuf 數(shù)據(jù)模型支持;
Grpc.Tools:編譯 .proto 文件生成 C# 代碼。
安裝命令如下:
Install-Package Grpc.Net.Client Install-Package Google.Protobuf Install-Package Grpc.Tools
項(xiàng)目代碼
/// <summary>
/// 初始化數(shù)據(jù)庫(kù)表 UploadSessions,用于記錄上傳會(huì)話(huà)信息。
/// 如果表不存在,則創(chuàng)建該表。
/// </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, -- 會(huì)話(huà)唯一標(biāo)識(shí)符(GUID)
FileName TEXT NOT NULL, -- 文件名
FileSize INTEGER NOT NULL, -- 文件總大?。ㄗ止?jié))
FileHash TEXT NOT NULL, -- 文件哈希值(用于斷點(diǎn)續(xù)傳校驗(yàn))
UploadedBytes INTEGER NOT NULL, -- 已上傳字節(jié)數(shù)(初始為0)
TempFilePath TEXT NOT NULL, -- 臨時(shí)文件路徑
CreatedAt TEXT NOT NULL, -- 創(chuàng)建時(shí)間(UTC格式字符串)
CompletedAt TEXT -- 完成時(shí)間(可為空)
)";
command.ExecuteNonQuery();
}
/// <summary>
/// 創(chuàng)建一個(gè)新的上傳會(huì)話(huà),并插入數(shù)據(jù)庫(kù)中。
/// </summary>
/// <param name="fileName">上傳文件的原始名稱(chēng)</param>
/// <param name="fileSize">文件總大小</param>
/// <param name="fileHash">文件的哈希值,用于校驗(yàn)完整性</param>
/// <returns>生成的會(huì)話(huà)ID</returns>
public string CreateSession(string fileName, long fileSize, string fileHash)
{
var sessionId = Guid.NewGuid().ToString(); // 生成唯一會(huì)話(huà)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 格式時(shí)間
command.ExecuteNonQuery();
return sessionId;
}
/// <summary>
/// 根據(jù)會(huì)話(huà)ID獲取上傳會(huì)話(huà)的信息。
/// </summary>
/// <param name="sessionId">會(huì)話(huà)ID</param>
/// <returns>UploadSession 對(duì)象,若未找到則返回 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>
/// 根據(jù)文件名和哈希查找最近的一次上傳會(huì)話(huà)。
/// 主要用于斷點(diǎn)續(xù)傳時(shí)查找已有會(huì)話(huà)。
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="fileHash">文件哈希值</param>
/// <returns>最近一次的 UploadSession 對(duì)象,若未找到則返回 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>
/// 更新指定會(huì)話(huà)的已上傳字節(jié)數(shù)。
/// </summary>
/// <param name="sessionId">會(huì)話(huà)ID</param>
/// <param name="uploadedBytes">當(dāng)前已上傳字節(jié)數(shù)</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>
/// 獲取指定會(huì)話(huà)的已上傳字節(jié)數(shù)。
/// </summary>
/// <param name="sessionId">會(huì)話(huà)ID</param>
/// <returns>已上傳字節(jié)數(shù)</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>
/// 將指定會(huì)話(huà)標(biāo)記為已完成。
/// </summary>
/// <param name="sessionId">會(huì)話(huà)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>
/// 終止指定會(huì)話(huà)并刪除臨時(shí)文件及數(shù)據(jù)庫(kù)記錄。
/// </summary>
/// <param name="sessionId">會(huì)話(huà)ID</param>
public void AbortSession(string sessionId)
{
var session = GetSession(sessionId);
if (session != null)
{
try
{
if (File.Exists(session.TempFilePath))
{
File.Delete(session.TempFilePath); // 刪除臨時(shí)文件
}
}
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>
/// 清理過(guò)期的上傳會(huì)話(huà)(未完成且超過(guò)指定時(shí)間)。
/// 同時(shí)刪除對(duì)應(yīng)的臨時(shí)文件和數(shù)據(jù)庫(kù)記錄。
/// </summary>
/// <param name="expirationTime">會(huì)話(huà)的過(guò)期時(shí)間跨度</param>
public void CleanupExpiredSessions(TimeSpan expirationTime)
{
var cutoff = DateTime.UtcNow - expirationTime;
using var connection = new SqliteConnection(_connectionString);
connection.Open();
// 首先查詢(xún)所有過(guò)期會(huì)話(huà)
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)));
}
}
// 然后依次刪除臨時(shí)文件和數(shù)據(jù)庫(kù)記錄
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();
}
}
項(xiàng)目效果

到此這篇關(guān)于.NET8 gRPC實(shí)現(xiàn)高效100G大文件斷點(diǎn)續(xù)傳工具 的文章就介紹到這了,更多相關(guān).NET大文件斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vs2010制作簡(jiǎn)單的asp.net網(wǎng)站
這篇文章主要介紹了vs2010制作簡(jiǎn)單的asp.net網(wǎng)站,只要十步哦,感興趣的小伙伴們可以參考一下2015-09-09
asp.net生成靜態(tài)后冗余代碼,去掉viewstate生成的代碼
asp.net生成的viewstate垃圾信息超過(guò)了20K,如果頁(yè)面前二K不是內(nèi)容會(huì)被引擎處罰,所以我們需要清理下asp.net生成靜態(tài)后冗余代碼2012-10-10
asp.net 通用分頁(yè)顯示輔助類(lèi)(改進(jìn)版)
在使用ASP.NET編程時(shí),如果不用控件的默認(rèn)分頁(yè)功能,想自己搞一個(gè),可以看看本文的asp.net通用分頁(yè)顯示輔助類(lèi)哦。2010-04-04
ASP.NET打開(kāi)新頁(yè)面而不關(guān)閉原來(lái)的頁(yè)面 實(shí)例代碼
這篇文章介紹了ASP.NET打開(kāi)新頁(yè)面而不關(guān)閉原來(lái)的頁(yè)面 實(shí)例代碼,有需要的朋友可以參考一下,希望對(duì)你有所幫助2013-07-07
Linkbutton控件在項(xiàng)目中的簡(jiǎn)單應(yīng)用
Button控件可分為button控件、LinkButton控件、ImageButton控件三類(lèi),而LinkButton控件則在頁(yè)面上顯示為一個(gè)超級(jí)鏈接,下面與大家分享下其具體應(yīng)用2013-05-05
VB.NET驗(yàn)證郵件地址的合法性實(shí)現(xiàn)代碼
現(xiàn)在,對(duì)于用戶(hù)在Web頁(yè)面上或電話(huà)中給出的Email地址,我們?cè)絹?lái)越不敢肯定它是否真的有效。在今天這個(gè)垃圾郵件泛濫成災(zāi)的年代,人們完全有理由舍不得輕易透露Email地址。2011-04-04
asp.net 頁(yè)面版文本框智能提示JSCode (升級(jí)版)
模擬百度,Google智能提示,非與服務(wù)器端交互的,數(shù)據(jù)源來(lái)自已經(jīng)綁定好的下拉列表。純客戶(hù)端腳本 升級(jí)版2009-12-12
ASP.NET Core對(duì)不同類(lèi)型的用戶(hù)進(jìn)行區(qū)別限流詳解
這篇文章主要介紹了ASP.NET Core對(duì)不同類(lèi)型的用戶(hù)進(jìn)行區(qū)別限流的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02

