C# 实现网络文件传输:打造稳定可靠的工业级工具

前言:为什么需要这样一个工具?

在企业级开发中,文件传输功能几乎是必备需求。无论是内网文件同步、远程数据备份,还是分布式系统间的文件交换,一个稳定高效的文件传输工具都显得至关重要。

今天就有位开发私信我:"老师,我需要开发一个文件传输工具,服务端只管接收文件并保存到指定目录,客户端只管发送文件。网上的示例要么功能复杂,要么不够稳定,能否提供一个完整的解决方案?"相信很多朋友都遇到过类似需求。今天我们就来彻底搞定这个问题,用 C# 打造一个功能专一、稳定可靠的网络文件传输工具!

很多现成的文件传输方案要么依赖第三方库,要么集成了太多非核心功能,导致维护困难、部署复杂。而我们追求的是最小可行、专注单一职责的设计哲学------服务端只做接收,客户端只做发送,界面简洁直观,异常处理完善,真正做到"开箱即用"。

正文

需求分析与设计目标

我们的核心目标很明确:

1、职责清晰:服务端专门接收,客户端专门发送

2、界面友好:实时进度、速度显示、状态提示

3、异常处理:网络中断、文件冲突等场景的优雅处理

4、即插即用:最小化配置,开箱即用

架构设计

整体采用经典的分层解耦模式,核心组件包括:

  • FileTransferServer:服务端核心类,负责监听和接收

  • FileTransferClient:客户端核心类,负责连接和发送

  • TransferEventArgs:事件参数类,统一状态通知

  • FrmMain:UI主窗体,用户交互界面

架构图如下

服务端实现:专注接收文件

cs 复制代码
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($"服务器启动,监听端口 {port},文件保存目录: {SaveDirectory}");
                // 在后台异步等待客户端连接
                _ = Task.Run(async () => await WaitForClientAsync());
            }
            catch (Exception ex)
            {
                OnStatusChanged($"服务器启动失败: {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("客户端已连接"));
                    // 开始处理客户端请求
                    _ = Task.Run(async () => await HandleClientAsync());
                    break;
                }
            }
            catch (ObjectDisposedException)
            {
                // 服务器已停止,正常情况
            }
            catch (Exception ex)
            {
                OnStatusChanged($"等待客户端连接时出错: {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);
                        // 发送错误响应
                        var errorBytes = Encoding.UTF8.GetBytes("ERR");
                        await _stream.WriteAsync(errorBytes, 0, errorBytes.Length, _cancellationTokenSource.Token);
                    }
                }
            }
            catch (Exception ex)
            {
                OnStatusChanged($"处理客户端请求时出错: {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);
                }
                // 构造完整的文件路径
                var localFilePath = Path.Combine(SaveDirectory, headerInfo.FileName);
                // 如果文件已存在,添加时间戳后缀
                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)})");
                // 发送确认响应
                var confirmBytes = Encoding.UTF8.GetBytes("OK");
                await _stream.WriteAsync(confirmBytes, 0, confirmBytes.Length, _cancellationTokenSource.Token);
                // 在开始接收前触发开始事件,让UI设置开始时间
                OnTransferStarted(headerInfo.FileName, headerInfo.FileSize);
                // 接收文件内容
                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);
                // 发送错误响应
                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("服务器已停止");
            }
            catch (Exception ex)
            {
                OnStatusChanged($"停止服务器时出错: {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]}";
        }
    }
}

客户端实现:专注发送文件

cs 复制代码
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($"已连接到服务器 {serverIP}:{port}");
            }
            catch (Exception ex)
            {
                OnStatusChanged($"连接服务器失败: {ex.Message}", true);
                throw;
            }
        }

        public void Disconnect()
        {
            try
            {
                _isConnected = false;
                _cancellationTokenSource?.Cancel();
                _stream?.Close();
                _client?.Close();
                OnStatusChanged("已断开连接");
            }
            catch (Exception ex)
            {
                OnStatusChanged($"断开连接时出错: {ex.Message}", true);
            }
        }

        public async Task SendFileAsync(string localFilePath)
        {
            if (!_isConnected || _stream == null)
                throw new InvalidOperationException("未连接到服务器");
            if (!File.Exists(localFilePath))
                throw new FileNotFoundException($"文件不存在: {localFilePath}");

            try
            {
                var fileInfo = new FileInfo(localFilePath);
                var fileName = fileInfo.Name;
                OnStatusChanged($"开始发送文件: {fileName} ({FormatBytes(fileInfo.Length)})");
                // 触发传输开始事件
                OnTransferStarted(fileName, fileInfo.Length);
                // 发送文件头信息
                var header = CreateFileHeader("SEND", fileName, fileInfo.Length);
                await _stream.WriteAsync(header, 0, header.Length, _cancellationTokenSource.Token);
                // 等待服务器确认
                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($"服务器响应错误: {responseStr}");
                }
                // 发送文件内容
                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($"文件发送完成: {fileName}");
            }
            catch (Exception ex)
            {
                OnStatusChanged($"发送文件失败: {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设计与常见坑点处理

智能模式切换逻辑:

cs 复制代码
private void cmbMode_SelectedIndexChanged(object sender, EventArgs e)
{
    _isServer = cmbMode.SelectedIndex == 0;
    
    // 动态显示对应功能区域
    grpServerSettings.Visible = _isServer;
    grpClientSettings.Visible = !_isServer;
    
    // 智能IP设置
    if (_isServer)
        txtServerIP.Text = GetLocalIPAddress();
    else
        txtServerIP.Text = "127.0.0.1";
        
    UpdateTransferControls();
}

常见坑点提醒:

1、TimeSpan溢出问题

cs 复制代码
private void OnProgressChanged(object sender, TransferEventArgs e)
{
    // 坑点:传输初期速度计算可能导致TimeSpan溢出
    var elapsed = DateTime.Now - _transferStartTime;
    
    if (elapsed.TotalSeconds >= 1.0) // 关键:等待足够时间
    {
        var speed = _transferredBytes / elapsed.TotalSeconds;
        var remainingSeconds = remainingBytes / speed;
        
        // 防溢出处理
        if (remainingSeconds > 0 && remainingSeconds <= TimeSpan.MaxValue.TotalSeconds)
        {
            if (remainingSeconds > 3600 * 999) // 限制最大显示时间
                lblTimeRemainingValue.Text = "> 999小时";
            else
                lblTimeRemainingValue.Text = FormatTime(TimeSpan.FromSeconds(remainingSeconds));
        }
    }
}

2、网络异常处理

cs 复制代码
// 坑点:网络中断时要优雅处理
private async Task HandleClientAsync()
{
    try
    {
        while (_isRunning && _isClientConnected)
        {
            var headerBuffer = new byte[HeaderSize];
            var totalRead = 0;
            
            // 关键:确保完整读取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($"网络异常: {ex.Message}", true);
    }
}

运行效果如下:

null
null

企业内网文件同步

cs 复制代码
// 批量文件传输示例
foreach (var filePath in Directory.GetFiles(sourceDirectory))
{
    await client.SendFileAsync(filePath);
    await Task.Delay(100); // 避免网络拥塞
}

自动备份系统集成

cs 复制代码
// 定时备份集成
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);
    }
};

总结

通过本次实战,我们成功打造了一个职责清晰、稳定可靠的网络文件传输工具。整个方案坚持三个关键设计原则:

1、职责分离:服务端专注接收,客户端专注发送,避免功能臃肿

2、异步处理:全程使用async/await,确保UI响应和传输效率

3、异常兜底:TimeSpan溢出、网络中断等边界情况的优雅处理

同时,两个实用技巧也值得借鉴:

  • 智能重命名:自动处理文件名冲突,避免覆盖重要数据
  • 进度可视化:实时速度计算和剩余时间估算,提升用户体验

这套方案已在多个工业场景中稳定运行,无论是作为独立工具,还是嵌入到更大的系统中,都能胜任文件传输的核心任务。

关键词

C#、文件传输、TCP、服务端、客户端、异步编程、WinForm、工业应用、网络通信、进度显示

mp.weixin.qq.com/s/qjrx0m98ZG2mZQ0MSA_pPQ

最后

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

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

优秀是一种习惯,欢迎大家留言学习!

相关推荐
一 乐1 小时前
美食推荐|基于springboot+vue的美食分享系统设计与实现(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·美食
清晓粼溪1 小时前
SpringMVC02:扩展知识
java·后端·spring
MobotStone1 小时前
一文看懂AI智能体架构:工程师依赖的8种LLM,到底怎么分工?
后端·算法·llm
谷哥的小弟1 小时前
Spring Framework源码解析——Ordere
java·后端·spring·源码
前端fighter1 小时前
全栈项目:宠物用品购物系统及后台管理
前端·vue.js·后端
MM_MS1 小时前
SQL Server数据库和Visual Studio (C#)联合编程
开发语言·数据库·sqlserver·c#·visual studio
卓码软件测评2 小时前
第三方软件测试评测机构:【基于Scala DSL的Gatling脚本开发:从零开始构建首个负载测试模型】
后端·测试工具·测试用例·scala·负载均衡·压力测试
子洋2 小时前
LLM 原理 - 输入预处理
前端·人工智能·后端
Lovely_Ruby3 小时前
前端er Go-Frame 的学习笔记:实现 to-do 功能(三),用 docker 封装成镜像,并且同时启动前后端数据库服务
前端·后端