在日常工作中,有时候需要到远程服务器上部署新版本的系统,由于远程服务器出于外网,所以每次都要开QQ连接,非常麻烦。索性就研究了下IHttpasyncHandler,并结合Juqery ProgressBar,打造了一款大文件传送器。其基本原理就是首先在客户端将大文件分段拆分,然后写入内存流,最后发送到服务器上。在上传的同时,会利用异步Handler来获取当前进度并推送到前台显示。图示效果如下(当前缓存设置为5000字节大小):
图示结果
(图1,传送到36%的时候)
(图2,传送到100%的时候)
异步Handler设计
下面来说说具体的实现方式,首先来说说实时通知这块:
using System; using System.Collections.Generic; using System.Web; using AsyncThermometerDeamon.Handlers.Code; namespace AsyncThermometerDeamon.Handlers { public class FileUploadWatcher : IHttpAsyncHandler { public static List<AsyncResultDaemon> asyncResults = new List<AsyncResultDaemon>(); public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { AsyncResultDaemon asyncResult = new AsyncResultDaemon(context,cb,extraData); asyncResults.Add(asyncResult); return asyncResult; } public void EndProcessRequest(IAsyncResult result) { asyncResults.Clear(); AsyncResultDaemon aResult = result as AsyncResultDaemon; aResult.Send(); } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { throw new NotImplementedException(); } } }
在程序中,首先申明了一个List容器,以便保存当前的请求。然后当有请求进来的时候,BeginProcessRequest会将当前请求进行初始化,然后加入到List容器中,最后将执行结果返回。
而在EndProcessRequest函数中,一旦当前的操作完成,将会触发此函数推送当前的进度数据到前台。
这里我们来看一下AsyncResultDaemon类:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace AsyncThermometerDeamon.Handlers.Code { public class AsyncResultDaemon:IAsyncResult { public AsyncResultDaemon(HttpContext context, AsyncCallback cb,object extraData) { this.context = context; this.cb = cb; } private HttpContext context; private AsyncCallback cb; private object extraData; public long percent; public bool isCompleted = false; public void Send() { context.Response.Write(percent); } public void CompleteTask() { if (cb != null) { cb(this); this.isCompleted = true; } } public object AsyncState { get { return null; } } public System.Threading.WaitHandle AsyncWaitHandle { get { return null; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get { return isCompleted; } } } }
这个类很简单,继承自IAsyncResult接口,主要用来返回异步执行结果的。当异步执行任务完毕后,其他函数可以通过调用CompleteTask方法来抛出任务完成事件,这个抛出的事件将会被EndProcessRequest接住,进而推送实时进度通知。
文件上传Handler设计
说完了异步Handler,再来说一说文件上传Handler:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; namespace AsyncThermometerDeamon.Handlers { public class FileUpload : IHttpHandler { //设置发送的缓冲大小 private int bufferSize = 5000; public void ProcessRequest(HttpContext context) { //得到文件全路径及文件名称 string filePath = context.Request.QueryString["path"]; string fileName = Path.GetFileName(filePath); byte[] byteToSend; FileStream fs = new FileStream(filePath, FileMode.Open); fs.Position = 0; long totalBytesLength = fs.Length; //文件分多少批发送 long totalBatch = 0; if (totalBytesLength % bufferSize == 0) totalBatch = totalBytesLength / bufferSize; else totalBatch = totalBytesLength / bufferSize + 1; //遍历 for (int i = 0; i < totalBatch; i++) { //设置当前需要获取的流的位置 fs.Position = i * bufferSize; //如果是最后一批流数据 if (i == totalBatch - 1) { long lastBatchLength = totalBytesLength = totalBytesLength - i * bufferSize; byteToSend = new byte[lastBatchLength]; fs.Read(byteToSend, 0, (int)lastBatchLength); } else { byteToSend = new byte[bufferSize]; fs.Read(byteToSend, 0, bufferSize); } //避免闭包 int j = i; //写数据 using (FileStream fsWrite = new FileStream(@"C:\" + fileName, FileMode.Append, FileAccess.Write)) { fsWrite.Write(byteToSend, 0, byteToSend.Length); fsWrite.Flush(); } //预报当前发送状态 long percentage = (j+1)*100/totalBatch; //while循环能够保证最后一批数据顺利推送到前台 while (FileUploadWatcher.asyncResults.Count == 0 && percentage == 100) { } if (FileUploadWatcher.asyncResults.Count != 0) { FileUploadWatcher.asyncResults[0].percent = percentage; FileUploadWatcher.asyncResults[0].CompleteTask(); } } fs.Close(); } public bool IsReusable { get { return false; } } } }
文件上传这块,主要是通过将大文件分割,然后放到一个容积为5000字节的缓冲区中,分段发送,以避免出现OutOfMemory的错误。所以,这种处理机制可以保证发送大数据的文件,比如说1GB大小的文件。在每次写文件的时候,程序会利用long percentage = (j+1)*100/totalBatch;来计算当前的进度,并且通过如下的代码来将当前的进度数据进行推送:
while (FileUploadWatcher.asyncResults.Count == 0 && percentage == 100) { } if (FileUploadWatcher.asyncResults.Count != 0) { FileUploadWatcher.asyncResults[0].percent = percentage; FileUploadWatcher.asyncResults[0].CompleteTask(); }
如果当前有请求,那么就可以调用异步Handler中的CompleteTask来报告本次的进度,CompleteTask将会抛出事件来触发EndProcessRequest,EndProcessRequest则会将当前的进度数据推送至前台。
While循环主要是保证最后一次数据推送能够正常完成,去掉这句,数据一直会显示完成度为99%。
前台设计
最后来看看前台设计吧:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="AsyncThermometerDeamon.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>文件上传</title>
<style type="text/css">
body{font-size:12px;}
#txtPath{border:none;border-bottom:1px solid black;width:300px;}
#fileUpload{border:none;border-bottom:1px solid black;width:250px;background-color:White;height:25px;}
#btnUpload{border:none;background:url(Image/btn_01.gif) no-repeat;width:100px;height:30px;}
#result {width:400px;}
</style>
<link href="Styles/jquery-ui-1.8.16.custom.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="Scripts/jquery-1.6.4.min.js"></script>
<script src="Scripts/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#btnUpload").bind("click", function () {
StartFileUpload();
TriggerAjax();
});
});
var StartFileUpload = function () {
var filePath = $("#fileUpload").val();
var handerUrl = "Handlers/FileUpload.ashx?path=" + filePath;
$.ajax({
url: handerUrl,
type: "GET",
success: function (result) {},
error: function (result) {}
});
}
var TriggerAjax = function () {
var handerUrl = "Handlers/FileUploadWatcher.ashx";
$.ajax({
url: handerUrl,
type: "GET",
success: function (result) {
var data = parseInt(result);
$("#result").progressbar({ value: data });
$("#percent").text(data+"%");
if (result != 100) {
TriggerAjax();
}
},
error: function (result) {
debugger;
alert(result);
}
});
}
</script>
</head>
<body>
<form id="form1" runat="server" >
请输入目标路径:<asp:TextBox ID="txtPath" runat="server" Text="\\192.168.0.180\MatiSoftDaemon\TestUploading" ></asp:TextBox>
<br />
<br />
请选择本地文件:<asp:FileUpload ID="fileUpload" runat="server" />
<input id="btnUpload" type="button" value="上传文件" />
<br />
<div id="result" ></div>
<div id="percent"></div>
</form>
</body>
</html>
前台代码很简单,就是通过StartFileUpload()函数触发文件上传动作, TriggerAjax()函数触发进度检测动作。