基於SignalR的超線程上載器


記得以前做過一個東西,就是當數據庫有數據更新的時候,能夠自動更新到前台,那時候signalr還沒出現的時候,需要自己實現long pooling, 比較痛苦,反正是最終做完,效果也不是多么理想. 沒想到最近幾天發現了SignalR這個開源的東西,並且,它居然還被.net 4.0收錄了. 懷着對實時交互性能的興趣,於是便誕生了本文.

效果演示

下面我們先來看看演示(四個文件,前三個大小差不多,都為10MB左右,最后一個為400MB)(本演示在Firefox以及Chrome下演示通過,在IE7及其以下版本未通過.):

 

看到了吧,多線程下載加上實時的通知功能,讓webui變得非常不一般了.這也得益於Signalr將long pooling方式封裝的非常好用,所以才會如此簡便.

那么,該如何來做呢?

實現方式

首先,我們需要安裝SignalR包,這個微軟都已經提供好了,我們需要用到的是VS2010的Package manager  console窗體,可以在Tools > library package manager處打開. 在使用這個工具之前,我們要確保機器已經安裝了powershell 2.0,這個大家都知道怎么安裝的.

安裝完畢以后,創建一個新的Web項目,然后請打開Package manager  console,然后輸入Install-Package Microsoft.AspNet.SignalR, 然后就等着安裝把,安裝完畢以后,項目就變成了這個樣子了.

從圖中我們可以看到微軟自動為我們引用了SignalR的類庫和一堆的Javascript文件.好了,一切都准備好了,下面開工.

這里,我們先要在Global.asax中進行下路徑映射: RouteTable.Routes.MapHubs();

這段代碼需要放到application_start中。

然后我們創建一個類LetsChat.cs,然后這個類需要繼承自Hub類,在類里面,我們需要實現send方法,為什么方法名字叫做send呢?這是一個約定. 然后我為這個類加上名稱   [HubName("myChatHub")],那么前台js就可以通過這個hubname來訪問類方法. 以下就是類里面具體的實現方式,大家不妨展開看一看,反正就是首先解析出文件路徑,然后利用APM模式異步的利用文件流方式進行文件上傳操作.

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR.Hubs;
using System.Threading;
using System.IO;
using System.Reflection;

namespace SignalRChat
{
    [HubName("myChatHub")]
    public class LetsChat : Hub
    {
        public void send(string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                Clients.All.addMessage("文件內容為空,請檢查!!");
                return;
            }

            int fileCount = 0;
            

            if (message.Contains("|"))
                fileCount = message.Split('|').Length;
            else
                fileCount = 1;

            string[] fileCollection = new string[fileCount];
            if (fileCount > 1)
                fileCollection = message.Split('|');
            else
                fileCollection[0] = message;


            string uploadPath = AppDomain.CurrentDomain.BaseDirectory;
            int fileFlag = 0;

            foreach (string filename in fileCollection)
            {
                if (File.Exists(filename))
                {
                    string newName = Path.Combine(uploadPath,"Upload",FileWithOutExtension(filename));
                    if (File.Exists(newName))
                        try
                        {
                            File.Delete(newName);
                        }
                        catch (Exception ex)
                        {
                            Clients.All.addMessage(ex.Message);
                        }
                    parameterCollection p = new parameterCollection();
                    p.filename = filename;
                    p.newName = newName;
                    p.eachLoopSize = 2048;
                    p.fileFlag = fileFlag;

                    //Thread t = new Thread(new ParameterizedThreadStart(CopyFilesAsync));
                    //t.IsBackground = true;
                    //t.Start((object)p);
                    BeginCopy(p);
                    
                    fileFlag++;
                    
                }
            }
        }

        private void BeginCopy(object obj)
        {
            try
            {
                parameterCollection pCollection = (parameterCollection)obj;
                Clients.All.addMessage("Start to copy " + pCollection.filename+ " now...");
                Action<object> actionStart = new Action<object>(CopyFilesAsync);
                actionStart.BeginInvoke(obj, new AsyncCallback(iar =>
                {
                    Action<object> actionEnd = (Action<object>)iar.AsyncState;
                    actionEnd.EndInvoke(iar);
                    Clients.All.addMessage("Copied " + pCollection.filename + " ok...");
                }), actionStart);
            }
            catch (Exception ex)
            {
                Clients.All.addMessage(ex.Message);
            }
        }

        private struct parameterCollection
        {
            public string filename;
            public string newName;
            public int eachLoopSize;
            public int fileFlag;
        }

        private void CopyFilesAsync(object obj)
        {
            parameterCollection objConvert = (parameterCollection)obj;
            CopyFile(objConvert.filename, objConvert.newName, objConvert.eachLoopSize,objConvert.fileFlag);
        }

        

        ///<summary>
        ///復制文件
        ///</summary>
        ///<param name="fromFile">要復制的文件</param>
        ///<param name="toFile">要保存的位置</param>
        ///<param name="lengthEachTime">每次復制的長度</param>
        private void CopyFile(string fromFile, string toFile, int lengthEachTime,int fileFlag)
        {
            FileStream fileToCopy = null;
            try{fileToCopy = new FileStream(fromFile, FileMode.Open, FileAccess.Read);}
            catch (Exception ex) { Clients.All.addMessage(ex.Message); return; }

            FileStream copyToFile = null;
            try { copyToFile = new FileStream(toFile, FileMode.Append, FileAccess.Write); }
            catch (Exception ex) { Clients.All.addMessage(ex.Message); return; }

            string fileFlagStr = fileFlag.ToString();
            int lengthToCopy;
            int pauseCount=0; //主要是進行計數,然后調用Thead.sleep來是界面滑行更加流暢

            if (lengthEachTime < fileToCopy.Length)//如果分段拷貝,即每次拷貝內容小於文件總長度
            {
                byte[] buffer = new byte[lengthEachTime];
                int copied = 0;
                while (copied <= ((int)fileToCopy.Length - lengthEachTime))//拷貝主體部分
                {
                    lengthToCopy = fileToCopy.Read(buffer, 0, lengthEachTime);
                    fileToCopy.Flush();
                    copyToFile.Write(buffer, 0, lengthEachTime);
                    copyToFile.Flush();
                    copyToFile.Position = fileToCopy.Position;
                    copied += lengthToCopy;

                    //send to front UI
                    string sendSizeCurrent = ((double)copied / (double)fileToCopy.Length).ToString();
                    Clients.All.addMessage(fileFlagStr + "|" + sendSizeCurrent);
                    pauseCount++;
                    if (pauseCount % 3 == 0)
                        Thread.Sleep(1); //加上這個很重要,主要是讓流能夠有足夠的事件寫入,我們可以控制這里來讓PrograssBar滑行的更流暢
                }
                int left = (int)fileToCopy.Length - copied;//拷貝剩余部分
                lengthToCopy = fileToCopy.Read(buffer, 0, left);
                fileToCopy.Flush();
                copyToFile.Write(buffer, 0, left);
                copyToFile.Flush();

                Clients.All.addMessage(fileFlagStr + "|" + 1);
            }
            else//如果整體拷貝,即每次拷貝內容大於文件總長度
            {
                byte[] buffer = new byte[fileToCopy.Length];
                fileToCopy.Read(buffer, 0, (int)fileToCopy.Length);
                fileToCopy.Flush();
                copyToFile.Write(buffer, 0, (int)fileToCopy.Length);
                copyToFile.Flush();

                Clients.All.addMessage(fileFlagStr + "|" + 1);
            }
            fileToCopy.Close();
            copyToFile.Close();
            Thread.Sleep(10);
        }


        private string FileWithOutExtension(string filePath)
        {
            if (filePath.Contains(@"\"))
                return filePath.Substring(filePath.LastIndexOf(@"\") + 1);

            if(filePath.Contains(@"/"))
                return filePath.Substring(filePath.LastIndexOf(@"/") + 1);
            return filePath;
        }
    }
}

需要注意的是,在這里,我們可以利用Clients.All.addMessage(Message);來向前台打印出消息而不用刷新頁面. 所以說,有了這個,我們就可以刷新進度,實時通知了.

那么前台該怎么弄呢?

首先,在chat.aspx頁面,我引入如下的外部文件:

View Code
<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR-1.0.0-rc1.js" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
<link href="Css/main.css" rel="stylesheet" type="text/css" />

記住的是, <script src="signalr/hubs" type="text/javascript"></script>一定要引用,雖然說文件並不存在.並且這個文件要放在jquery文件和signalR文件后面.

然后在chat.aspx頁面,我也輸入如下的代碼:

View Code
$(function () {
        //創建鏈接的實例
            var IWannaChat = $.connection.myChatHub;
            var count = 0;

            //瀏覽文件
            $("#btnBrowse").bind("click", function () {
                $("#fileBrowe").click();
                $("#fileBrowe").bind("change", function () {
                    var path = $(this).val();
                    if (path != null && path != "") {
                    //當選擇好文件以后,就將文件路徑信息加入到UI中.
                        $('#listFiles').append('<tr><td id="fileNameSpecific">' + path + '</td><td id="myPrograss' + (count) + '" "></td><td id="myState' + count + '">Ready</td></tr>');
                        count++;
                        preventDefault();
                    }
                });
            });

            //點擊上傳按鈕,將文件名稱用豎線分割,然后發送到后台
            $("#btnUpload").bind("click", function () {
                var resultFeed = "";
                $("#listFiles td ").each(function (index, element) {
                    if (index % 3 == 0)  //get feed names and concreate.
                        resultFeed = $(this).text() + "|" + resultFeed;
                });
                if (resultFeed != null && resultFeed != "")
                //將文件發送到后台
                    IWannaChat.server.send(resultFeed.substring(0, resultFeed.length - 1));
            });

            //這個主要是接收后台處理的結果,然后打印到前台來
            IWannaChat.client.addMessage = function (message) {
                if (message.contains("|")) {
                    var result = message.split('|');
                    var fileFlag = result[0];
                    var filePrograss = result[1];

                    $('#myPrograss' + fileFlag).html('<table><tr><th  style="width:' + filePrograss * 200 + 'px;background-color:green;"></th><th style="line-height:10px;background-color:white;border:none;">' + parseInt(filePrograss * 100) + '%</th></tr></table>');
                    if (filePrograss != 1)
                        $('#myState' + fileFlag).html('In Prograss');
                    else
                        $('#myState' + fileFlag).html('Done');
                }
                else {
                    $("#log").append("<li>"+message+"</li>");
                }
            };

            //開啟(長輪訓的方式)
            $.connection.hub.start();
        });

        String.prototype.contains = function (strInput) {
            return this.indexOf(strInput) != -1;
        }

看完這些,你是不是感覺和微軟提供的某個接口非常相像呢? 對,這就是ICallbackEventHandler,請參見我的文章BlogEngine學習二:基於ICallbackEventHandler的輕量級Ajax方式

好了,就寫到這里,這個demo剛做完,還有很多bug,當然也沒有優化,還請大家自行測試吧.

代碼下載

請點擊這里下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM