C#上位机通过Http传输文件(压缩文件,图片,csv等等)

C# WinForm/WPF 上位机 HTTP 文件上传流程

覆盖场景:单文件上传(CSV / 图片 / 压缩包)、多文件批量上传、多文件先压缩 ZIP 再上传、后端接收保存 + 解析文件,分客户端(C# 上位机)服务端(ASP.NET Core API) 两大部分,兼容 WinForm/WPF,支持大文件、进度回调、参数附带、异常处理。

一、整体技术选型

  1. 客户端:HttpClient(推荐,线程安全,替代 WebClient),System.IO.Compression 实现本地多文件打包 ZIP
  2. 服务端:ASP.NET Core WebAPI,接收 IFormFile 文件流
  3. 支持文件类型:.csv/.jpg/.png/.bmp/.gif/.zip/.rar/.txt/.xlsx 等任意二进制文件
  4. 功能点:
    • 单文件直接 HTTP 上传
    • 多选文件批量并行上传
    • 本地打包多文件为临时 ZIP,仅上传一个压缩包(减少请求次数)
    • 上传进度实时回调(UI 进度条)
    • 携带自定义业务参数(设备编号、时间戳、工位号)
    • 后端文件校验、自动创建目录、重命名防覆盖、解压服务端 ZIP
    • 超时、断流、文件不存在、超大文件异常捕获

第一部分:C# 上位机客户端通用工具类(WinForm/WPF 通用)

新建工具类 HttpFileUploadHelper.cs,无需第三方 NuGet,纯.NET 原生。

1. 基础依赖引用

复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.IO.Compression;

2. 进度回调委托(UI 更新进度条)

复制代码
/// <summary>上传进度回调:当前字节、总字节</summary>
public delegate void UploadProgressDelegate(long currentBytes, long totalBytes);

public static class HttpFileUploadHelper
{
    // 全局HttpClient静态实例,复用连接(禁止每次new HttpClient)
    private static readonly HttpClient _httpClient = new HttpClient()
    {
        Timeout = TimeSpan.FromMinutes(5) // 大文件5分钟超时
    };

    #region 场景1:单文件直接上传(CSV/图片/压缩包通用)
    /// <summary>
    /// 单文件HTTP上传
    /// </summary>
    /// <param name="apiUrl">后端上传接口地址</param>
    /// <param name="filePath">本地文件完整路径</param>
    /// <param name="businessParams">自定义业务参数 如设备Id、工位</param>
    /// <param name="progress">上传进度回调</param>
    /// <param name="cancellationToken">取消上传</param>
    /// <returns>后端返回的JSON字符串</returns>
    public static async Task<string> UploadSingleFileAsync(
        string apiUrl,
        string filePath,
        Dictionary<string, string> businessParams = null,
        UploadProgressDelegate progress = null,
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(filePath))
            throw new FileNotFoundException("文件不存在", filePath);

        var fileInfo = new FileInfo(filePath);
        long totalLength = fileInfo.Length;

        // 构造multipart/form-data表单
        using var content = new MultipartFormDataContent();

        // 1. 添加业务自定义参数
        if (businessParams != null)
        {
            foreach (var kv in businessParams)
            {
                content.Add(new StringContent(kv.Value), kv.Key);
            }
        }

        // 2. 创建带进度监听的文件流
        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
        var progressStream = new ProgressStream(fileStream, progress, totalLength);
        var streamContent = new StreamContent(progressStream);

        // 设置文件MIME类型
        string mimeType = GetMimeType(fileInfo.Extension);
        streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
        content.Add(streamContent, "uploadFile", fileInfo.Name);

        // 3. 发起POST请求
        using var response = await _httpClient.PostAsync(apiUrl, content, cancellationToken);
        response.EnsureSuccessStatusCode(); // 非200直接抛异常
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
    #endregion

    #region 场景2:多文件分别批量上传(多次HTTP请求)
    /// <summary>批量逐个上传多个独立文件</summary>
    public static async Task<List<string>> UploadMultiFilesSeparateAsync(
        string apiUrl,
        List<string> filePaths,
        Dictionary<string, string> businessParams = null,
        UploadProgressDelegate singleFileProgress = null,
        CancellationToken cancellationToken = default)
    {
        var resultList = new List<string>();
        foreach (var file in filePaths)
        {
            var res = await UploadSingleFileAsync(apiUrl, file, businessParams, singleFileProgress, cancellationToken);
            resultList.Add(res);
        }
        return resultList;
    }
    #endregion

    #region 场景3:多文件本地打包ZIP后只上传一个压缩包(推荐大量文件场景)
    /// <summary>
    /// 多个文件先压缩成临时ZIP,再上传压缩包,上传完成自动删除临时zip
    /// </summary>
    public static async Task<string> UploadFilesAsZipAsync(
        string apiUrl,
        List<string> sourceFilePaths,
        string tempZipSavePath = null,
        Dictionary<string, string> businessParams = null,
        UploadProgressDelegate progress = null,
        CancellationToken cancellationToken = default)
    {
        // 生成临时压缩包路径(不传则自动放系统临时目录)
        if (string.IsNullOrEmpty(tempZipSavePath))
        {
            tempZipSavePath = Path.Combine(Path.GetTempPath(), $"upload_{Guid.NewGuid():N}.zip");
        }

        // 本地打包所有文件到zip
        CreateZipFromFiles(sourceFilePaths, tempZipSavePath);

        try
        {
            // 上传生成的zip压缩包
            var result = await UploadSingleFileAsync(apiUrl, tempZipSavePath, businessParams, progress, cancellationToken);
            return result;
        }
        finally
        {
            // 上传成功/失败都删除临时压缩文件
            if (File.Exists(tempZipSavePath))
                File.Delete(tempZipSavePath);
        }
    }

    /// <summary>将多个本地文件打包成ZIP</summary>
    private static void CreateZipFromFiles(List<string> fileList, string zipPath)
    {
        if (File.Exists(zipPath)) File.Delete(zipPath);

        using var zipFile = ZipFile.Open(zipPath, ZipArchiveMode.Create);
        foreach (var file in fileList)
        {
            if (!File.Exists(file)) continue;
            var fileName = Path.GetFileName(file);
            zipFile.CreateEntryFromFile(file, fileName, CompressionLevel.Optimal);
        }
    }
    #endregion

    #region 辅助工具:MIME类型映射 + 进度监听流
    /// <summary>文件后缀对应MIME类型</summary>
    private static string GetMimeType(string ext)
    {
        return ext.ToLower() switch
        {
            ".csv" => "text/csv",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".bmp" => "image/bmp",
            ".gif" => "image/gif",
            ".zip" => "application/zip",
            ".rar" => "application/x-rar-compressed",
            ".txt" => "text/plain",
            ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            _ => "application/octet-stream" // 未知二进制文件通用
        };
    }

    /// <summary>包装文件流,读取时触发上传进度回调</summary>
    private class ProgressStream : Stream
    {
        private readonly Stream _innerStream;
        private readonly UploadProgressDelegate _progressCallback;
        private readonly long _totalLength;
        private long _readBytes;

        public ProgressStream(Stream inner, UploadProgressDelegate callback, long totalLen)
        {
            _innerStream = inner;
            _progressCallback = callback;
            _totalLength = totalLen;
            _readBytes = 0;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            var read = _innerStream.Read(buffer, offset, count);
            if (read > 0)
            {
                _readBytes += read;
                _progressCallback?.Invoke(_readBytes, _totalLength);
            }
            return read;
        }

        // 下面简单转发所有Stream虚方法
        public override bool CanRead => _innerStream.CanRead;
        public override bool CanSeek => _innerStream.CanSeek;
        public override bool CanWrite => _innerStream.CanWrite;
        public override long Length => _innerStream.Length;
        public override long Position { get; set; }
        public override void Flush() => _innerStream.Flush();
        public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
        public override void SetLength(long value) => _innerStream.SetLength(value);
        public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
        protected override void Dispose(bool disposing) => _innerStream.Dispose();
    }
    #endregion
}

3. WinForm 上位机界面调用示例(带进度条)

界面控件:

  • textBox_ApiUrl:输入后端接口地址 http://localhost:5000/api/FileUpload/Upload

  • progressBar1:上传进度条

  • label_ProgressText:显示百分比

  • button_SelectSingle:选择单个文件上传

  • button_SelectMultiZip:多选文件打包 ZIP 上传

    using System.Windows.Forms;

    private async void button_SelectSingle_Click(object sender, EventArgs e)
    {
    using var dialog = new OpenFileDialog
    {
    Filter = "所有文件|.|CSV文件|.csv|图片|.jpg;.png;.bmp|压缩包|.zip;.rar",
    Multiselect = false
    };
    if (dialog.ShowDialog() != DialogResult.OK) return;

    复制代码
      string filePath = dialog.FileName;
      string api = textBox_ApiUrl.Text.Trim();
    
      // 业务参数:上位机设备信息
      var param = new Dictionary<string, string>()
      {
          {"DeviceId", "MACHINE_001"},
          {"Station", "装配工位3"},
          {"UploadTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}
      };
    
      // 进度回调(UI线程安全,Invoke更新控件)
      void ProgressUpdate(long curr, long total)
      {
          double percent = curr * 100.0 / total;
          this.Invoke(new Action(() =>
          {
              progressBar1.Maximum = (int)total / 1024; // KB单位
              progressBar1.Value = (int)curr / 1024;
              label_ProgressText.Text = $"已上传 {curr / 1024}KB / {total / 1024}KB  {percent:F1}%";
          }));
      }
    
      try
      {
          string result = await HttpFileUploadHelper.UploadSingleFileAsync(api, filePath, param, ProgressUpdate);
          MessageBox.Show($"上传成功!服务端返回:\n{result}");
      }
      catch (Exception ex)
      {
          MessageBox.Show($"上传失败:{ex.Message}");
      }

    }

    // 多选文件打包ZIP上传
    private async void button_SelectMultiZip_Click(object sender, EventArgs e)
    {
    using var dialog = new OpenFileDialog
    {
    Multiselect = true,
    Filter = "所有文件|."
    };
    if (dialog.ShowDialog() != DialogResult.OK) return;

    复制代码
      List<string> files = dialog.FileNames.ToList();
      string api = textBox_ApiUrl.Text.Trim();
      var param = new Dictionary<string, string>() { {"DeviceId", "MACHINE_001"} };
    
      void ProgressUpdate(long curr, long total)
      {
          double percent = curr * 100.0 / total;
          this.Invoke(new Action(() =>
          {
              progressBar1.Maximum = (int)total / 1024;
              progressBar1.Value = (int)curr / 1024;
              label_ProgressText.Text = $"压缩包上传进度 {percent:F1}%";
          }));
      }
    
      try
      {
          string res = await HttpFileUploadHelper.UploadFilesAsZipAsync(api, files, null, param, ProgressUpdate);
          MessageBox.Show($"打包上传完成:{res}");
      }
      catch (Exception ex)
      {
          MessageBox.Show($"打包上传异常:{ex.Message}");
      }

    }

4. WPF 适配说明

WPF 无需修改工具类,进度更新把 this.Invoke 替换成 Application.Current.Dispatcher.Invoke 即可。

第二部分:后端 ASP.NET Core API 接收文件处理(配套上位机)

1. 创建 FileUpload 控制器 FileUploadController.cs

支持:接收单文件、接收压缩包并自动解压、读取业务参数、文件存储、返回 JSON 结果

复制代码
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.IO.Compression;

namespace UploadServer.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class FileUploadController : ControllerBase
    {
        // 文件根存储目录
        private readonly string _saveRoot = Path.Combine(AppContext.BaseDirectory, "UploadFiles");

        public FileUploadController()
        {
            // 不存在自动创建目录
            if (!Directory.Exists(_saveRoot))
                Directory.CreateDirectory(_saveRoot);
        }

        /// <summary>统一上传接口:接收单文件/zip压缩包</summary>
        [HttpPost("Upload")]
        public IActionResult Upload([FromForm] UploadModel model)
        {
            // 1. 获取上位机传的业务参数
            string deviceId = model.DeviceId ?? "UnknownDevice";
            string station = model.Station ?? "Default";
            string uploadTime = model.UploadTime ?? DateTime.Now.ToString("yyyyMMddHHmmss");

            if (model.uploadFile == null || model.uploadFile.Length == 0)
                return BadRequest(new { code = -1, msg = "未检测到上传文件" });

            // 按设备+日期分目录存储
            string dateDir = DateTime.Now.ToString("yyyy-MM-dd");
            string targetDir = Path.Combine(_saveRoot, deviceId, dateDir);
            Directory.CreateDirectory(targetDir);

            // 原始文件名,重命名防止覆盖
            string fileName = Path.GetFileName(model.uploadFile.FileName);
            string saveFullPath = Path.Combine(targetDir, $"{uploadTime}_{fileName}");

            // 保存文件到磁盘
            using var stream = new FileStream(saveFullPath, FileMode.Create);
            model.uploadFile.CopyTo(stream);

            // 2. 如果是zip压缩包,自动解压到同目录unzip文件夹
            if (Path.GetExtension(fileName).ToLower() == ".zip")
            {
                string unzipDir = Path.Combine(targetDir, $"unzip_{Path.GetFileNameWithoutExtension(fileName)}");
                Directory.CreateDirectory(unzipDir);
                ZipFile.ExtractToDirectory(saveFullPath, unzipDir, true); // true覆盖重名
            }

            // 3. 返回成功JSON给上位机
            return Ok(new
            {
                code = 0,
                msg = "文件接收处理完成",
                data = new
                {
                    DeviceId = deviceId,
                    SavePath = saveFullPath,
                    FileSize = model.uploadFile.Length,
                    IsZip = Path.GetExtension(fileName).ToLower() == ".zip",
                    UnzipFolder = Path.GetDirectoryName(saveFullPath) + $"/unzip_{Path.GetFileNameWithoutExtension(fileName)}"
                }
            });
        }
    }

    /// <summary>接收表单参数绑定模型</summary>
    public class UploadModel
    {
        public IFormFile uploadFile { get; set; }
        // 上位机自定义业务参数
        public string DeviceId { get; set; }
        public string Station { get; set; }
        public string UploadTime { get; set; }
    }
}

2. 后端配置文件大小限制(Program.cs)

默认ASP.NET限制文件 30MB,上位机上传大图片 / 大 CSV / 大压缩包需要放开限制:

复制代码
var builder = WebApplication.CreateBuilder(args);

// 放开文件上传最大尺寸 100MB
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100 * 1024 * 1024;
});

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run("http://localhost:5000");

第三部分:上位机高频业务场景总结与优化方案

场景 1:单 CSV / 单图片实时上传(巡检截图、报表)

直接调用 UploadSingleFileAsync,无需压缩,上传速度最快,适合单次少量文件。

场景 2:单次几十上百张图片 + 报表 CSV 批量上传

优先用本地打包 ZIP 上传 UploadFilesAsZipAsync 优势:

  1. 只发起 1 次 HTTP 请求,减少网络握手耗时
  2. 压缩后体积大幅减小(图片无损压缩、文本 CSV 压缩率极高)
  3. 后端自动解压,一次性拿到全部原始文件
  4. 自动清理本地临时压缩包,无垃圾文件残留

场景 3:定时循环采集文件,后台静默上传

封装异步后台任务,搭配 CancellationTokenSource 支持停止上传,UI 不卡死。

场景 4:超大文件(>500MB)断点续传扩展

现有代码是完整文件上传,超大文件建议分片上传:

  1. 上位机分割文件为 10MB 分片,携带分片索引、文件 MD5
  2. 后端接收分片缓存,全部接收完成合并完整文件
  3. 工具类可在现有基础上增加分片上传方法

第四部分:常见问题踩坑指南

  1. HttpClient 频繁创建导致套接字泄漏 解决方案:工具类静态全局复用 HttpClient,不要按钮点击每次 new。

  2. WinForm 跨线程更新进度条报错 必须使用 Invoke/BeginInvoke 切 UI 主线程更新控件。

  3. 后端接收文件为空 IFormFile = null

    • 前端表单字段名必须和后端 uploadFile 完全一致(区分大小写)
    • 接口必须标记 [FromForm],不能用 JSON 传文件
    • 检查 Content-Type 为 multipart/form-data
  4. 文件上传超时

    • 增大 HttpClient.Timeout
    • 后端放开 MultipartBodyLengthLimit 限制
    • 大文件优先压缩再上传
  5. 中文文件名乱码 ASP.NET Core 默认支持,无需额外配置;旧版 IIS 部署需修改 web.config 编码 UTF-8。

  6. 临时 ZIP 文件残留 工具类 finally 块自动删除,即使上传报错也会清理。

第五部分:扩展功能建议(工业上位机常用)

  1. 上传前计算文件 MD5,传给后端做文件完整性校验
  2. 增加上传失败自动重试机制(网络波动)
  3. 上传队列管理,避免多线程并发上传占用带宽
  4. 增加日志写入本地:上传成功 / 失败记录文件、时间、设备号
  5. HTTPS 加密传输(生产环境禁止 HTTP 明文)
  6. 接口增加 Token 身份校验,防止非法设备上传文件