4

C#上传文件显示进度条

 3 years ago
source link: https://www.fengxianqi.com/index.php/archives/11/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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%,而此时服务器可能还在保存文件到磁盘,可能过了很久后才返回保存成功的结果。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK