C#上传文件显示进度条

本文共有14840个字,关键词:文件上传.netc#进度条

前几天阿里电话面试时候被问到如何实现上传文件,前端显示进度条。当时思路不够清晰,支支吾吾答不上来。现在回过头来实现一次。

发现有两种方法,首先讲一下思路:

  1. 我这里用MVC的框架实现,前端通过Ajax上传文件到/Home/UploadFile,控制器接收文件并以字节的形式一步步写入磁盘,期间将已经写入的百分比存到缓存中(也可以是Session,memcahe,数据库中),然后前端在上传文件的同时启动一个计数器,不断的向服务器请求获取缓存中的百分比,以此来操作进度条的显示。

  2. 第一种方法虽然可行而且思路简单粗暴,但是性能不好和实现起来很麻烦。通过了解发现,XMLHttpRequest 还支持一个progress事件。思路:Ajax上传文件是,用xhr的progress事件直接可以获得已上传的大小,从而操作进度条的显示。

  3. webSockets可以与服务器全双工通信,但目前不是很了解,但用webSocket来实现进度条应该也是一种很好的方法,有机会一定尝试一下。
  4. 还了解到可以用Promise的类库,第三个参数notify。(只是了解,来源http://www.alloyteam.com/2014/05/javascript-promise-mode/


我用前两种方式都实现了一次,后面会对比一下两种方法的优缺点。

第一种方法

用户访问Home/Index页面,服务器返回一个上传的界面。

        //控制器
        public ActionResult Index()
        {
            //用一个guid作为标识客户端的用户
            string clientId = System.Guid.NewGuid().ToString("N");
            //用cookie保存用户身份
            HttpCookie cookie = new HttpCookie("clientId",clientId);
            cookie.HttpOnly = true;
            cookie.Expires = DateTime.Now.AddHours(2);
            Response.Cookies.Add(cookie);
            return View();
        }

这里说明一下为什么需要标识用户身份:因为服务器缓存是键值对的形式,服务器把用户的标识作为键,就可以区分具体是哪个用户上传的文件数据。标识也可以是form表单中的一个隐藏的字段、用户的session或者用户名,我这里用Cookie,当用户提交http请求的时候服务器可以获取该用户的cookie信息,从而识别用户身份。(一般上传文件应该是一个已经登录的用户发起的,所以一般场景中应该是已经有了可以标识用户身份的字段的,比如用户名,可以直接拿来用,不需再额外标识,我这里为了演示没有登录,所以需要自己创建一个标识)

    <style>
        .bar {
            border: 1px solid #000;
            width: 300px;
            height: 20px;
            background: #fff;
        }

        .current-bar {
            width: 1%;
            height: 100%;
            background: green;
        }
    </style>
<form>
    <input type="text" name="title" id="title" value="0" />
    <input type="file" name="file" id="file" value="" />
    <input type="button" id="btnUpload" value="提交" />
    <div class="bar">
        <div class="current-bar" id="current-bar"></div>
    </div>
</form>

<script>
    var btn = document.getElementById("btnUpload");
    //var isSucess = false;//文件上传是否成功的标记,来决定是否中断定时器
    
    btn.addEventListener("click", function () {
        var file = document.getElementById('file').files[0];
        if (file===null) {
            alert("请先选择文件!");
            return false;
        }

        //new一个formdata对象并将文件添加到对象中
        var formData = new FormData();
        formData.append("file", file);
        //formData.append("title", title);
        //ajax开始,new xhr对象
        var request;
        if (window.XMLHttpRequest) {
            request = new XMLHttpRequest();
        }
        else {
            request = new ActiveXObject('Microsoft.XMLHTTP');
        }
         //开启定时器询问服务器已上传的字节
         var timer = setInterval(checkProgress, 1000);

        //请求发送后处理函数
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    //成功
                     clearInterval(timer);
                    console.log("上传成功");
                }
                else {
                    console.log(request.status);
                }
            }
            else {
                //请求还在继续
            }
        }
        request.open('POST', '/Home/UploadFile', true);
        request.send(formData);
    })

    ///查询服务器的
    function checkProgress() {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            }
            else {
                xhr = new ActiveXObject("Microsoft.XMLHTTP");
            }
            xhr.onreadystatechange = function () {
                var bar = document.getElementById("current-bar");
                if (xhr.readyState == 4) {
                    if (xhr.status == 200) {
                        var response = JSON.parse(xhr.responseText);
                        if (response.success) {
                            //已经全部上传
                            console.log("进度:" + response.progress);
                            bar.style.width = response.progress + "%";
                        }
                        else {
                            //还没全部上传
                            console.log("进度:" + response.progress);
                            bar.style.width = response.progress + "%";
                        }
                    }
                }
            }
            xhr.open("GET", "/Home/GetProgress");
            xhr.send();
    }
</script>

Home控制器的处理

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
using UploadDemo.Common;
namespace UploadDemo.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/
        public ActionResult Index()
        {
            //用一个guid作为标识客户端的用户
            string clientId = System.Guid.NewGuid().ToString("N");
            //用cookie保存用户身份
            HttpCookie cookie = new HttpCookie("clientId",clientId);
            cookie.HttpOnly = true;
            cookie.Expires = DateTime.Now.AddHours(2);
            Response.Cookies.Add(cookie);
            return View();
        }
        [HttpPost]//上传文件的处理
        public string UploadFile()
        {
            //获取请求的文件
            HttpPostedFileBase file = Request.Files["file"];
            if (file.ContentLength <= 0)
            {
                return "{\"sucess\":0,\"message\":\"文件大小为0\"}";
            }
            //获得用户的身份
            string clientId = Request.Cookies["clientId"].Value;

            //用一个guid作为保存在文件夹中的文件名,防止重复上传同一文件造成重名
            string fileSaveName = System.Guid.NewGuid().ToString("N");
            //文件保存的路径,根目录下的Uploads文件夹
            string fileSavePath = HttpRuntime.AppDomainAppPath + "/Uploads/"+fileSaveName+Path.GetExtension(file.FileName);
            Stream stream=file.InputStream;
            byte[] buffer;
            //每次上传的字节
            int bufferSize = 4096;
            //总大小
            long totalLength = stream.Length;
            long writterSize = 0;//已上传的文件大小
               //保存在缓存中的进度
            object caheObj=new object();
            using (FileStream fs = new FileStream(fileSavePath, FileMode.Create))
            {
                while (writterSize < totalLength)
                {
                    if (totalLength - writterSize >= bufferSize)
                    {
                        buffer = new byte[bufferSize];
                    }
                    else
                    {
                        buffer = new byte[totalLength - writterSize];
                    }
                    //读取上传的文件到字节数组
                    stream.Read(buffer, 0, buffer.Length);
                    //写入文件流
                    fs.Write(buffer, 0, buffer.Length);
                    writterSize += buffer.Length;
                    //把上传的百分比写入到服务器缓存中
                    caheObj="{\"success\":0,\"progress\":\""+(writterSize*100/totalLength).ToString()+"\"}";
                    ////每循环一次都更新一次缓存的内容
                    Common.CaheHelper.SetCache("file"+clientId,caheObj);   
                    
                    Thread.Sleep(2000);//为了看到明显的过程故意暂停 
                }
                //退出循环后,说明已经上传完成了,进度是100
                caheObj = "{\"success\":1,\"progress\":\"100\"}";
                Common.CaheHelper.SetCache("file" + clientId, caheObj);   
            }
            
            return "{\"sucess\":1,\"message\":\"已经上传成功了\"}";
        }
        ///处理用户询问进度的方法
        public string GetProgress()
        {
             //获得用户的身份
            string clientId = Request.Cookies["clientId"].Value;
            if(string.IsNullOrEmpty(clientId))
            {
                return "{\"success\":0,\"progress\":\"0\"}";
            }
            //根据用户标识获得了缓存中的进度
            object obj = Common.CaheHelper.GetCache("file" + clientId);
            if (obj != null)
            {
                return obj.ToString();
            }
            else
            {
                return "{\"success\":0,\"progress\":\"0\"}";
            }
        }
    }
}

因为用到了缓存,这里需要封装一个CaheHelper的类来操作缓存

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace UploadDemo.Common
{
    public class CaheHelper
    {
        /// <summary>
        /// 获取数据缓存
        /// </summary>
        /// <param name="CacheKey">键</param>
        public static object GetCache(string CacheKey)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            return objCache[CacheKey];
        }

        /// <summary>
        /// 设置数据缓存
        /// </summary>
        public static void SetCache(string CacheKey, object objObject)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            objCache.Insert(CacheKey, objObject);
        }

        /// <summary>
        /// 设置数据缓存滑动过期
        /// </summary>
        public static void SetCache(string CacheKey, object objObject, TimeSpan Timeout)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            objCache.Insert(CacheKey, objObject, null, DateTime.MaxValue, Timeout, System.Web.Caching.CacheItemPriority.NotRemovable, null);
        }

        /// <summary>
        /// 设置缓存
        /// </summary>
        /// <param name="CacheKey"></param>
        /// <param name="objObject"></param>
        /// <param name="absoluteExpiration">绝对过期时间</param>
        /// <param name="slidingExpiration">滑动过期时间</param>
        public static void SetCache(string CacheKey, object objObject, DateTime absoluteExpiration, TimeSpan slidingExpiration)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            objCache.Insert(CacheKey, objObject, null, absoluteExpiration, slidingExpiration);
        }

        /// <summary>
        /// 移除指定数据缓存
        /// </summary>
        public static void RemoveAllCache(string CacheKey)
        {
            System.Web.Caching.Cache _cache = HttpRuntime.Cache;
            _cache.Remove(CacheKey);
        }

        /// <summary>
        /// 移除全部缓存
        /// </summary>
        public static void RemoveAllCache()
        {
            System.Web.Caching.Cache _cache = HttpRuntime.Cache;
            IDictionaryEnumerator CacheEnum = _cache.GetEnumerator();
            while (CacheEnum.MoveNext())
            {
                _cache.Remove(CacheEnum.Key.ToString());
            }
        }
    }
}


第二种方法

这种方法是直接在前端完成,不需要标识用户身份也不需要询问服务器进度,它的进度是表示把文件从客户端全部发送到服务器后的进度,并不关心服务器的处理情况。(会出现进度条已经100%,而服务器还没完全写入磁盘的情况)

    <style>
        .bar {
            border: 1px solid #000;
            width: 300px;
            height: 20px;
            background: #fff;
        }

        .current-bar {
            width: 1%;
            height: 100%;
            background: green;
        }
    </style>
<form>
    <input type="text" name="title" id="title" value="0" />
    <input type="file" name="file" id="file" value="" />
    <input type="button" id="btnUpload" value="提交" />
    <div class="bar">
        <div class="current-bar" id="current-bar"></div>
    </div>
    
    <progress id="uploadprogress" min="0" max="100" value="0">0</progress>
</form>
<script>
    var btn = document.getElementById("btnUpload");
    btn.addEventListener("click", function () {
        var file = document.getElementById('file').files[0];
        if (file===null) {
            alert("请先选择文件!");
            return false;
        }
        //new一个formdata对象并将文件添加到对象中
        var formData = new FormData();
        formData.append("file", file);
        //formData.append("title", title);
        //ajax开始,new xhr对象
        var request;
        if (window.XMLHttpRequest) {
            request = new XMLHttpRequest();
        }
        else {
            request = new ActiveXObject('Microsoft.XMLHTTP');
        }
        //progress事件
        request.upload.addEventListener("progress", uploadProgress, false);

        //请求发送后处理函数
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                
                if (request.status === 200) {
                    //成功
                    console.log("成功了");
                }
                else {
                    console.log(request.status);
                }
            }
            else {
                //请求还在继续
            }
            
            
        }
        request.open('POST', '/Home/UploadFile', true);
        request.send(formData);
        
    })
    

    function uploadProgress(evt) {
        if (evt.lengthComputable) {
            //evt.loaded:文件上传的大小   evt.total:文件总的大小                      
            var percentComplete = Math.round((evt.loaded) * 100 / evt.total);
            //加载进度条,同时显示信息            
            var bar = document.getElementById("current-bar");
            bar.style.width = percentComplete + "%";
        }
    }
  
</script>

控制器处理

  [HttpPost]
        public string UploadFile()
        {
            //获取请求的文件
            
            HttpPostedFileBase file = Request.Files["file"];
            if (file.ContentLength <= 0)
            {
                return "{\"sucess\":0,\"message\":\"文件大小为0\"}";
            }
            //用一个guid作为保存在文件夹中的文件名,防止重复上传同一文件造成重名
            string fileSaveName = System.Guid.NewGuid().ToString("N");
            //文件保存的路径,根目录下的Uploads文件夹
            string fileSavePath = HttpRuntime.AppDomainAppPath + "/Uploads/"+fileSaveName+Path.GetExtension(file.FileName);

            if (File.Exists(fileSavePath))
            {
                return "{\"sucess\":0,\"message\":\"文件已存在\"}";
            }
               ///可以直接保存,不用关心流的问题
            file.SaveAs(physicalPath);
            
            return "{\"sucess\":1,\"message\":\"已经上传成功了\"}";
        }



总结

对比两种方法,其实是很不同的:

  1. 第一种方法忽略了一个很重要的问题:当用户点击上传的时候,文件会尽可能快的上传到服务器上,服务器捕获文件保存在内存中,但还没操作保存到磁盘上,此时对于客户端来说其实应该表示上传的文件已经成功了的。而服务器什么时候能够处理完内存中的文件,客户端应该不用关心了的。第一种方法实际上表现的是服务器写入磁盘的进度,而不是通过网络上传的进度。
  2. 第一种方法中的进度条显示会是一段一段的,可能从0%直接到40%再100%,是有级的。当网络卡顿可能会延迟的很厉害,多次Ajax可能会服务器压力,把进度信息保存到内存、session、memcahe也消耗服务器内存。
  3. 第二种方法的进度表示的是文件完全从客户端发送到服务器的时间,这应该是真正的网络上传进度,不关心文件在服务器上如何处理。网速快的话可能会非常快的达到进度100%,而此时服务器可能还在保存文件到磁盘,可能过了很久后才返回保存成功的结果。

「一键投喂 软糖/蛋糕/布丁/牛奶/冰阔乐!」

fengxianqi

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码完成支付

版权声明:本文为作者原创,如需转载须联系作者本人同意,未经作者本人同意不得擅自转载。
添加新评论
暂无评论