这次是因为项目上需要用到多文件上传功能,需求是有一个winfrom程序要上传多个文件给netcore webapi 并且上传接口要能够支持多个参数的传递方式;
期间也遇到了很多问题,随手记录一下,方便自己也也方便他人;
首先第一步要搞清楚什么是 multipart/form-data 格式上传 ;
这里我写了个表单,然后用抓包工具来抓取协议;
表单提交页面如下:
抓包的请求头信息
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryagmh7QIcWZuIziUf
----WebKitFormBoundaryagmh7QIcWZuIziUf #这是用来做边界的我称之为分界符;
Content-Type: multipart/form-data; #这就是提交的方式啦;
提交后抓包的总体结构信息信息
由图可见参数格式是如何构成的;
不难看出 ------WebKitFormBoundaryagmh7QIcWZuIziUf 这是个分界线用于和下一个参数数据分隔;
每一行参数格式定义为 如下图:
------WebKitFormBoundaryagmh7QIcWZuIziUf 【换行】 #分界符
Content-Disposition: form-data; name="参数字段名称" 【换行】
【换行】
下面看看代码如何实现的 api服务端代码
///
/// 订单保存对象
///
public class OrderSaveEntity : Add_ChenryOrder_Model
{
public List Files { get; set; }
}
///
/// 提交保存订单并上传信息
///
///
///
[HttpPost, DisableRequestSizeLimit] //DisableRequestSizeLimit 上传文件不限制文件大小
public async Task SaveOrder([FromForm] OrderSaveEntity m)
{
var files = m.Files;
//base._httpContextAccessor.HttpContext.Request.Form.Files; 你也可以不用定义 List 直接用这个方式获取上传的文件,但需要注入 httpContextAccessor 对象
if (m == null) return Result.Error("上传内容错误");
if (!files.Any()) return Result.Error("没有检测到上传数据");
string webroot = hostingEnvironment.WebRootPath;//拿到 wwwroot 路径
string spac = Path.DirectorySeparatorChar.ToString();//不同的系统会有不同的目录盘符 如 微软的系统为\ LINXU系统为/
//定义存储路径
string localpath = $"{spac}{token.UserId}{spac}{m.BuessID}{spac}";//自己定义的存储目录结构 相对路径【看个人业务,如不需要请忽略,我这里是需要存相对路径的】
string path = $"{webroot}{spac}{localpath}"; //绝对存储路径
//批量上传
files.ForEach(x =>
{
string ext = Path.GetExtension(x.FileName);//文件后缀
string filename = Guid.NewGuid().ToString("N");//自定义文件名
var fullsavepath = $"{path}{filename}{ext}";
FileUpLoad(x, fullsavepath);
});
m.Source = localpath; //路径
m.Uid = base.Token.UserId;//用户ID
m.FileCount = fcount;//文件数量
await chenryOrderService.SaveData(m);//提交业务层保存
return Result.Ok($"上传成功!总计:{ files.Count}个文件。");
}
///
/// 通用上传方法 PS : 该方法不建议使用 Task 方式,以免引发多个文件上传线程对流的管道被共享报错
///
/// IFormFile 对象
/// 保存的文件完整物理路径
[NonAction]
public void FileUpLoad(IFormFile file, string savepath)
{
var index = savepath.LastIndexOf(Path.DirectorySeparatorChar);//拿到最后一个目录位置
var dir = savepath.Substring(0, index);//截取到最后一个目录位置
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); //判断目录是否创建不存在则创建
//从IFromFile文件流上载到服务器目录上
using (var stream = new FileStream(savepath, FileMode.Create))
{
file.CopyTo(stream);
}
}
C#客户端上传代码:
public const string ApiSite = "http://localhost:56253";//接口域名
public const string Token = "U0pfVG9rZW5fMjRiZGViYjNmZmRlNGYxOGI0MTZkNzhkMTFjNDlmZWU="; //测试token
static void Main(string[] args)
{
//即将上传的文件根路径
string filepath = Path.Combine(AppContext.BaseDirectory, "files");
//键值对象参数后续提交使用 【文本型参数 PS 测试只加了必填项】
Dictionary kys = new Dictionary();
kys.Add("BuessID", "1505022b92ec482782f11f2a4108f003");
kys.Add("OrderNum", "2020402193203958");
kys.Add("Name", "测试一下有没有更新");
//文件参数键值对象 key 文件名 value 文件所在本地路径
Dictionary files = new Dictionary();
for (int i = 1; i <= 3; i++) //上传模型3个 图片 + 模型文件
{
files.Add($"{i}.jpg", $"{Path.Combine(filepath, i.ToString())}.jpg");//key :图片名 value :图片路径
files.Add($"{i}.obj", $"{Path.Combine(filepath, i.ToString())}.obj");//key :文件名 value :文件路径
}
//上传接口地址
string url = $"{ApiSite}/DesignOrder/SaveOrder";
var webRequest = HttpWebRequest.Create(url);
webRequest.Method = "POST"; //POST提交方式
webRequest.Timeout = 60000; //请求超时时间
webRequest.Headers.Add("token", Token); // ** token 必填
// 边界符 定义
var boundary = "------WebKitFormBoundary" + DateTime.Now.Ticks.ToString("x");
webRequest.ContentType = "multipart/form-data; boundary=" + boundary; //form-data 形式的请求头类型
//用于打印的可以忽略
string start = "--" + boundary + "\r\n";
string end = "--" + boundary + "--\r\n";
string newline = "\r\n";
//写入流的固定格式值
var beginBoundary = Encoding.ASCII.GetBytes("--" + boundary + "\r\n"); // 开始边界符
var endBoundary = Encoding.ASCII.GetBytes("--" + boundary + "--\r\n"); // 结束结束符
var newLineBytes = Encoding.UTF8.GetBytes("\r\n"); //换行符
//区分上传类型 获得 content 类型
Func getcontent = (s) =>
{
var ex = Path.GetExtension(s); //拿到后缀
return FileContentType.GetMimeType(ex); //请求后缀返回对应的 type类型
//PS 获取Content-Type 的 三种方法传送门:https://blog.csdn.net/a873744779/article/details/100514010
};
using (var stream = new MemoryStream())
{
// 写入字段参数
var keyValue = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
// 装载form表单字段【非上传文件字段】
foreach (string key in kys.Keys)
{
//参数字段转字节
var keyValueBytes = Encoding.UTF8.GetBytes(string.Format(keyValue, key, kys[key]));
stream.Write(beginBoundary, 0, beginBoundary.Length);//写入边界开始
stream.Write(keyValueBytes, 0, keyValueBytes.Length);//写入字节
stream.Write(newLineBytes, 0, newLineBytes.Length);//写入换行符
//打印日志
Console.Write(start);
Console.Write(string.Format(keyValue, key, kys[key]));
}
//多文件上传
foreach (var item in files)
{
var fileData = File.ReadAllBytes(item.Value); //读文件流
// 写入文件 name = \"Files\" 这里对应的是接口参数 List Files
var fileHeader = "Content-Disposition: form-data; name=\"Files\"; filename=\"" + item.Key + "\"\r\n"
+ "Content-Type: " + getcontent(item.Key) + "\r\n\r\n";
var fileHeaderBytes = Encoding.UTF8.GetBytes(fileHeader);
stream.Write(beginBoundary, 0, beginBoundary.Length);// 写入开始边界
stream.Write(fileHeaderBytes, 0, fileHeaderBytes.Length);//写入文件格式对象流
stream.Write(fileData, 0, fileData.Length); // 写入文件流对象
stream.Write(newLineBytes, 0, newLineBytes.Length);//写入换行符
//打印日志
Console.Write(start);
Console.Write(fileHeader);
Console.Write(newline);
}
Console.WriteLine(end);//打印结束边界
// 写入结束边界符
stream.Write(endBoundary, 0, endBoundary.Length);
webRequest.ContentLength = stream.Length;//设置请求对象,字节长度总量
stream.Position = 0;//指定流开始索引
var tempBuffer = new byte[stream.Length];//定义新字节流对象,用于提交请求结果
stream.Read(tempBuffer, 0, tempBuffer.Length); //从0开始索引读流到新 tempbuffer 对象
//请求结果流
using (Stream requestStream = webRequest.GetRequestStream())
{
requestStream.Write(tempBuffer, 0, tempBuffer.Length);//写入请求流
using (var response = webRequest.GetResponse())//请求结果流
using (StreamReader httpStreamReader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) //从结果流对象中读取结果流并设置流格式转换为 uft8 格式
{
string result = httpStreamReader.ReadToEnd();//返回服务器返回json
Console.WriteLine(result);//输出结果 如有需要请 自行 json 序列化返回结果
}
}
}
}
另外可能会遇到上传被服务器拒绝的问题 因为文件太大了
下面是解除限制方法:
Startup.CS 设置
public void ConfigureServices(IServiceCollection services)
{
//上传文件不做限制可以上传最大
services.Configure(options =>
{
options.ValueLengthLimit = int.MaxValue;
options.MultipartBodyLengthLimit = long.MaxValue;
options.MemoryBufferThreshold = int.MaxValue;
});
}
Program.cs 设置
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
//设置应用服务器Kestrel请求体最大
options.Limits.MaxRequestBodySize = long.MaxValue;
});
webBuilder.UseStartup().UseUrls("http://*:5000;");
});
}
还有一种方式是在 Conntroller 的 Action 上打标记
///
/// 提交保存订单并上传信息
///
///
///
[HttpPost, DisableRequestSizeLimit] //DisableRequestSizeLimit 上传文件不限制文件大小
public async Task SaveOrder([FromForm] OrderSaveEntity m)
到此结束了,第一次写技术文章, 欢迎各界大佬的批评指正。