文章目录
前言
在.NET 里 实现文本下载可以依赖于HTTP协议来实现,通过基于客户端和服务器之间的请求 - 响应机制实现文件下载。本文先由HTTP协议的回顾开始,进而分别讨论.NET Core和.NET Framework里如何实现服务器本地文件下载。
一、HTTP 的 请求 - 响应模型
HTTP协议采用请求/响应模式,客户端向服务器发送一个请求报文,通过请求告诉服务器 “我需要某个文件”,服务器通过响应将文件数据传输给客户端。
1.1 浏览器(客户端)发起请求
通过请求的 URI,解析域名,建立TCP连接成功后,浏览器发起http请求。
1.2 服务器处理请求并构建响应
服务器收到请求后,检索指定资源,通过设置响应头字段Content-Type(指示资源的MIME类型,告诉客户端如何解析和显示内容),Content-Disposition(设置浏览器行为,指示内容的展示形式,可以是内联显示或附件下载),Content-Length(文件字节大小),Accept-Ranges(表示可用于定义范围的单位,向客户端宣传其对文件下载的部分请求,浏览器可能会尝试恢复中断的下载)。最后返回状态码。常用的状态码有200 OK(请求成功),400 Bad Request(服务器无法理解请求),401 Unauthorized(未授权),404 Not Found(资源未找到),408 Request Time-out(请求超时),415 Unsupported Media Type(不支持的媒体类型),500 Internal Server Error(服务器内部错误),502 Bad Gateway(服务器之间的通信存在问题)。
1.3 客户端接受文件
服务器将文件以 字节流 形式写入响应体,逐块传输给客户端(如 Content-Type: application/octet-stream)。客户端解析响应头,解析上文提到的 Content-Disposition,Content-Length等,将响应体数据以何种方式保存
何种行为展示
。
二、代码实现
2.1 实现背景
数据批量导入导出是一个在工作中十分常见的业务,这里讨论从服务器中下载导入数据Excel模板这种情况,该模板是一种已经存在的资源文件。
2.2 .NET 8 实现流程
找到文件目录,通过Path.Combine结合项目根目录与资源文件夹和资源名称。
string fileName = "上传数据模板.xlsx";
string basePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot","static");
string filePath = Path.Combine(basePath, fileName);
验证文件
if (!System.IO.File.Exists(filePath))
{
return NotFound("文件未找到");
}
设置文件类型,默认字节流(octet-stream)
var contentTypeProvider = new FileExtensionContentTypeProvider();
if (!contentTypeProvider.TryGetContentType(fileName, out var contentType))
{
contentType = "application/octet-stream";
}
设定文件名称并返回文件
PhysicalFile 是直接指定文件的物理路径(绝对路径),而 File 方法有其他重载,比如使用相对路径,或者从流中读取。使用PhysicalFile 要确认地址访问的安全性,防止目录遍历攻击。
string encodedFileName = WebUtility.UrlEncode(fileName, Encoding.UTF8);
return PhysicalFile(filePath, contentType, encodedFileName);
2.3 .NET Framework 实现流程
找到文件目录,通过Path.Combine结合项目根目录与资源文件夹和资源名称。
// 获取应用程序的基目录
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
// 相对路径
string relativePath = Path.Combine("Comm", "Excel", "上传数据模板.xlsx");
// 拼接完整路径
string fullPath = Path.Combine(baseDirectory, relativePath);
找到指定的资源文件
FileInfo fi = new FileInfo(fullPath);
if (!fi.Exists)
{
HttpContext.Current.Response.StatusCode = 404;
HttpContext.Current.Response.Write("文件未找到");
return;
}
设定响应头并写入文件到响应体
HttpContext.Current.Response.Clear();
// //当要下载的文件名是中文时,需加上HttpUtility.UrlEncode
HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(fi.Name));
HttpContext.Current.Response.AddHeader("Content-Length", fi.Length.ToString());
HttpContext.Current.Response.ContentType = "application/octet-stream";
HttpContext.Current.Response.WriteFile(fi.FullName);
HttpContext.Current.Response.End();
三、完整代码
3.1 .NET8 代码实现
[HttpGet]
public IActionResult DownloadExcel()
{
string fileName = "上传数据模板.xlsx";
string basePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot","static");
string filePath = Path.Combine(basePath, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound("文件未找到");
}
// 获取文件类型
var contentTypeProvider = new FileExtensionContentTypeProvider();
if (!contentTypeProvider.TryGetContentType(fileName, out var contentType))
{
contentType = "application/octet-stream";
}
// 显式指定编码
string encodedFileName = WebUtility.UrlEncode(fileName, Encoding.UTF8);
return PhysicalFile(filePath, contentType, encodedFileName);
}
3.2 .NET Framework 代码实现
public void DownloadExcel()
{
try
{
// 获取应用程序的基目录
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
// 相对路径
string relativePath = Path.Combine("Comm", "Excel", "上传数据模板.xlsx");
// 拼接完整路径
string fullPath = Path.Combine(baseDirectory, relativePath);
FileInfo fi = new FileInfo(fullPath);
if (!fi.Exists)
{
HttpContext.Current.Response.StatusCode = 404;
HttpContext.Current.Response.Write("文件未找到");
return;
}
HttpContext.Current.Response.Clear();
// //当要下载的文件名是中文时,需加上HttpUtility.UrlEncode
HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(fi.Name));
HttpContext.Current.Response.AddHeader("Content-Length", fi.Length.ToString());
HttpContext.Current.Response.ContentType = "application/octet-stream";
HttpContext.Current.Response.WriteFile(fi.FullName);
HttpContext.Current.Response.End();
}
catch (Exception ex)
{
HttpContext.Current.Response.StatusCode = 500;
HttpContext.Current.Response.Write(ex.Message);
}
}