简介:这是一份面向C# Winform开发者的实战项目源码,聚焦局域网环境下将本地文件上传至服务器共享文件夹这一常见企业内网需求。项目基于VS2010构建,完整演示了从OpenFileDialog选择文件、通过NetworkCredential与UNC路径建立网络连接,到FileStream与NetworkStream流式读写、BackgroundWorker异步更新进度条,再到异常捕获与权限校验的全流程实现,适合具备一定C#基础、希望掌握文件服务器交互的开发者参考。压缩包共23个文件,约36KB,以cs源码文件为主,辅以exe可执行程序、pdb调试符号、resx与resources资源文件、csproj工程配置及sln解决方案文件,结构紧凑便于直接编译运行。目前已有1423人学习下载。通过研读源码,读者可掌握共享文件夹读写、网络凭据认证、上传进度反馈与错误处理等关键技巧,并借鉴文件类型与大小限制等安全思路,快速迁移到自身项目中。
1. 从一次“文件传不上去”的排查说起:WinForm 上传到共享文件夹到底难在哪
上周同事找我,说他们车间那台工控机上的 WinForm 程序点“上传”按钮后,进度条卡在 0% 不动,日志里只留下一句Could not find a part of the path。我过去一看,代码里写的是File.Copy(@"D:\报表.xlsx", @"\\192.168.1.20\共享\报表.xlsx"),路径本身没错,但程序是以普通域账号跑的,而那台文件服务器上根本没给这个账号写权限。这件事特别典型——很多人以为“上传文件到共享文件夹”就是一句File.Copy的事,真到部署环境里,权限、凭据、网络路径、大文件超时、中文文件名乱码,随便一个都能让功能翻车。
这篇笔记就围绕 WinForm 上传文件到共享文件夹这个场景,把从选型、写代码、处理凭据,到排查坑点的完整链路拆开讲。它适合两类人:一类是刚接手 WinForm 项目、需要把本地文件送到局域网共享目录的开发者;另一类是已经写过上传、但被权限或大文件问题反复折磨的熟手。下面所有代码都是我在实际项目里跑过的,参数和边界我会尽量说清楚,你照着改改就能用。
2. 先想清楚走哪条路:File.Copy、映射盘还是 SMB 直连
在动手写代码之前,得先决定用哪种方式把文件送到共享文件夹。WinForm 里常见做法有三种:直接File.Copy走 UNC 路径、先net use映射网络驱动器再当本地盘操作、以及用WNetAddConnection2这类 Win32 API 建立带凭据的 SMB 连接。选错了,后面权限和凭据问题会一直缠着你。
2.1 三种方式的适用边界与选型理由
File.Copy走 UNC 路径是最省事的,代码就一行,但它依赖当前进程的运行身份。如果程序以域账号运行、且该账号在共享服务器上有写权限,那它完全够用。问题在于很多现场是工作组环境,或者程序被安排成 Windows 服务、计划任务运行,这时候进程身份可能是SYSTEM或某个本地账号,对远程共享根本没有凭据,File.Copy就会直接抛异常。
映射网络驱动器(net use Z: \\server\share /user:xxx pwd)的好处是路径变成Z:\,看起来像本地盘,老代码改起来少。但它有两个硬伤:一是映射是会话级的,服务或不同用户会话里看不到;二是映射盘符可能被占用,部署到几十台机器时盘符冲突很烦。
WNetAddConnection2建立的是带显式凭据的连接,不依赖进程当前身份,也不占用盘符,用完可以WNetCancelConnection2断开。它最适合“程序运行账号和共享访问账号不一致”的场景,也是我在工控、产线项目里最常用的方案。代价是要写一点 P/Invoke,代码量比File.Copy多,但换来的是凭据可控。
| 方式 | 依赖进程身份 | 是否占盘符 | 适合场景 |
|---|---|---|---|
| File.Copy + UNC | 是 | 否 | 域环境、运行账号有权限 |
| 映射网络驱动器 | 是 | 是 | 临时脚本、单机调试 |
| WNetAddConnection2 | 否 | 否 | 工作组、服务、凭据分离 |
提示:如果你的程序将来可能被做成 Windows 服务,建议一开始就用
WNetAddConnection2,否则从File.Copy迁移过去要改不少地方。
2.2 用 WNetAddConnection2 建立带凭据的连接
下面这段代码封装了连接和断开两个操作。NETRESOURCE结构里lpRemoteName填共享根路径,比如\\192.168.1.20\共享,不要带具体文件名。dwType用RESOURCETYPE_DISK,表示磁盘资源。
using System; using System.Runtime.InteropServices; public static class NetworkConnection { [StructLayout(LayoutKind.Sequential)] private class NETRESOURCE { public int dwScope = 0; public int dwType = 1; // RESOURCETYPE_DISK public int dwDisplayType = 0; public int dwUsage = 0; public string lpLocalName = null; public string lpRemoteName = null; public string lpComment = null; public string lpProvider = null; } [DllImport("mpr.dll")] private static extern int WNetAddConnection2( NETRESOURCE lpNetResource, string lpPassword, string lpUsername, int dwFlags); [DllImport("mpr.dll")] private static extern int WNetCancelConnection2( string lpName, int dwFlags, bool fForce); // 建立连接,返回 0 表示成功 public static int Connect(string remotePath, string user, string pwd) { var nr = new NETRESOURCE { lpRemoteName = remotePath }; return WNetAddConnection2(nr, pwd, user, 0); } // 断开连接,fForce=true 表示有打开文件也强制断 public static int Disconnect(string remotePath) { return WNetCancelConnection2(remotePath, 0, true); } }逻辑上,Connect把用户名、密码、远程路径交给系统去建立 SMB 会话,成功返回 0,失败返回错误码。常见错误码 1326 表示用户名或密码错,53 表示找不到网络路径,5 表示权限不足。参数dwFlags传 0 即可,传CONNECT_INTERACTIVE会弹窗,不适合后台程序。断开时fForce设 true,避免文件句柄没释放导致断不掉。
2.3 上传动作本身:分块复制与进度反馈
连接建好后,上传可以用File.Copy,但大文件会卡界面。WinForm 里我一般用流式复制,每读一块就更新进度条,顺便支持取消。下面这个方法把源文件复制到目标路径,progress回调用来刷新 UI。
public static void UploadWithProgress( string source, string target, Action<long, long> progress, CancellationToken token) { const int bufferSize = 81920; // 80KB,兼顾吞吐和响应 var total = new FileInfo(source).Length; long copied = 0; using (var src = new FileStream(source, FileMode.Open, FileAccess.Read)) using (var dst = new FileStream(target, FileMode.Create, FileAccess.Write)) { var buffer = new byte[bufferSize]; int read; while ((read = src.Read(buffer, 0, buffer.Length)) > 0) { token.ThrowIfCancellationRequested(); dst.Write(buffer, 0, read); copied += read; progress?.Invoke(copied, total); } } }bufferSize设 80KB 是经验值,太小会增加系统调用次数,太大则进度更新不流畅。progress回调里不要直接操作控件,用Invoke切回 UI 线程。CancellationToken让用户能中途取消,取消时FileStream的using会释放句柄,但目标文件可能残留半截,需要在上层捕获异常后删除。
3. 把上传接进 WinForm:线程、进度条与中文文件名
代码能跑通只是第一步,真正放进 WinForm 界面里,还要处理 UI 线程、进度条刷新、中文文件名乱码这些细节。这一章把界面层的接法讲透。
3.1 用 Task 跑上传,别阻塞 UI 线程
WinForm 的按钮点击事件跑在 UI 线程上,如果直接在里面调UploadWithProgress,界面会假死。正确做法是await Task.Run把复制放到线程池,进度回调里用Invoke更新控件。
private CancellationTokenSource _cts; private async void btnUpload_Click(object sender, EventArgs e) { var source = txtSource.Text; var target = @"\\192.168.1.20\共享\" + Path.GetFileName(source); _cts = new CancellationTokenSource(); progressBar1.Value = 0; try { await Task.Run(() => NetworkConnection.UploadWithProgress( source, target, (done, total) => { var percent = (int)(done * 100 / total); progressBar1.Invoke(new Action(() => { progressBar1.Value = percent; lblStatus.Text = $"{done / 1024}KB / {total / 1024}KB"; })); }, _cts.Token), _cts.Token); } catch (OperationCanceledException) { lblStatus.Text = "已取消"; } catch (Exception ex) { MessageBox.Show("上传失败:" + ex.Message); } }Task.Run把耗时操作移出 UI 线程,Invoke保证进度条更新在 UI 线程执行。_cts保存为字段,方便取消按钮调用_cts.Cancel()。注意progressBar1.Value最大是 100,如果文件超过 100MB,按百分比算没问题,但lblStatus里显示 KB 更直观。
3.2 中文文件名乱码的根因与处理
中文文件名乱码通常不是 WinForm 本身的问题,而是共享服务器端编码或传输环节的编码不一致。SMB 协议本身支持 Unicode,File.Copy和FileStream传中文路径一般不会乱。真正会出问题的是两种情况:一是目标路径字符串在拼接时用了错误的编码转换,二是共享服务器是某些 Linux 系统通过 Samba 提供,unix charset配置不对。
排查时先确认源文件名和目标文件名在资源管理器里是否正常。如果资源管理器正常、程序写入后乱码,检查代码里有没有Encoding.Default.GetBytes这类转换。如果资源管理器里就乱,那是服务器端 Samba 配置问题,需要改smb.conf里的unix charset = UTF-8。在 WinForm 侧,我一般直接用Path.GetFileName(source)取文件名,不做任何编码转换,让系统去处理。
注意:如果共享路径里有中文,确保项目文件保存为 UTF-8,且字符串前不加
@以外的转义处理。C# 源文件编码不对也会导致路径字符串本身出错。
3.3 上传前先做可达性与权限预检
与其等File.Copy抛异常,不如上传前先探测一下。我通常做两步:先用Ping或TcpClient测 445 端口是否通,再用Directory.Exists判断共享路径是否可访问。注意Directory.Exists对无权限的路径返回 false,不抛异常,所以它只能判断“能不能看到”,不能判断“能不能写”。
private bool CanAccessShare(string remotePath) { try { // 先测 445 端口,超时 2 秒 var uri = new Uri("file://" + remotePath); using (var client = new System.Net.Sockets.TcpClient()) { var result = client.BeginConnect(uri.Host, 445, null, null); if (!result.AsyncWaitHandle.WaitOne(2000)) return false; client.EndConnect(result); } return Directory.Exists(remotePath); } catch { return false; } }TcpClient测 445 端口能快速判断网络是否可达,避免Directory.Exists卡很久。WaitOne(2000)设 2 秒超时,现场网络差时可以适当放宽。返回 false 时给用户提示“共享不可达,请检查网络或权限”,比直接抛异常友好。
4. 避坑与排查:权限、凭据、大文件与断点
这一章是我踩过的坑合集,每条都按“现象 → 原因 → 解决”写。你如果正卡在某个问题上,可以直接对号入座。
4.1 现象:提示“找不到网络路径”或“拒绝访问”
原因:最常见的是运行账号对共享目录没有写权限,或者共享服务器只允许特定账号访问。工作组环境下,两台机器的本地账号即使同名,密码不同也会导致凭据校验失败。另一个原因是 Windows 的“凭据管理器”里缓存了旧密码,程序用新密码连接时被旧凭据覆盖。
解决:先用WNetAddConnection2显式传用户名密码,不要依赖系统缓存。如果仍然失败,在命令行执行net use \\192.168.1.20\共享 /delete清除已有连接,再重试。共享服务器侧要确认共享权限和 NTFS 权限都给了写权限,两者缺一不可。
4.2 现象:程序在开发机正常,部署到现场就失败
原因:开发机当前登录账号有权限,现场机器可能用另一个账号运行,或者程序被配置成开机自启、以服务方式运行。服务默认运行在SYSTEM账号下,SYSTEM访问网络共享时使用的是机器账号,工作组环境下机器账号无法通过共享服务器认证。
解决:把程序改成用指定账号运行,或者在代码里用WNetAddConnection2带凭据连接。如果必须做成服务,可以在服务的“登录”选项卡里指定一个有权限的域账号。现场部署前,务必在目标机器上用目标账号手动访问一次共享,确认权限没问题。
4.3 现象:大文件传到一半卡住或超时
原因:File.Copy没有超时控制,网络抖动时会一直等。另外 SMB 默认有会话超时,长时间无响应可能被服务器断开。如果目标磁盘空间不足,写入会失败但异常信息可能不直观。
解决:改用流式复制,每读一块检查取消令牌,并在上层加超时。目标路径先用DriveInfo检查剩余空间,或者捕获IOException后提示“磁盘空间不足”。对于超过 1GB 的文件,建议分块复制并记录已传字节,支持断点续传。
4.4 现象:中文文件名在共享服务器上变成乱码
原因:Samba 服务器的unix charset不是 UTF-8,或者客户端用了非 Unicode 的 API。WinForm 默认用 Unicode,但如果路径字符串经过Encoding.Default转换,就会按本地代码页编码,到服务器端解码错误。
解决:代码里不要做任何编码转换,直接用string传路径。服务器端检查smb.conf,确保unix charset = UTF-8、dos charset = CP936。如果服务器是 Windows,一般不会有这个问题,重点查客户端代码。
4.5 现象:上传成功后文件大小为 0 或内容不全
原因:FileStream没有正确Flush或Dispose,数据还在缓冲区。另一种可能是复制过程中抛了异常,但异常被吞掉,目标文件已创建但没写完。
解决:用using包裹FileStream,确保释放时刷新缓冲区。异常处理里不要只catch不处理,至少记录日志。上传完成后可以比对源文件和目标文件的长度,不一致就报错。
5. 进阶:断点续传、多文件队列与上传后校验
基础功能跑通后,现场往往还会提更多要求:文件太大要能续传、一次选多个文件要排队、传完要确认内容没坏。这一章讲三个进阶技巧,都是我实际项目里用过的。
5.1 断点续传:记录已传偏移量
断点续传的核心是记录已成功写入的字节数,下次从该位置继续。实现时用FileStream的Seek定位,目标文件以OpenOrCreate打开,先读目标文件长度作为已传量。
public static void ResumeUpload( string source, string target, Action<long, long> progress, CancellationToken token) { var total = new FileInfo(source).Length; long start = File.Exists(target) ? new FileInfo(target).Length : 0; using (var src = new FileStream(source, FileMode.Open, FileAccess.Read)) using (var dst = new FileStream(target, FileMode.OpenOrCreate, FileAccess.Write)) { src.Seek(start, SeekOrigin.Begin); dst.Seek(start, SeekOrigin.Begin); var buffer = new byte[81920]; long copied = start; int read; while ((read = src.Read(buffer, 0, buffer.Length)) > 0) { token.ThrowIfCancellationRequested(); dst.Write(buffer, 0, read); copied += read; progress?.Invoke(copied, total); } } }start从目标文件长度取,如果目标文件比源文件还大,说明之前传坏了,应该删除重传。Seek之后读写位置对齐,后续逻辑和普通复制一样。这个方案适合网络不稳定、文件又大的场景,但要注意目标文件在续传期间不能被其他程序修改。
5.2 多文件队列:用 ConcurrentQueue 加后台线程
用户一次选多个文件时,不要每个文件开一个 Task,那样并发写同一个共享目录可能触发服务器限流。我一般用ConcurrentQueue存文件列表,后台一个Task顺序处理,界面显示“第几个/共几个”。
private ConcurrentQueue<string> _queue = new ConcurrentQueue<string>(); private async Task ProcessQueueAsync() { while (_queue.TryDequeue(out var file)) { var target = @"\\192.168.1.20\共享\" + Path.GetFileName(file); await Task.Run(() => NetworkConnection.UploadWithProgress( file, target, UpdateProgress, CancellationToken.None)); // 每个文件传完可以短暂休眠,避免服务器压力过大 await Task.Delay(200); } }ConcurrentQueue保证多线程入队安全,TryDequeue在队列空时返回 false,循环自然结束。每个文件之间加 200ms 延迟,是给共享服务器一点喘息时间,文件特别多时可以适当加大。进度显示要区分“当前文件进度”和“总进度”,否则用户会困惑。
5.3 上传后校验:比对哈希值
传完文件后,如果业务对完整性要求高,可以比对源文件和目标文件的哈希。注意大文件算哈希很耗时,建议只对关键文件做,或者用文件长度加修改时间做快速校验。
private bool VerifyFile(string source, string target) { var srcInfo = new FileInfo(source); var dstInfo = new FileInfo(target); if (srcInfo.Length != dstInfo.Length) return false; using (var md5 = System.Security.Cryptography.MD5.Create()) using (var src = File.OpenRead(source)) using (var dst = File.OpenRead(target)) { var srcHash = md5.ComputeHash(src); md5.Initialize(); var dstHash = md5.ComputeHash(dst); return BitConverter.ToString(srcHash) == BitConverter.ToString(dstHash); } }MD5在这里只做完整性校验,不涉及安全场景。md5.Initialize()在两次计算之间重置状态,否则第二次会接着第一次的上下文算。如果文件超过几百 MB,算 MD5 可能几秒钟,建议放在后台线程并给用户提示。
5.4 一个我常用的部署检查习惯
从那以后我每次部署 WinForm 上传功能前,都强制走一遍这个检查:在目标机器上用目标账号打开资源管理器,手动往共享目录里拖一个文件,确认能写;然后在程序里点上传,看进度条和日志;最后把程序配成开机自启,重启一次再测。这三步能挡住九成的现场问题。希望帮到你。
本文还有配套的精品资源,点击获取