接口调通了,文件下下来只有 0 字节,或者打开提示压缩包已损坏。多数情况是拿 GetStringAsync 去接二进制流,编码转换把字节改坏了。
小文件直接拿字节数组
using var client = new HttpClient(); var bytes = await client.GetByteArrayAsync(url); await File.WriteAllBytesAsync(savePath, bytes);写法短,适合几百 KB 的图片、配置、证书。HttpClient 要复用,放在 static readonly 字段里,每次 new 一个会把本地端口耗光,跑久了就报 socket 耗尽。
大文件走流,不要整个读进内存
using var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); resp.EnsureSuccessStatusCode(); await using var fs = File.Create(savePath); await resp.Content.CopyToAsync(fs);ResponseHeadersRead 让方法在收到响应头时就返回,body 边收边写。几百兆的安装包这样下,内存曲线是平的。要做进度条就自己读流,用累计字节除以 Content-Length,注意这个头可能为空或者是压缩后长度。
几个反复踩的坑
- 目标站点校验 User-Agent 或 Referer,补上请求头再试
- HttpClient 默认 100 秒超时,大文件必须单独设 Timeout
- 保存目录不存在,File.Create 直接抛异常,先 Directory.CreateDirectory
- WebClient 已经过时,新代码不必再写
还有一种坑是重定向,短链跳到 CDN 之后方法变了,默认会跟随 302,但认证头会被丢掉,需要显式处理。下载完比对一次字节数,和响应头里的 Content-Length 对上再交给业务。
我的取舍:临时脚本用字节数组,正式服务一律流式加超时和重试,下载失败重试两次比抛异常给用户强。