Netcore webapi + 后端多文件多参数 multipart/form-data 上传

这次是因为项目上需要用到多文件上传功能,需求是有一个winfrom程序要上传多个文件给netcore webapi 并且上传接口要能够支持多个参数的传递方式;
期间也遇到了很多问题,随手记录一下,方便自己也也方便他人;
首先第一步要搞清楚什么是 multipart/form-data 格式上传  ;
这里我写了个表单,然后用抓包工具来抓取协议;
表单提交页面如下:
Netcore webapi + 后端多文件多参数 multipart/form-data 上传_第1张图片
抓包的请求头信息 
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryagmh7QIcWZuIziUf
----WebKitFormBoundaryagmh7QIcWZuIziUf #这是用来做边界的我称之为分界符;
Content-Type: multipart/form-data; #这就是提交的方式啦;
Netcore webapi + 后端多文件多参数 multipart/form-data 上传_第2张图片

提交后抓包的总体结构信息信息
Netcore webapi + 后端多文件多参数 multipart/form-data 上传_第3张图片
由图可见参数格式是如何构成的;

不难看出  ------WebKitFormBoundaryagmh7QIcWZuIziUf    这是个分界线用于和下一个参数数据分隔;

每一行参数格式定义为 如下图: 
------WebKitFormBoundaryagmh7QIcWZuIziUf    【换行】 #分界符
Content-Disposition: form-data; name="参数字段名称"  【换行】
【换行】

Netcore webapi + 后端多文件多参数 multipart/form-data 上传_第4张图片

下面看看代码如何实现的 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)



到此结束了,第一次写技术文章, 欢迎各界大佬的批评指正。


 

你可能感兴趣的:(C#,多文件上传,c#,后端)