使用C#實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)文件傳輸工具
前言:為什么需要這樣一個(gè)工具?
在企業(yè)級(jí)開發(fā)中,文件傳輸功能幾乎是必備需求。無論是內(nèi)網(wǎng)文件同步、遠(yuǎn)程數(shù)據(jù)備份,還是分布式系統(tǒng)間的文件交換,一個(gè)穩(wěn)定高效的文件傳輸工具都顯得至關(guān)重要。
今天就有位開發(fā)私信我:"老師,我需要開發(fā)一個(gè)文件傳輸工具,服務(wù)端只管接收文件并保存到指定目錄,客戶端只管發(fā)送文件。網(wǎng)上的示例要么功能復(fù)雜,要么不夠穩(wěn)定,能否提供一個(gè)完整的解決方案?"相信很多朋友都遇到過類似需求。今天我們就來徹底搞定這個(gè)問題,用 C# 打造一個(gè)功能專一、穩(wěn)定可靠的網(wǎng)絡(luò)文件傳輸工具!
很多現(xiàn)成的文件傳輸方案要么依賴第三方庫(kù),要么集成了太多非核心功能,導(dǎo)致維護(hù)困難、部署復(fù)雜。而我們追求的是最小可行、專注單一職責(zé)的設(shè)計(jì)哲學(xué)——服務(wù)端只做接收,客戶端只做發(fā)送,界面簡(jiǎn)潔直觀,異常處理完善,真正做到"開箱即用"。
需求分析與設(shè)計(jì)目標(biāo)
我們的核心目標(biāo)很明確:
1、職責(zé)清晰:服務(wù)端專門接收,客戶端專門發(fā)送
2、界面友好:實(shí)時(shí)進(jìn)度、速度顯示、狀態(tài)提示
3、異常處理:網(wǎng)絡(luò)中斷、文件沖突等場(chǎng)景的優(yōu)雅處理
4、即插即用:最小化配置,開箱即用
架構(gòu)設(shè)計(jì)
整體采用經(jīng)典的分層解耦模式,核心組件包括:
- FileTransferServer:服務(wù)端核心類,負(fù)責(zé)監(jiān)聽和接收
- FileTransferClient:客戶端核心類,負(fù)責(zé)連接和發(fā)送
- TransferEventArgs:事件參數(shù)類,統(tǒng)一狀態(tài)通知
- FrmMain:UI主窗體,用戶交互界面
架構(gòu)圖如下

服務(wù)端實(shí)現(xiàn):專注接收文件
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AppNetworkFileTransfer.Core;
using AppNetworkFileTransfer.Models;
namespace AppNetworkFileTransfer.Core
{
public class FileTransferServer
{
private TcpListener _listener;
private TcpClient _client;
private NetworkStream _stream;
private bool _isRunning = false;
private bool _isClientConnected = false;
private CancellationTokenSource _cancellationTokenSource;
private const int BufferSize = 8192;
private const int HeaderSize = 1024;
// 添加保存目錄屬性
public string SaveDirectory { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public event EventHandler<TransferEventArgs> ProgressChanged;
public event EventHandler<TransferEventArgs> StatusChanged;
public event EventHandler<TransferEventArgs> ClientConnected;
public event EventHandler<TransferEventArgs> TransferStarted;
public async Task StartListening(int port)
{
try
{
_listener = new TcpListener(IPAddress.Any, port);
_listener.Start();
_isRunning = true;
_cancellationTokenSource = new CancellationTokenSource();
OnStatusChanged($"服務(wù)器啟動(dòng),監(jiān)聽端口 {port},文件保存目錄: {SaveDirectory}");
// 在后臺(tái)異步等待客戶端連接
_ = Task.Run(async () => await WaitForClientAsync());
}
catch (Exception ex)
{
OnStatusChanged($"服務(wù)器啟動(dòng)失敗: {ex.Message}", true);
throw;
}
}
private async Task WaitForClientAsync()
{
try
{
while (_isRunning && !_cancellationTokenSource.Token.IsCancellationRequested)
{
_client = await _listener.AcceptTcpClientAsync();
_stream = _client.GetStream();
_isClientConnected = true;
OnStatusChanged($"客戶端已連接: {_client.Client.RemoteEndPoint}");
ClientConnected?.Invoke(this, new TransferEventArgs("客戶端已連接"));
// 開始處理客戶端請(qǐng)求
_ = Task.Run(async () => await HandleClientAsync());
break;
}
}
catch (ObjectDisposedException)
{
// 服務(wù)器已停止,正常情況
}
catch (Exception ex)
{
OnStatusChanged($"等待客戶端連接時(shí)出錯(cuò): {ex.Message}", true);
}
}
private async Task HandleClientAsync()
{
try
{
while (_isRunning && _isClientConnected && !_cancellationTokenSource.Token.IsCancellationRequested)
{
// 等待接收文件頭信息
var headerBuffer = new byte[HeaderSize];
var totalRead = 0;
while (totalRead < HeaderSize)
{
var bytesRead = await _stream.ReadAsync(headerBuffer, totalRead, HeaderSize - totalRead, _cancellationTokenSource.Token);
if (bytesRead == 0)
{
OnStatusChanged("客戶端斷開連接");
_isClientConnected = false;
return;
}
totalRead += bytesRead;
}
var headerInfo = ParseFileHeader(headerBuffer);
if (headerInfo.Command == "SEND")
{
await ReceiveFileFromClientAsync(headerInfo);
}
else
{
OnStatusChanged($"收到未知命令: {headerInfo.Command}", true);
// 發(fā)送錯(cuò)誤響應(yīng)
var errorBytes = Encoding.UTF8.GetBytes("ERR");
await _stream.WriteAsync(errorBytes, 0, errorBytes.Length, _cancellationTokenSource.Token);
}
}
}
catch (Exception ex)
{
OnStatusChanged($"處理客戶端請(qǐng)求時(shí)出錯(cuò): {ex.Message}", true);
_isClientConnected = false;
}
}
private async Task ReceiveFileFromClientAsync((string Command, string FileName, long FileSize, DateTime Timestamp) headerInfo)
{
try
{
// 確保保存目錄存在
if (!Directory.Exists(SaveDirectory))
{
Directory.CreateDirectory(SaveDirectory);
}
// 構(gòu)造完整的文件路徑
var localFilePath = Path.Combine(SaveDirectory, headerInfo.FileName);
// 如果文件已存在,添加時(shí)間戳后綴
if (File.Exists(localFilePath))
{
var fileInfo = new FileInfo(localFilePath);
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.Name);
var extension = fileInfo.Extension;
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
localFilePath = Path.Combine(SaveDirectory, $"{nameWithoutExt}_{timestamp}{extension}");
}
OnStatusChanged($"開始接收文件: {headerInfo.FileName} ({FormatBytes(headerInfo.FileSize)})");
// 發(fā)送確認(rèn)響應(yīng)
var confirmBytes = Encoding.UTF8.GetBytes("OK");
await _stream.WriteAsync(confirmBytes, 0, confirmBytes.Length, _cancellationTokenSource.Token);
// 在開始接收前觸發(fā)開始事件,讓UI設(shè)置開始時(shí)間
OnTransferStarted(headerInfo.FileName, headerInfo.FileSize);
// 接收文件內(nèi)容
using (var fileStream = new FileStream(localFilePath, FileMode.Create, FileAccess.Write))
{
var buffer = new byte[BufferSize];
long totalReceived = 0;
int bytesRead;
while (totalReceived < headerInfo.FileSize)
{
var remainingBytes = headerInfo.FileSize - totalReceived;
var bytesToRead = (int)Math.Min(buffer.Length, remainingBytes);
bytesRead = await _stream.ReadAsync(buffer, 0, bytesToRead, _cancellationTokenSource.Token);
if (bytesRead == 0) break;
await fileStream.WriteAsync(buffer, 0, bytesRead, _cancellationTokenSource.Token);
totalReceived += bytesRead;
OnProgressChanged(totalReceived, headerInfo.FileSize, headerInfo.FileName);
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
}
}
OnStatusChanged($"文件接收完成: {Path.GetFileName(localFilePath)} -> {localFilePath}");
}
catch (Exception ex)
{
OnStatusChanged($"接收文件失敗: {ex.Message}", true);
// 發(fā)送錯(cuò)誤響應(yīng)
try
{
var errorBytes = Encoding.UTF8.GetBytes("ERR");
await _stream.WriteAsync(errorBytes, 0, errorBytes.Length, _cancellationTokenSource.Token);
}
catch { }
}
}
public void Stop()
{
try
{
_isRunning = false;
_isClientConnected = false;
_cancellationTokenSource?.Cancel();
_stream?.Close();
_client?.Close();
_listener?.Stop();
OnStatusChanged("服務(wù)器已停止");
}
catch (Exception ex)
{
OnStatusChanged($"停止服務(wù)器時(shí)出錯(cuò): {ex.Message}", true);
}
}
private (string Command, string FileName, long FileSize, DateTime Timestamp) ParseFileHeader(byte[] header)
{
var headerString = Encoding.UTF8.GetString(header).TrimEnd('\0');
var parts = headerString.Split('|');
if (parts.Length >= 4)
{
return (
Command: parts[0],
FileName: parts[1],
FileSize: long.TryParse(parts[2], out var size) ? size : 0,
Timestamp: DateTime.TryParse(parts[3], out var time) ? time : DateTime.Now
);
}
return ("UNKNOWN", "", 0, DateTime.Now);
}
private void OnTransferStarted(string fileName, long totalBytes)
{
TransferStarted?.Invoke(this, new TransferEventArgs(0, totalBytes, fileName));
}
private void OnProgressChanged(long transferred, long total, string fileName)
{
ProgressChanged?.Invoke(this, new TransferEventArgs(transferred, total, fileName));
}
private void OnStatusChanged(string message, bool isError = false)
{
StatusChanged?.Invoke(this, new TransferEventArgs(message, isError));
}
private string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}
客戶端實(shí)現(xiàn):專注發(fā)送文件
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AppNetworkFileTransfer.Core;
using AppNetworkFileTransfer.Models;
namespace AppNetworkFileTransfer.Core
{
public class FileTransferClient
{
private TcpClient _client;
private NetworkStream _stream;
private bool _isConnected = false;
private CancellationTokenSource _cancellationTokenSource;
private const int BufferSize = 8192;
private const int HeaderSize = 1024;
public event EventHandler<TransferEventArgs> ProgressChanged;
public event EventHandler<TransferEventArgs> StatusChanged;
public event EventHandler<TransferEventArgs> TransferStarted;
public async Task ConnectAsync(string serverIP, int port)
{
try
{
_client = new TcpClient();
_cancellationTokenSource = new CancellationTokenSource();
await _client.ConnectAsync(serverIP, port);
_stream = _client.GetStream();
_isConnected = true;
OnStatusChanged($"已連接到服務(wù)器 {serverIP}:{port}");
}
catch (Exception ex)
{
OnStatusChanged($"連接服務(wù)器失敗: {ex.Message}", true);
throw;
}
}
public void Disconnect()
{
try
{
_isConnected = false;
_cancellationTokenSource?.Cancel();
_stream?.Close();
_client?.Close();
OnStatusChanged("已斷開連接");
}
catch (Exception ex)
{
OnStatusChanged($"斷開連接時(shí)出錯(cuò): {ex.Message}", true);
}
}
public async Task SendFileAsync(string localFilePath)
{
if (!_isConnected || _stream == null)
throw new InvalidOperationException("未連接到服務(wù)器");
if (!File.Exists(localFilePath))
throw new FileNotFoundException($"文件不存在: {localFilePath}");
try
{
var fileInfo = new FileInfo(localFilePath);
var fileName = fileInfo.Name;
OnStatusChanged($"開始發(fā)送文件: {fileName} ({FormatBytes(fileInfo.Length)})");
// 觸發(fā)傳輸開始事件
OnTransferStarted(fileName, fileInfo.Length);
// 發(fā)送文件頭信息
var header = CreateFileHeader("SEND", fileName, fileInfo.Length);
await _stream.WriteAsync(header, 0, header.Length, _cancellationTokenSource.Token);
// 等待服務(wù)器確認(rèn)
var response = new byte[4];
await _stream.ReadAsync(response, 0, 4, _cancellationTokenSource.Token);
var responseStr = Encoding.UTF8.GetString(response).TrimEnd('\0');
if (responseStr != "OK")
{
throw new Exception($"服務(wù)器響應(yīng)錯(cuò)誤: {responseStr}");
}
// 發(fā)送文件內(nèi)容
using (var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read))
{
var buffer = new byte[BufferSize];
long totalSent = 0;
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, _cancellationTokenSource.Token)) > 0)
{
await _stream.WriteAsync(buffer, 0, bytesRead, _cancellationTokenSource.Token);
totalSent += bytesRead;
OnProgressChanged(totalSent, fileInfo.Length, fileName);
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
}
}
OnStatusChanged($"文件發(fā)送完成: {fileName}");
}
catch (Exception ex)
{
OnStatusChanged($"發(fā)送文件失敗: {ex.Message}", true);
throw;
}
}
private byte[] CreateFileHeader(string command, string fileName, long fileSize)
{
var header = new byte[HeaderSize];
var headerString = $"{command}|{fileName}|{fileSize}|{DateTime.Now:yyyy-MM-dd HH:mm:ss}";
var headerBytes = Encoding.UTF8.GetBytes(headerString);
Array.Copy(headerBytes, 0, header, 0, Math.Min(headerBytes.Length, HeaderSize));
return header;
}
private void OnTransferStarted(string fileName, long totalBytes)
{
TransferStarted?.Invoke(this, new TransferEventArgs(0, totalBytes, fileName));
}
private void OnProgressChanged(long transferred, long total, string fileName)
{
ProgressChanged?.Invoke(this, new TransferEventArgs(transferred, total, fileName));
}
private void OnStatusChanged(string message, bool isError = false)
{
StatusChanged?.Invoke(this, new TransferEventArgs(message, isError));
}
private string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}
UI設(shè)計(jì)與常見坑點(diǎn)處理
智能模式切換邏輯:
private void cmbMode_SelectedIndexChanged(object sender, EventArgs e)
{
_isServer = cmbMode.SelectedIndex == 0;
// 動(dòng)態(tài)顯示對(duì)應(yīng)功能區(qū)域
grpServerSettings.Visible = _isServer;
grpClientSettings.Visible = !_isServer;
// 智能IP設(shè)置
if (_isServer)
txtServerIP.Text = GetLocalIPAddress();
else
txtServerIP.Text = "127.0.0.1";
UpdateTransferControls();
}
常見坑點(diǎn)提醒:
1、TimeSpan溢出問題
private void OnProgressChanged(object sender, TransferEventArgs e)
{
// 坑點(diǎn):傳輸初期速度計(jì)算可能導(dǎo)致TimeSpan溢出
var elapsed = DateTime.Now - _transferStartTime;
if (elapsed.TotalSeconds >= 1.0) // 關(guān)鍵:等待足夠時(shí)間
{
var speed = _transferredBytes / elapsed.TotalSeconds;
var remainingSeconds = remainingBytes / speed;
// 防溢出處理
if (remainingSeconds > 0 && remainingSeconds <= TimeSpan.MaxValue.TotalSeconds)
{
if (remainingSeconds > 3600 * 999) // 限制最大顯示時(shí)間
lblTimeRemainingValue.Text = "> 999小時(shí)";
else
lblTimeRemainingValue.Text = FormatTime(TimeSpan.FromSeconds(remainingSeconds));
}
}
}
2、網(wǎng)絡(luò)異常處理
// 坑點(diǎn):網(wǎng)絡(luò)中斷時(shí)要優(yōu)雅處理
private async Task HandleClientAsync()
{
try
{
while (_isRunning && _isClientConnected)
{
var headerBuffer = new byte[HeaderSize];
var totalRead = 0;
// 關(guān)鍵:確保完整讀取header
while (totalRead < HeaderSize)
{
var bytesRead = await _stream.ReadAsync(headerBuffer, totalRead,
HeaderSize - totalRead, _cancellationTokenSource.Token);
if (bytesRead == 0) // 客戶端斷開
{
OnStatusChanged("客戶端斷開連接");
return;
}
totalRead += bytesRead;
}
}
}
catch (Exception ex)
{
OnStatusChanged($"網(wǎng)絡(luò)異常: {ex.Message}", true);
}
}
運(yùn)行效果如下:


企業(yè)內(nèi)網(wǎng)文件同步
// 批量文件傳輸示例
foreach (var filePath in Directory.GetFiles(sourceDirectory))
{
await client.SendFileAsync(filePath);
await Task.Delay(100); // 避免網(wǎng)絡(luò)擁塞
}
自動(dòng)備份系統(tǒng)集成
// 定時(shí)備份集成
var timer = new System.Timers.Timer(TimeSpan.FromHours(6).TotalMilliseconds);
timer.Elapsed += async (s, e) =>
{
var backupFiles = GetBackupFiles();
foreach (var file in backupFiles)
{
await client.SendFileAsync(file);
}
};
總結(jié)
通過本次實(shí)戰(zhàn),我們成功打造了一個(gè)職責(zé)清晰、穩(wěn)定可靠的網(wǎng)絡(luò)文件傳輸工具。整個(gè)方案堅(jiān)持三個(gè)關(guān)鍵設(shè)計(jì)原則:
1、職責(zé)分離:服務(wù)端專注接收,客戶端專注發(fā)送,避免功能臃腫
2、異步處理:全程使用async/await,確保UI響應(yīng)和傳輸效率
3、異常兜底:TimeSpan溢出、網(wǎng)絡(luò)中斷等邊界情況的優(yōu)雅處理
同時(shí),兩個(gè)實(shí)用技巧也值得借鑒:
- 智能重命名:自動(dòng)處理文件名沖突,避免覆蓋重要數(shù)據(jù)
- 進(jìn)度可視化:實(shí)時(shí)速度計(jì)算和剩余時(shí)間估算,提升用戶體驗(yàn)
這套方案已在多個(gè)工業(yè)場(chǎng)景中穩(wěn)定運(yùn)行,無論是作為獨(dú)立工具,還是嵌入到更大的系統(tǒng)中,都能勝任文件傳輸?shù)暮诵娜蝿?wù)。
以上就是使用C#實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)文件傳輸工具的詳細(xì)內(nèi)容,更多關(guān)于C#網(wǎng)絡(luò)文件傳輸?shù)馁Y料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Python利用ftplib庫(kù)實(shí)現(xiàn)ftp文件傳輸自動(dòng)化全攻略
- Python使用Paramiko庫(kù)進(jìn)行SSH遠(yuǎn)程連接與文件傳輸
- 基于Vue3+WebSocket實(shí)現(xiàn)實(shí)時(shí)文件傳輸監(jiān)控系統(tǒng)
- 從Windows到Linux的三種文件傳輸實(shí)現(xiàn)方式
- React+Node.js實(shí)現(xiàn)大文件傳輸與斷點(diǎn)續(xù)傳
- 詳解Python如何使用Netmiko進(jìn)行文件傳輸
- 臨時(shí)文件傳輸工具哪個(gè)好用?WorkBuddy一鍵分享用完自動(dòng)銷毀不留痕
相關(guān)文章
C#利用性能計(jì)數(shù)器監(jiān)控網(wǎng)絡(luò)狀態(tài)
這篇文章主要為大家詳細(xì)介紹了C#利用性能計(jì)數(shù)器監(jiān)控網(wǎng)絡(luò)狀態(tài)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
C#難點(diǎn)逐個(gè)擊破(3):params數(shù)組參數(shù)
注意,這里的paras全稱是array parameter,也就是數(shù)組參數(shù)。 paras類型參數(shù)主要用于在對(duì)數(shù)組長(zhǎng)度未知(可變)的情況下進(jìn)行函數(shù)聲明。2010-02-02
Unity的BuildPlayerProcessor實(shí)用案例深入解析
這篇文章主要為大家介紹了Unity的BuildPlayerProcessor實(shí)用案例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
C#使用文件流FileStream和內(nèi)存流MemoryStream操作底層字節(jié)數(shù)組byte[]
這篇文章介紹了C#使用文件流FileStream和內(nèi)存流MemoryStream操作底層字節(jié)數(shù)組byte[]的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
基于C#實(shí)現(xiàn)鼠標(biāo)設(shè)置功能
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)鼠標(biāo)設(shè)置功能,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
C#調(diào)用打印機(jī)實(shí)現(xiàn)打印
這篇文章介紹了C#調(diào)用打印機(jī)實(shí)現(xiàn)打印的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04

