Thursday, October 14, 2004

 

Some notes on file upload - 2

I.
ASP.net里大文件上传问题

By WuCountry
From http://blog.aspcool.com/wucountry/archive/2005/10/19/3129.html
http://blog.aspcool.com/wucountry/archive/2005/10/19/3132.html
http://blog.aspcool.com/wucountry/archive/2005/10/19/3133.html
http://blog.aspcool.com/wucountry/archive/2005/10/19/3134.html

.NET大文件上传知识整理

最近做在做ePartner项目,涉及到文件上传的问题。 以前也做过文件上传,但都是些小文件,不超过2M。 这次要求上传100M以上的东西。 没办法找来资料研究了一下。基于WEB的文件上传可以使用FTP和HTTP两种协议,用FTP的话虽然传输稳定,但安全性是个严重的问题,而且FTP服务器读用户库获取权限,这样对于用户使用来说还是不太方便。 剩下只有HTTP。在HTTP中有3种方式,PUT、WEBDAV、RFC1867,前2种方法不适合大文件上传,目前我们使用的web上传都是基于RFC1867标准的HTML中基于表单的文件上传。

一、先简要介绍一下RFC1867(Form-based File Upload in HTML)标准:
1.带有文件提交功能的HTML表单
现有的HTML规范为INPUT元素的TYPE属性定义了八种可能的值,分别是:CHECKBOX, HIDDEN, IMAGE, PASSWORD, RADIO, RESET, SUBMIT, TEXT. 另外,当表单采用POST方式的时候,表单默认的具有"application/x-www-form-urlencoded" 的ENCTYPE属性。

RFC1867标准对HTML做出了两处修改:
1)为INPUT元素的TYPE属性增加了一个FILE选项。
2)INPUT标记可以具有ACCEPT属性,该属性能够指定可被上传的文件类型或文件格式列表。

另外,本标准还定义了一种新的MIME类型:multipart/form-data,以及当处理一个带有ENCTYPE="multipart/form-data" 并且/或含有的标记的表单时所应该采取的行为。

举例来说,当HTML表单作者想让用户能够上传一个或更多的文件时,他可以这么写:


File to process:




HTML DTD里所需要做出的改动是为InputType实体增加一个选项。此外,我们也建议用一系列用逗号分隔的文件类型来作为INPUT标记的ACCEPT属性。

... (其他元素) ...

RADIO | SUBMIT | RESET |
IMAGE | HIDDEN | FILE )">

TYPE %InputType TEXT
NAME CDATA #IMPLIED -- required for all but submit and reset
VALUE CDATA #IMPLIED
SRC %URI #IMPLIED -- for image inputs --
CHECKED (CHECKED) #IMPLIED
SIZE CDATA #IMPLIED --like NUMBERS,
but delimited with comma, not space
MAXLENGTH NUMBER #IMPLIED
ALIGN (top|middle|bottom) #IMPLIED
ACCEPT CDATA #IMPLIED --list of content types
>

... (其他元素) ...

2.文件传输延迟
在某些情况下,在确实准备接受数据前,服务器先对表单数据中的某些元素(比如说用户名,账号等)进行验证是推荐的做法。但是,经过一定的考虑后,我们认为如果服务器想这样做的话,最好是采用一系列的表单,并将前面所验证过的数据元素作为“隐藏”字段传回给客户端,或者是通过安排表单使那些需要验证的元素先显示出来。这样的话,那些需要做复杂的应用的服务器可以自己维持事务处理的状态,而那些简单的应用的则可以实现得简单些。

HTTP协议可能需要知道整个事务处理中的内容总长度。即使没有明确要求,HTTP客户端也应该提供上传的所有文件的内容总长度,这样一个繁忙的服务器就能够判断文件的内容是否是过大以至于将不能完整地处理,从而返回一个错误代码并关闭该连接,而不用等到接受了所有的数据才进行判断。目前一些现有的CGI应用对所有的POST事务都需要知道内容总长度。

如果INPUT标记含有一个MAXLENGTH属性,客户端可以将这个属性值看作是服务器端所能够接受的传送文件的最大字节数。在这种情况下,服务器能够在上传开始前,提示客户端在服务器上有多少空间可以用来进行文件上传。但是应该引起注意的是,这仅仅是一个提示,在表单被创建后和文件上传前,服务器的实际需求可能会发生改变。

在任何情况下,如果接受的文件过大的话,任何一个HTTP服务器都有可能在文件传输的过程中中断传输。

3.传输二进制数据的其他解决办法
有些人曾经建议使用一种新的MIME类型"aggregate",比如说aggregate/mixed 或是content-transfer-encoding "包"来描述那些不确定长度的二进制数据,而不是靠分解为多个部分来表示。虽然我们并不反对这么做,但这需要增加额外的设计和标准化工作来让大家接受并理解"aggregate"。 从另一方面来说,"分解为多部分"的机制工作得很好,能够非常简单的在客户发送端和服务器接受端加以实现,而且能像其他一些综合处理二进制数据的方式一样高效率地工作。

4.例子
假设服务器段提供的是如下的HTML:
ENCTYPE="multipart/form-data"
METHOD=POST>
What is your name?
What files are you sending?

用户在“姓名”字段里面填写"Joe Blow",对问题'What files are you sending?',用户选择
了一个文本文件"file1.txt"。
客户段可能发送回如下的数据:
Content-type: multipart/form-data, boundary=AaB03x

--AaB03x
content-disposition: form-data; name="field1"

Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain

... file1.txt 的内容...
--AaB03x--
如果用户同时还选择了另一个图片文件"file2.gif",那么客户端可能发送的数据将是:
Content-type: multipart/form-data, boundary=AaB03x

--AaB03x
content-disposition: form-data; name="field1"

Joe Blow
--AaB03x
content-disposition: form-data; name="pics"
Content-type: multipart/mixed, boundary=BbC04y

--BbC04y
Content-disposition: attachment; filename="file1.txt"

Content-Type: text/plain

... file1.txt 的内容...
--BbC04y
Content-disposition: attachment; filename="file2.gif"
Content-type: image/gif
Content-Transfer-Encoding: binary

... file2.gif的内容...
--BbC04y--
--AaB03x--


二、利用RFC1867标准处理文件上传的两种方式:
1.一次性得到上传的数据,然后分析处理。
看了N多代码之后发现,目前无组件程序和一些COM组件都是使用Request.BinaryRead方法。一次性得到上传的数据,然后分析处理。这就是为什么上传大文件很慢的原因了,IIS超时不说,就算几百M文件上去了,分析处理也得一阵子。
2.一边接收文件,一边写硬盘。

了解了一下国外的商业组件,比较流行的有Power-Web,AspUpload,ActiveFile,ABCUpload,aspSmartUpload,SA-FileUp。其中比较优秀的是ASPUPLOAD和SA-FILE,他们号称可以处理2G的文件(SA-FILE EE版甚至没有文件大小的限制),而且效率也是非常棒,难道编程语言的效率差这么多?查了一些资料,觉得他们都是直接操作文件流。这样就不受文件大小的制约。但老外的东西也不是绝对完美,ASPUPLOAD处理大文件后,内存占用情况惊人。1G左右都是稀松平常。至于SA-FILE虽然是好东西但是破解难寻。然后发现2款.NET上传组件,Lion.Web.UpLoadModule和AspnetUpload也是操作文件流。但是上传速度和CPU占用率都不如老外的商业组件。
做了个测试,LAN内传1G的文件。ASPUPLOAD上传速度平均是4.4M/s,CPU占用10-15,内存占用700M。SA-FILE也差不多这样。而AspnetUpload最快也只有1.5M/s,平均是700K/s,CPU占用15-39,测试环境: PIII800,256M内存,100M LAN。我想AspnetUpload速度慢是可能因为一边接收文件,一边写硬盘。资源占用低的代价就是降低传输速度。但也不得不佩服老外的程序,CPU占用如此之低.....

三、ASP.NET上传文件遇到的问题
我们在用ASP.NET上传大文件时都遇到过这样或那样的问题。设置很大的maxRequestLength值并不能完全解决问题,因为ASP.NET会block直到把整个文件载入内存后,再加以处理。实际上,如果文件很大的话,我们经常会见到Internet Explorer显示 "The page cannot be displayed - Cannot find server or DNS Error",好像是怎么也catch不了这个错误。为什么?因为这是个client side错误,server side端的Application_Error是处理不到的。
四、ASP.NET大文件上传解决方案
解决的方法是利用隐含的HttpWorkerRequest,用它的GetPreloadedEntityBody 和 ReadEntityBody方法从IIS为ASP.NET建立的pipe里分块读取数据。Chris Hynes为我们提供了这样的一个方案(用HttpModule),该方案除了允许你上传大文件外,还能实时显示上传进度。
Lion.Web.UpLoadModule和AspnetUpload 两个.NET组件都是利用的这个方案。

方案原理:
利用HttpHandler实现了类似于ISAPI Extention的功能,处理请求(Request)的信息和发送响应(Response)。

方案要点:
1. httpHandler or HttpModule
a.在asp.net进程处理request请求之前截获request对象
b.分块读取和写入数据
c.实时跟踪上传进度更新meta信息
2. 利用隐含的HttpWorkerRequest用它的GetPreloadedEntityBody 和 ReadEntityBody方法处理文件流
IServiceProvider provider = (IServiceProvider) HttpContext.Current;
HttpWorkerRequest wr = (HttpWorkerRequest) provider.GetService(typeof(HttpWorkerRequest));
byte[] bs = wr.GetPreloadedEntityBody();
....
if (!wr.IsEntireEntityBodyIsPreloaded())
{
int n = 1024;
byte[] bs2 = new byte[n];
while (wr.ReadEntityBody(bs2,n) >0)
{
.....
}
}
3. 自定义Multipart MIME 解析器
自动截获MIME分割符
将文件分块写如临时文件
实时更新Appliaction 状态(ReceivingData, Error, Complete)

因为connection对象仅仅跟host对象相关,且处理一个套接字,所以其数据成员仅有:

private Host _host; //指向宿主对象

private Socket _socket; //当前套接字

我们知道host调用且仅了conn.ProcessOneRequest();方法,所以我们首先要找到此方法(coneetion很多方法,我们先看看主要的):

public void ProcessOneRequest() { // wait for at least some input

if (WaitForRequestBytes() == 0) {

WriteErrorAndClose(400);

return;

}



Request request = new Request(_host, this);

request.Process();

}

从代码来看,此过程首先要确保socket有数据读入,如果发生无法读取,就向socket写入一个400错误;如果有数据读入,那么构造一个requese对象,调用request的Process方法(呵呵,麻烦事大家总是一层层甩给别人J)。

还是继续分析Connection对象, WaitForRequestBytes()实际上是单独线程来读取socket,如果socket连接但是没有数据流,就至多等待10秒。

WriteErrorAndClose(int);函数调用WriteErrorAndClose(int , string)

{

String body = Messages.FormatErrorMessageBody(statusCode, _host.VirtualPath);

if (message != null && message.Length > 0)

body += "\r\n";

WriteEntireResponseFromString(statusCode, null, body, false);

}

WriteEntireResponseFromString()函数根据状态码构造返回的http数据流,并写回到客户端。实际上体现了对http协议的具体实现。我们暂时放下,追踪看看Request对象。

internal class Request : SimpleWorkerRequest {。。。。。。}继承自.net,根据MSDN介绍:HttpWorkerRequest 的这种简单实现提供请求 URL 和查询字符串,并将输出的正文捕获到 TextWriter 中。若要实现更为丰富的功能(如提供已发送的内容和标头以及捕获以二进制数据表示的响应标头或响应正文),则应扩展 SimpleWorkerRequest 并重写适当的 HttpWorkerRequest 方法。

还是从Process函数入手看看(代码较长):

{

ReadAllHeaders(); //阅读处所有的http请求头



if (_headerBytes == null || _endHeadersOffset < 0 ||

_headerByteStrings == null || _headerByteStrings.Count == 0) {

_conn.WriteErrorAndClose(400);

return; //如果读取http头错误就返回

}



ParseRequestLine(); //处理request行输入



// Check for bad path

if (IsBadPath()) { //防止用户请求bad路径

_conn.WriteErrorAndClose(400);

return;

}



// Limit to local requests only

if (!_conn.IsLocal) {

_conn.WriteErrorAndClose(403);

return;

}



// Check if the path is not well formed or is not for the current app

bool isClientScriptPath = false;

String clientScript = null;



if (!_host.IsVirtualPathInApp(_path, out isClientScriptPath, out clientScript)) {

_conn.WriteErrorAndClose(404); //检验url请求是否属于应用程序路径范围内,如果不是,报404错误。

return;

}



ParseHeaders(); //解析http请求头



ParsePostedContent(); //解析post方法的内容



if (_verb == "POST" && _contentLength > 0 && _preloadedContentLength < _contentLength) { //如果是post方法,需要等待post数据完成才继续那么调用conn的等待方法Write100Continue直到post完成

_conn.Write100Continue();

}



// special case for client script

if (isClientScriptPath) { //如果请求的是脚本路径,那么直接读取文件(也就是.js文件,按照文本文件来看待)

_conn.WriteEntireResponseFromFile(_host.PhysicalClientScriptPath + clientScript, false);

return;

}



// special case for directory listing

if (ProcessDirectoryListingRequest()) { //如果是请求目录list,则处理后返回

return;

}



PrepareResponse(); //准备响应内容



// Hand the processing over to HttpRuntime



HttpRuntime.ProcessRequest(this); //通过HttpRuntime的方法执行asp.net的内容,驱动所有 ASP.NET Web 处理执行。

}

针对该函数细节,逐个分析以下函数:

ReadAllHeaders

ParseRequestLine

ParsePostedContent

ProcessDirectoryListingRequest

PrepareResponse

因为他们处理一次http request。

private void ReadAllHeaders() {

_headerBytes = null;



do {

if (!TryReadAllHeaders())

break; // something bad happened

}

while (_endHeadersOffset < 0); // found \r\n\r\n

} 该函数不断调用TryReadAllHeaders,仔细看看这个TryReadAllHeaders:

private bool TryReadAllHeaders() {

// read the first packet (up to 32K)

byte[] headerBytes = _conn.ReadRequestBytes(maxHeaderBytes); //从connection读取最大数据32*1024字节。

if (headerBytes == null || headerBytes.Length == 0)

return false; //如果读取数据失败,返回错误,调用此函数者应当检查数据读取是否完整

if (_headerBytes != null) { // previous partial read 以下将当前读取的数据累加

int len = headerBytes.Length + _headerBytes.Length;

if (len > maxHeaderBytes)

return false;



byte[] bytes = new byte[len];

//注意调用了快速的Buffer.BlockCopy方法,不过我认为可以更好的处理读取数据问题,因为一再的产生byte数组显然不是很好的方法。

Buffer.BlockCopy(_headerBytes, 0, bytes, 0, _headerBytes.Length);

Buffer.BlockCopy(headerBytes, 0, bytes, _headerBytes.Length, headerBytes.Length);

_headerBytes = bytes;

}

else {

_headerBytes = headerBytes;

}



// start parsing 下面准备解析请求行

_startHeadersOffset = -1;

_endHeadersOffset = -1;

_headerByteStrings = new ArrayList();



// find the end of headers ByteParser是自定义的工具类,此时我们只要知道是帮助字节数组转换方便得到行,ByteString也是一个工具类,帮助将字节数组转换为字符串

ByteParser parser = new ByteParser(_headerBytes);

for (;;) {

ByteString line = parser.ReadLine();

if (line == null)

break;

if (_startHeadersOffset < 0) {

_startHeadersOffset = parser.CurrentOffset;

}

if (line.IsEmpty) {

_endHeadersOffset = parser.CurrentOffset;

break;

}

_headerByteStrings.Add(line);

}

return true;

}

如何处理分解行呢?

private void ParseRequestLine() {

ByteString requestLine = (ByteString)_headerByteStrings[0];

ByteString[] elems = requestLine.Split(' '); //我们知道每一个header同header值之间都有一个必然的空格,例如cassini返回的http响应的头:

HTTP/1.1 404 Not Found

//判断header是否读取正确,一般请求头第一行应该是例如:GET /pub/WWW/ HTTP/1.1

if (elems == null || elems.Length < 2 || elems.Length > 3) {

return;

}



_verb = elems[0].GetString(); //读取 http 请求方法

ByteString urlBytes = elems[1];

_url = urlBytes.GetString(); //获取请求的url



if (elems.Length == 3) //确定http请求的协议版本

_prot = elems[2].GetString(); //目前仅有HTTP/1.1或者HTTP/1.0

else

_prot = "HTTP/1.0";



// query string

int iqs = urlBytes.IndexOf('?'); //请求的参数获取 字节数组表示

if (iqs > 0)

_queryStringBytes = urlBytes.Substring(iqs+1).GetBytes();

else

_queryStringBytes = new byte[0];



iqs = _url.IndexOf('?'); //取得path 和 参数的字符串表示

if (iqs > 0) {

_path = _url.Substring(0, iqs);

_queryString = _url.Substring(iqs+1);

}

else {

_path = _url;

_queryStringBytes = new byte[0];

}

// url-decode path 开始url解码,这个MS之前犯的著名URL解码错误就在此处了

if (_path.IndexOf('%') >= 0) {

_path = HttpUtility.UrlDecode(_path); //调用.net的工具方法

}



// path info 以下获取path

int lastDot = _path.LastIndexOf('.');

int lastSlh = _path.LastIndexOf('/');

if (lastDot >= 0 && lastSlh >= 0 && lastDot < lastSlh) {

int ipi = _path.IndexOf('/', lastDot);

_filePath = _path.Substring(0, ipi);

_pathInfo = _path.Substring(ipi);

}

else {

_filePath = _path;

_pathInfo = String.Empty;

}

_pathTranslated = MapPath(_filePath); //映射路径,将文件映射到具体的磁盘路径

}



处理完http header后,开始处理http的请求正文,看看ParsePostedContent

private void ParsePostedContent() {

_contentLength = 0;

_preloadedContentLength = 0;



String contentLengthValue = _knownRequestHeaders[HttpWorkerRequest.HeaderContentLength]; //察看头部中的定义的长度

if (contentLengthValue != null) {

try {

_contentLength = Int32.Parse(contentLengthValue);

}

catch {

}

}

//以下检查各个长度数据是否异常

if (_headerBytes.Length > _endHeadersOffset) {

_preloadedContentLength = _headerBytes.Length - _endHeadersOffset;

if (_preloadedContentLength > _contentLength && _contentLength > 0)

_preloadedContentLength = _contentLength; // don't read more than the content-length 注意不要读取过多的数据

_preloadedContent = new byte[_preloadedContentLength];

Buffer.BlockCopy(_headerBytes, _endHeadersOffset, _preloadedContent, 0, _preloadedContentLength); //拷贝数据

}

}

以上将http请求的content数据字节拷贝到_preloadedContent成员变量中去。

接下来的ProcessDirectoryListingRequest处理不带文件名的请求,也就是直接请求浏览某个目录。略。

处理完毕后,准备构造响应数据,看看PrepareResponse

_headersSent = false; //准备回写到connection中去

_responseStatus = 200;

_responseHeadersBuilder = new StringBuilder();

_responseBodyBytes = new ArrayList();

其他的,我们应当注意

HttpRuntime.ProcessRequest(this); 隐含的调用关系。HttpRuntime.ProcessRequest会在需要回写数据的时候调用相关的函数,这些函数被cassini注释。

我们要明白这些重载的函数会在asp.net处理过程中被调用。

///////////////////////////////////////////////////////////////////////////////////////////////

//

// Implementation of HttpWorkerRequest

//

///////////////////////////////////////////////////////////////////////////////////////////////

public override String GetUriPath() { //返回请求的 URI 的虚拟路径。

return _path;

}



public override String GetQueryString() { //返回请求 URL 中指定的查询字符串。

return _queryString;

}



public override byte[] GetQueryStringRawBytes() { //在派生类中被重写时,以字节数组的形式返回响应查询字符串。

return _queryStringBytes;

}



public override String GetRawUrl() { //返回附加了查询字符串的请求标头中包含的 URL 路径。

return _url;

}



public override String GetHttpVerbName() { //返回请求标头的指定成员。

return _verb;

}



public override String GetHttpVersion() { //提供对请求的 HTTP 版本(如“HTTP/1.1”)的访问。

return _prot;

}



public override String GetRemoteAddress() {//提供对请求标头的指定成员的访问。

return _conn.RemoteIP;

}



public override int GetRemotePort() {//提供对请求标头的指定成员的访问。

return 0;

}



public override String GetLocalAddress() {//

return _conn.LocalIP;

}



public override int GetLocalPort() {

return _host.Port;

}



public override String GetFilePath() {//在派生类中被重写时,返回所请求的 URI 的物理路径。

return _filePath;

}



public override String GetFilePathTranslated() {//返回请求的 URI 的物理文件路径(并将其从虚拟路径翻译成物理路径:例如,从“/proj1/page.aspx”翻译成“c:\dir\page.aspx”)

return _pathTranslated;

}



public override String GetPathInfo() {//返回具有 URL 扩展的资源的其他路径信息。即对于路径/virdir/page.html/tail,GetPathInfo 值为/tail。

return _pathInfo;

}



public override String GetAppPath() {//返回当前正在执行的服务器应用程序的虚拟路径。

return _host.VirtualPath;

}



public override String GetAppPathTranslated() {//返回当前正在执行的服务器应用程序的 UNC 翻译路径。

return _host.PhysicalPath;

}



public override byte[] GetPreloadedEntityBody() {//返回 HTTP 请求正文已被读取的部分。

return _preloadedContent;

}



public override bool IsEntireEntityBodyIsPreloaded() {//返回一个值,该值指示是否所有请求数据都可用,以及是否不需要对客户端进行进一步读取。

return (_contentLength == _preloadedContentLength);

}



public override int ReadEntityBody(byte[] buffer, int size) {//读取客户端的请求数据(在尚未预加载时)。

int bytesRead = 0;

byte[] bytes = _conn.ReadRequestBytes(size);

if (bytes != null && bytes.Length > 0) {

bytesRead = bytes.Length;

Buffer.BlockCopy(bytes, 0, buffer, 0, bytesRead);

}

return bytesRead;

}



public override String GetKnownRequestHeader(int index) {//返回与指定的索引相对应的标准 HTTP 请求标头。

return _knownRequestHeaders[index];

}



public override String GetUnknownRequestHeader(String name) {//返回非标准的 HTTP 请求标头值。指定了名称

int n = _unknownRequestHeaders.Length;



for (int i = 0; i < n; i++) {

if (String.Compare(name, _unknownRequestHeaders[i][0], true, CultureInfo.InvariantCulture) == 0)

return _unknownRequestHeaders[i][1];

}

return null;

}



public override String[][] GetUnknownRequestHeaders() {//获取所有非标准的 HTTP 标头的名称-值对。

return _unknownRequestHeaders;

}



public override String GetServerVariable(String name) {//从与请求关联的服务器变量词典返回单个服务器变量。

String s = String.Empty;

switch (name) {

case "ALL_RAW":

s = _allRawHeaders;

break;

case "SERVER_PROTOCOL":

s = _prot;

break;

// more needed?

}

return s;

}



public override String MapPath(String path) {//返回与指定虚拟路径相对应的物理路径。

String mappedPath = String.Empty;

if (path == null || path.Length == 0 || path.Equals("/")) {

// asking for the site root

if (_host.VirtualPath == "/") {

// app at the site root

mappedPath = _host.PhysicalPath;

}

else {

// unknown site root - don't point to app root to avoid double config inclusion

mappedPath = Environment.SystemDirectory;

}

}

else if (_host.IsVirtualPathAppPath(path)) {

// application path

mappedPath = _host.PhysicalPath;

}

else if (_host.IsVirtualPathInApp(path)) {

// inside app but not the app path itself

mappedPath = _host.PhysicalPath + path.Substring(_host.NormalizedVirtualPath.Length);

}

else {

// outside of app -- make relative to app path

if (path.StartsWith("/"))

mappedPath = _host.PhysicalPath + path.Substring(1);

else

mappedPath = _host.PhysicalPath + path;

}

mappedPath = mappedPath.Replace('/', '\\');

if (mappedPath.EndsWith("\\") && !mappedPath.EndsWith(":\\"))

mappedPath = mappedPath.Substring(0, mappedPath.Length-1);

return mappedPath;

}



public override void SendStatus(int statusCode, String statusDescription) {//指定响应的 HTTP 状态代码和状态说明;例如 SendS

//例子
HttpApplication application1 = sender as HttpApplication;
HttpWorkerRequest request1 = (HttpWorkerRequest) ((IServiceProvider) HttpContext.Current).GetService(typeof(HttpWorkerRequest));
try
{
if (application1.Context.Request.ContentType.IndexOf("multipart/form-data") <= -1)
{
return;
}
//Check The HasEntityBody
if (!request1.HasEntityBody())
{
return;
}
int num1 = 0;
TimeSpan span1 = DateTime.Now.Subtract(this.beginTime);

string text1 = application1.Context.Request.ContentType.ToLower();

byte[] buffer1 = Encoding.ASCII.GetBytes(("\r\n--" + text1.Substring(text1.IndexOf("boundary=") + 9)).ToCharArray());
int num2 = Convert.ToInt32(request1.GetKnownRequestHeader(11));
Progress progress1 = new Progress();

application1.Context.Items.Add("FileList", new Hashtable());

byte[] buffer2 = request1.GetPreloadedEntityBody();
num1 += buffer2.Length;

string text2 = this.AnalysePreloadedEntityBody(buffer2, "UploadGUID");
if (text2 != string.Empty)
{
application1.Context.Items.Add("LionSky_UpLoadModule_UploadGUID", text2);
}
bool flag1 = true;
if ((num2 > this.UpLoadFileLength()) && ((0 > span1.TotalHours) || (span1.TotalHours > 3)))
{
flag1 = false;
}
if ((0 > span1.TotalHours) || (span1.TotalHours > 3))
{
flag1 = false;
}
string text3 = this.AnalysePreloadedEntityBody(buffer2, "UploadFolder");
ArrayList list1 = new ArrayList();
RequestStream stream1 = new RequestStream(buffer2, buffer1, null, RequestStream.FileStatus.Close, RequestStream.ReadStatus.NoRead, text3, flag1, application1.Context, string.Empty);
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.FileLength = num2;
progress1.ReceivedLength = num1;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
application1.Application["_UploadGUID_" + text2] = progress1;
}

if (!request1.IsEntireEntityBodyIsPreloaded())
{
byte[] buffer4;
ArrayList list2;
int num3 = 204800;
byte[] buffer3 = new byte[num3];
while ((num2 - num1) >= num3)
{
if (!application1.Context.Response.IsClientConnected)
{
this.ClearApplication(application1);
}
num3 = request1.ReadEntityBody(buffer3, buffer3.Length);
num1 += num3;
list2 = stream1.ContentBody;
if (list2.Count > 0)
{
buffer4 = new byte[list2.Count + buffer3.Length];
list2.CopyTo(buffer4, 0);
buffer3.CopyTo(buffer4, list2.Count);
stream1 = new RequestStream(buffer4, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
else
{
stream1 = new RequestStream(buffer3, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.ReceivedLength = num1;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
application1.Application["_UploadGUID_" + text2] = progress1;
}
}
buffer3 = new byte[num2 - num1];
if (!application1.Context.Response.IsClientConnected && (stream1.FStatus == RequestStream.FileStatus.Open))
{
this.ClearApplication(application1);
}
num3 = request1.ReadEntityBody(buffer3, buffer3.Length);
list2 = stream1.ContentBody;
if (list2.Count > 0)
{
buffer4 = new byte[list2.Count + buffer3.Length];
list2.CopyTo(buffer4, 0);
buffer3.CopyTo(buffer4, list2.Count);
stream1 = new RequestStream(buffer4, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
else
{
stream1 = new RequestStream(buffer3, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.ReceivedLength = num1 + buffer3.Length;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
if (flag1)
{
progress1.UploadStatus = Progress.UploadStatusEnum.Uploaded;
}
else
{
application1.Application.Remove("_UploadGUID_" + text2);
}
}
}
byte[] buffer5 = new byte[list1.Count];
list1.CopyTo(buffer5);
this.PopulateRequestData(request1, buffer5);
}
catch (Exception exception1)
{
this.ClearApplication(application1);
throw exception1;
}

最后自己写一点总结。

本来上而的几篇文章都是很好的关于实现ASP.net里大文件上传的方案,而且网上还有好多。而我在搜索期间,找到的两个用现成的组件:

Lion.Web.UpLoadModule v1.0 Demo及AspNetUpload2.0,而这两个的核心方案是一至的(在接下来的时间里我会好好的研究一下里面的问题)。

而这些内容的核心来自思归博客的一篇POST。上面引用了 Chris Hynes 的文章来说明这些问题。

我最后找到源代码是在2005.net的博客上。

http://blog.aspcool.com/rogerwang/archive/2005/10/18/1841.html#3115

(感觉有些讽刺。)当我看到源代码的时候,可以下结论了:Lion.Web.UpLoadModule v1.0 Demo完全就是Mic的gotdotnet那里下载的源文件,改的东西很少。而AspNetUpload的改动的要多一些,可惜改的都不是很核心的东西(当然Chris Hynes 已经为我们提供了理论依据),而是加了一些注册和代码加密以及一些函数的重载。当然,不可否认AspNetUpload的效率要比Lion.Web.UpLoadModule v1.0 Demo高得多,这里也就是说Bestcomy确实在里面下了不少功夫的。这也难怪bestcomy会在每个注册号上要收600元了。然而我最不 喜欢的是bestcomy的商业手段使得我在搜索大文件上传的时候几乎是陷入了他的商业陷井中。

II.
SunriseUpload.0.9.1的源码分析

By WuCountry
From http://computer.mblogger.cn/wucountry/archive/112005.aspx

今天正式开始研究SunriseUpload.0.9.1的源码。

先看web.config里的内容:









第一个设定是上传文件的最大长度和执行时间。我不知道其它的组件里是怎样处理的,像没有这些设定。
关键的还是第二个,它表示所有的上传请求都将映射到Sunrise.Web.Upload.HttpUploadModule模块上,这样使得我们自己的上传请求都将失败。所以如果想自己

测试上传的时候,请注释掉这一设定。
第三个设定是对进度条的请求,由于ashx的扩展各被IIS映射在aspnet.dll上,所以可以通过httpHeadler将请求拦截,这一功能比httpMoudle小一些,它只对

progress.ashx的请求进行拦截,当然是里可以用通配符。

SingleUpload的示例:
很简单的一些设定:只有一个上传的input(type=file,注意它的value属性是只读的)
然后一个button,最后是一个表格显示上传后的数据。
protected Table tbFileList;
protected Button btnUpload;
private void btnUpload_Click(object sender, EventArgs e)
{
string path = Path.Combine(Server.MapPath("."), "UploadFile");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}

//获取上传文件,"files"是的name属性
UploadFile uploadFile = UploadHelper.GetUploadFile("file1");

if(uploadFile != null)
{
TableRow tr = new TableRow();
TableCell[] tc = new TableCell[2] {new TableCell(), new TableCell()};

tc[0].Text = Path.GetFileName(uploadFile.FileName);
tc[1].Text = uploadFile.ContentLength.ToString("###,###") + " K bytes";
tr.Cells.AddRange(tc);
tbFileList.Rows.Add(tr);

//Save file
uploadFile.SaveAs(Path.Combine(path, Path.GetFileName(uploadFile.FileName)));
}
}
去掉细节,可以简化到:
private void btnUpload_Click(object sender, EventArgs e)
{
string path = Path.Combine(Server.MapPath("."), "UploadFile");
//获取上传文件,"files"是的name属性
UploadFile uploadFile = UploadHelper.GetUploadFile("file1");
if(uploadFile != null)
{
uploadFile.SaveAs(Path.Combine(path, Path.GetFileName(uploadFile.FileName)));
}
}
这里使用用了UploadHelper的静态成员函数,GetUpladFile,我觉得这不是一个好的做法,所以其它的组件里都改掉了这一做法。
下面我们就去看看UploadHelper类。

UploadHelper类有点长,近500行的代码。先看看核心函数:GetUploadFile()
public static UploadFile GetUploadFile(string name)
{
UploadFile uploadFile = new UploadFile(name);

return (uploadFile.FilePath == string.Empty) ? null : uploadFile;
}
呵呵,比想像的简单得多吧!!!接下来就又要看UploadFile类了!

UploadFile类代码不长,200多行,但都不好理解,要对HTTP协议有所了解。
先看看构造函数:
public UploadFile(string name)
{
if ((name == null) || (name == string.Empty))
{
throw new ArgumentNullException("Name", "Name can not be null!");
}

string content = String.Empty;

this.filename = string.Empty;
this.filepath = string.Empty;
this.contenttype = string.Empty;
this.filelength = 0;

前面的是初始化变量,直接从这里开始分析:看后面的解释
if (IsContentHeader(name))
{
content = name;
}
else if (IsContentHeader(Utils.GetContext().Request[name]))
{
content = Utils.GetContext().Request[name];
}

if ((content == null) || (content == string.Empty))
{
return;
}

//Get file info from content.
string[] contentArray = content.Split(';');

this.contenttype = contentArray[0];
this.contenttype = this.contenttype.Substring(14, (this.contenttype.Length - 14));
this.filename = contentArray[1];
this.filename = this.filename.Substring(10, (this.filename.Length - 11));
this.filepath = contentArray[2];
this.filepath = this.filepath.Substring(10, (this.filepath.Length - 11));

string uploadFolder = Utils.GetUploadFolder();
this.filepath = Path.Combine(uploadFolder, this.filepath);

try
{
this.filelength = new FileInfo(this.filepath).Length;
}
catch (Exception exception)
{
string uploadGuid = Utils.GetContext().Request["Sunrise_Web_Upload_UploadGUID"];

if (uploadGuid != null)
{
Utils.GetContext().Application.Remove(("_UploadGUID_" + uploadGuid));
}

throw exception;
}
}
//
研究说明:
if (IsContentHeader(name))
{
content = name;
}
else if (IsContentHeader(Utils.GetContext().Request[name]))
{
content = Utils.GetContext().Request[name];
}
这里又用name(上传文件的名字)参数调用了IsContentHeader函数,开始我不大明白,后面的分隔数组是怎么回事?上传的控件名不就是个字符串吗?实际上这

个函数在一次上传过程中被调用了两次,其中后面的分隔就是为了下次调用时做准备的,这是我的输出日志:
11/1/2005 4:58:04 PM file1
11/1/2005 4:58:04 PM Content-Type: video/x-ms-wmv;filename="D:\video\Defense.wmv";filepath="ec2f062d-21ab-4591-ada0-892a32a05625.wmv"
可以看到,第二次调用的时候,是以文件的上传信息来调用的,我们先不管它。往后看。
private bool IsContentHeader(string line)
{
if((line == null)||(line == String.Empty))
{
return false;
}
string[] contentArray = line.Split(';');
if (((contentArray.Length == 3)
&& contentArray[0].StartsWith("Content-Type:"))
&& (contentArray[1].StartsWith("filename=\"")
&& contentArray[2].StartsWith("filepath=\"")))
{
return true;
}
return false;
}
显然第一次调用是返回false;
第二次调用是在
else if (IsContentHeader(Utils.GetContext().Request[name]))
那么我们又要看看Utils类了。
public static HttpContext GetContext()
{
HttpContext context = HttpContext.Current;
if (context == null)
{
throw new Exception("HttpContext not found");
}

return context;
}
它返回当前HTTP请求的所有信息,里面就包含了文件上传的源信息。关于HttpContext类,可以查看MSDN里的帮助。也就是说,我们在HttpContext里取得数据的时

候,所得到的信息是完整的,所以我们可以得到上传文件的文件路径以文件类型。我们可以自己试一下:
1、建立一个新的Web Application,
2、创建一个类,
using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web;
using System.Xml;

namespace WebbTest
{
///
/// Summary description for UploadTest.
///

public class HttpModuleTest : IHttpModule
{
public HttpModuleTest()
{
//
// TODO: Add constructor logic here
//这里只是一个测试的调试输出,看看这个类为我们都做了些什么?
WebbSystem.TraceMsg("Construct the HttpModule.");
}

public void Dispose()
{
}

public void Init(System.Web.HttpApplication m_application)
{
}
}
}
3、添加到Web.config里,让所有的请求都要经过HttpModuleTest。



4、然后测试页面,可以得到结果:
11/1/2005 5:22:36 PM Construct the HttpModule.
11/2/2005 8:32:55 AM Construct the HttpModule.
(呵呵,我才发现一个问题,第二个时间上的信息是VS.net打开项目的时候产生的,因为VS.net在打开项目的时候对服务器做了一次请求。所以被记录下了。)
利用这个原理,我们是不是可以自己做一个网站统计系统呢?

好了,继续向下分析代码。

UploadFile的构造函数实际上是从服务器的临时目录里取得上传文件的信息,以便保存它。后面的代码通过对content的分析就可以一清二楚了。
那我们的问题是:为什么当文件上传的时候,它可以直接就处理上传的文件呢?
文件的上传过程是在什么地方呢?
于是我们要分析:HttpUploadModule了。
前面已经做了一个测试了,可以自己处理所有的用户请求,而且不管是什么状态的,因为我们在模块的构造函数里添加了自己处理代码,实际上我们要实现application的几个事件,。
HttpUploadModule是最长的文件了,它用来处理上传时的请求,还是来慢慢分析:
1、事件的实现:
httpUploadModule类必须实现IHttpModule接口,而且必须实现它的初始化函数:
///
///
///

///
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(this.Application_BeginRequest);
application.EndRequest += new EventHandler(this.Application_EndRequest);
application.Error += new EventHandler(this.Application_Error);
}
这里就实现的了几个事件。而实现上它的构造函数里什么也没做。
我们先来看看BeginRequest事件,该事件在用户请求页面时发生。
private void Application_BeginRequest(object sender, EventArgs e)
其中seder就是application自己,e是参数(具体的是什么我目前还不明白)
接下来实现了这两行代码:
HttpApplication application = (sender as HttpApplication);
HttpWorkerRequest workerRequest = GetWorkerRequest();
我们自己来处理application和workerRequest,这是的选择来自己处理上传大文件。
关于HttpContent和HttpWorkerRequest可以查看MSDN里的说明。

以下代码是在try块中:

if (!IsUploadRequest(application.Request))
{
return;
}

if (!workerRequest.HasEntityBody())
{
return;
}
用来判断用户的某一请求是否是文件上传请求,如果不是,就返回交给aspnet.dll来处理其它的事。

string contentType = application.Context.Request.ContentType.ToLower();
用来取得请求的类型,测试结果为:
11/2/2005 9:47:09 AM multipart/form-data; boundary=---------------------------7d52ee98033a
也就是说,用户用multipart/form-data的方式向服务器发出了请求。这也是我们想要的,否则我们不与处理。
试着删除Form里的: enctype="multipart/form-data",结果是我们模块不再处理结果了。(当然,这里是我们自己不想处理,交给了IIS自己去处理,而不是我们不能处理。)

byte[] boundaryData = Encoding.ASCII.GetBytes(("\r\n--" + contentType.Substring((contentType.IndexOf("boundary=") + 9))).ToCharArray());
int FileLength = Convert.ToInt32(workerRequest.GetKnownRequestHeader(11));
不知道为什么,我测试的文件大小比真实的文件要大一点点!先不管它了。

byte[] preloadedEntityBody = workerRequest.GetPreloadedEntityBody();
从用户那里取得已经上传上来的数据,里面包括所有的数据,所以我们要分析这里的数据。这是我测试后取得的数据,我上传一个文件文件,里面就一句话。
11/2/2005 10:15:58 AM -----------------------------7d52033a1004ec
Content-Disposition: form-data; name="__VIEWSTATE"

dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
-----------------------------7d52033a1004ec
Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Temp.txt"
Content-Type: text/plain

This is a test!
-----------------------------7d52033a1004ec
Content-Disposition: form-data; name="Button1"

Button
-----------------------------7d52033a1004ec--

11/2/2005 10:15:58 AM :in test file.
看明白了这个文件,相信要处理它不难吧!!我们再看看上传一个二进制文件后的内容,因为是测试,所以用了一个很小的文件。
11/2/2005 10:18:50 AM -----------------------------7d51f321004ec
Content-Disposition: form-data; name="__VIEWSTATE"

dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
-----------------------------7d51f321004ec
Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
Content-Type: image/x-png

?PNG


IHDR ] & ¦??Á gAMA ¯È7?é tEXtSoftware Adobe ImageReadyqÉe< ÂIDATxÚbüÿÿ?Ã0 ?@Ìļ@ü?¿fÇã0 t`b1 V?FÂ] ~>
@Ã)Ð?S½:?@Sÿ ¾ Äÿ?h8:2Ðbs â¯@| ?/´£ h¸: ?ÊúP â@<??c h¤: °q§ñ' ?Ä?ø=@#-ÐÑS~"óA~_¡?å 4R¹Ò9Ý@,ÄË?¸?¯ÒÒR? é?ü@\ ÄeÐÏ2 n£UàÐh £P??Ó???7 ñb ÞLMK h4бE ?ÄPþV îâCÔ0 ???Ø<@, ­d8èà±2hf3ý>{q
??²ñd ?£Ôp? b ¥t æâ @ü?ßñ- ^ ĶPyjc ¾þ?idµ°oú? Ï?¸??É5 ?` '¨?ï?x 4àAà%+ÐÀ#Ò@ü?o 1û tvâH?b§?c@ Á?s(½?}?ØÚs
¹@å?¸?÷C;RÐîugB?_ Pv ±´,u¿§±2T?!´;Ä???Z?mâ%@l
U¯â ?âMPóx??|@¼?×±;RQY Ä?@ì -({ eü~(ÔÄ?u/IE@ ÁB4öÒ?bä*T,?ÚAÅÁ ?ß Ä²@ü ?¿@õxBå?±>ßâC@<*¾
ª®Ê?b Þå??Ò????? |?ù??RY ´??
Ä÷?¸?ï ñW Vbs¤?ú ?ÏP1Å?rf'ÿCrÏc NbFbÌ  ¨§_ñhùÅ
°_Ð"@À 0
?Û?ø/?/Äë¡| ~
ħ?? ?·BÕ,âV þ
į???BõÈq-T]ÔC« | Î?²K¡új ül¨'Aà(×Aë$XDæAÙ ¢R?FEXø?
@e¿!½ ÄLíZ@,ÄO ãÏ "£ÚNÝ
§vâ?Ðl
jáÌ??Ú}?? ñ,h?íZ»ñ5h dÙ! ÄFÐñî7Ð"à´øù µÿ´x³?Ê???P» öF@Õ?ÌÑ â³@|jg9T]/´%B 6x mËÃÜå Äб??¸44?ü?¦?@Rë
­ä@¹PjÇWhÎ?¤C mõ!P?Å¥ ? ??½× x´lç?*b?f××Ðy-?¹¡ò âé4oFËÆ©@üªT®vAÍÌâÛÐr??× ?}
M u&P1P1uÊ]?P}FPþ7 þ-J??X?x@õ3?Z6VP¿¢?ÙØÜ @ å?&úPüG¦?C/¡Å2`?ÿqèo¡bÐ"ç'Òl´hz
Äï¡bÐÖR6ß?; »ÿ ñhkê9Ô,Pkè:´Øµ?X êöB³¾??>@g? ]<Ú2yÄ r@lÏ ?] U?2 E$: µîê? h0·?@³i4Õ?âýPl Ä÷¡ì¨| ´RU Püªö>?­?¤ff=÷#?ý???¤$7?{¹ Å):ø-?ájh(tJ`?v?b¨x?Ç
bç¡?'
`X@î?Ê×CÕ @ÕÎG2W ªî<Ôì~¨x=4° ì¤"?rÐR)¬h?©5@23)G$ ?+ÎÐr.\ú7è © ¡è÷±¤ôóP,?¥lGÖ«?)°?Z?V4!§àÿHö?G?\&4?'ÐA=kM? b?¶&?¡??ÁÑø C lGh+ä??®ÖÚ*?±p¨= m ñaãD°VÎ-d ?G?4=@ &#tp
Ô$ûmj½?Î??ÒÁ^h8@,Ðîõ3¤QE>èÈ?,Tü6ßbäë ¨"à` @ø¦ë@?µúÐì%Í %
?¡?Q?
@ã.ë 3¼3f Dì);4ðAãÜ?Ðqî'Ðñð?ÐA¥#4°A? ZÀÔ-%ðPÚ ?È??V?¾6´>à?FÌGhQt Ï?q` A'5@+ì?Pª'A ?öµVpA#Á Z¦©@ÇS@õÀ
è?ëUh«á´²ª@???8:dM xîÝâ ­?`0B×?@§- 3¨Éô
?CsÃèxø= ~
Ðú7ÈY
Z·?üáe³? 1têð!L ?è¹îÔþ??F«BS?$t$?ZY??H¾?âW@ü?=ß"©ùÍI?ÉÈ9?Ð?ã??âÐÉQ¨{$¡nÍ}Jh{cç¡ó¹;¡õ
 Á°Ø? êI)è,?&RÄÈAü?væ¾CsÎghEþ :4ý ?c~A;~ÌH
hÑ[ßà e³Pà?÷ÐVÝh;ý,41` 4ØWxñCs?´Ùª?hî??×ó -?¢ø?Kx
-oCSòUR
4T?Õ1BZ1°Õi<Ðâ???y¡)?ÂÙ?R4Ò²Pîø Å? ©ô ?o þ©È#  ¸¸s?5L IEND®B`?
-----------------------------7d51f321004ec
Content-Disposition: form-data; name="Button1"

Button
-----------------------------7d51f321004ec--

11/2/2005 10:18:50 AM :in test file.

好了,接下来的任务:1、分析上传的数据,2、分析如何断点续传,因为上面的例子都是在一次请求里就完成的,如果一次请求完成不了呢?。

在分析上传的数据时,这里用到了这样的一个函数:
///
/// Get value from preloaded entity body. Identified by name.
///

///
///
///
private string AnalysePreloadedEntityBody(byte[] preloadedEntityBody, string name)
{
string val = string.Empty;
string preloadedContent = Utils.GetContext().Request.ContentEncoding.GetString(preloadedEntityBody);

if (preloadedContent.Length > 0)
{
int startIndex = ((preloadedContent.IndexOf(("name=\"" + name + "\"")) + 11) + name.Length);
int endIndex = preloadedContent.IndexOf("\r\n", startIndex);
val = preloadedContent.Substring(startIndex, (endIndex - startIndex));
}

return val;
}
输出的结果让我郁闷:(输出的是startIndex和endIndex及函数返回值)
11/2/2005 10:33:55 AM 5ea
11/2/2005 10:33:55 AM a
11/2/2005 10:33:55 AM :in test file.
还是先不管它的输出结果,分析代码吧。
看了半天也不明白这个函数是在干什么,测试输出结果让我很费解,这个函数在一次申请过程中调用过两次?
......输出的是startIndex和endIndex及函数返回值)
11/2/2005 10:55:09 AM 39 start
11/2/2005 10:55:09 AM 41 end
11/2/2005 10:55:09 AM 02 var
......
11/2/2005 10:55:09 AM 41
11/2/2005 10:55:09 AM 41
11/2/2005 10:55:09 AM
我试着上传一个很大的文件,然后得到以下信息:
11/2/2005 11:03:27 AM 39
11/2/2005 11:03:27 AM 43
11/2/2005 11:03:27 AM 0266

11/2/2005 11:03:28 AM 41
11/2/2005 11:03:28 AM 43
11/2/2005 11:03:28 AM 66

11/2/2005 11:04:18 AM 39
11/2/2005 11:04:18 AM 43
11/2/2005 11:04:18 AM 0212

11/2/2005 11:04:18 AM 41
11/2/2005 11:04:18 AM 43
11/2/2005 11:04:18 AM 12
可以这样分析吧:如果再一次请求过程中,提交了所有的数据,那么这个函数只被调用两次(我不知道不是上传文件都会有两次请求?)
否则会么多次请求,而且在上传没有结束之前,这个函数返回值都不为空。。。。。我也只能分析这些了。

看调用它的主函数的处理方法吧:
string uploadGuid = this.AnalysePreloadedEntityBody(preloadedEntityBody, "Sunrise_Web_Upload_UploadGUID");
if (uploadGuid != string.Empty)
{
application.Context.Items.Add("Sunrise_Web_Upload_UploadGUID", uploadGuid);
}
当然,在第一次调用this.AnalysePreloadedEntityBody的时候,返回不为空,所以会在application里添加一个Item
而里面记录的就是我们this.AnalysePreloadedEntityBody返回的值。。。。

string uploadFolder = this.AnalysePreloadedEntityBody(preloadedEntityBody, "Sunrise_Web_Upload_UploadFolder");
这里是第二次调用该方法,用于取回上传文件的临时路径。应该明白了,this.AnalysePreloadedEntityBody用来分析和读取用户上传的一些数据。这里主要是分析一些参数。

ArrayList readBody = new ArrayList();
RequestStream preloadedStream = new RequestStream(preloadedEntityBody, boundaryData,
null, RequestStream.FileStatus.Close, RequestStream.ReadStatus.NoRead, uploadFolder, isUploadFinished, application.Context, string.Empty);
这里是它的一个核心类了。看看它的构造函数!
核心在这个循环上了:
while ((preloadPosition < preloadBytes.Length))
代码太长,一下子还不好分析。。。郁闷中。。。。

没有再向下分析代码了,理由是我目前对用户提交上来的内容还不清楚,不知道应该怎样处理用户提交上来的数据。因此决定自己先把前面的内容搞清楚,于是自己写了一个HttpModule来测试。
经过这个模块的测试,终于对HTTP协议的请求有了一个全面的认识,当然这只是我自己推测的,至于对不对,那就不好说了。

先看看我写的代码:

using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web;
using System.Xml;

namespace WebbTest
{
///
/// Summary description for UploadTest.
///

public class HttpModuleTest : IHttpModule
{
private System.DateTime m_startTime;

public HttpModuleTest()
{
//
// TODO: Add constructor logic here
//
m_startTime = System.DateTime.Now;
WebbSystem.TraceMsg("Construct the HttpModule.");
}

public void Dispose()
{
}

#region initialization function
///
///
///

///
public void Init(System.Web.HttpApplication m_application)
{
m_application.BeginRequest += new EventHandler(m_application_BeginRequest);
m_application.EndRequest += new EventHandler(m_application_EndRequest);
m_application.Error += new EventHandler(m_application_Error);
TimeSpan m_timeSpan = System.DateTime.Now.Subtract(m_startTime);
WebbSystem.TraceMsg("Init in the HttpModule."+m_timeSpan.TotalSeconds.ToString());
}
#endregion

#region Event functions
///
///
///

///
///
private void m_application_BeginRequest(object sender, EventArgs e)
{
#region debug message
TimeSpan m_timeSpan = System.DateTime.Now.Subtract(m_startTime);
WebbSystem.TraceMsg("m_application_BeginRequest in the HttpModule."+m_timeSpan.TotalSeconds.ToString());
#endregion

HttpApplication m_application = (sender as HttpApplication);
HttpWorkerRequest m_workerRequest = GetWorkerRequest();
//WebbSystem.TraceMsg(m_application.Request.ContentType.ToString());

byte[] m_preLoadedData = m_workerRequest.GetPreloadedEntityBody();
// Encoding targetEncoding;
// targetEncoding.EncodingName = Encoding.UTF8;
if(m_preLoadedData!=null)
{
string m_temp = Encoding.UTF8.GetString(m_preLoadedData);
WebbSystem.TraceMsg(m_temp);
}
}

private void m_application_EndRequest(object sender, EventArgs e)
{
TimeSpan m_timeSpan = System.DateTime.Now.Subtract(m_startTime);
WebbSystem.TraceMsg("m_application_EndRequest in the HttpModule."+m_timeSpan.TotalSeconds.ToString());
//Do some thing to release resouce.
HttpApplication application = (sender as HttpApplication);
application.Context.Items.Clear();

}

private void m_application_Error(object sender, EventArgs e)
{
TimeSpan m_timeSpan = System.DateTime.Now.Subtract(m_startTime);
WebbSystem.TraceMsg("Error in the HttpModule."+m_timeSpan.TotalSeconds.ToString());
//Do some thing to release resouce.
}
#endregion

#region Assistant functions
private HttpWorkerRequest GetWorkerRequest()
{
IServiceProvider provider = HttpContext.Current;
return ((HttpWorkerRequest) provider.GetService(typeof (HttpWorkerRequest)));
}
#endregion
}
}
其中WebbSystem.TraceMsg是一个向文本文件中写入记录的辅助函数,用于查看结果。最后我得到的内容:
当第一次请求的时候:
11/2/2005 2:47:16 PM m_application_BeginRequest in the HttpModule.59.28125
11/2/2005 2:47:16 PM
11/2/2005 2:47:16 PM m_application_EndRequest in the HttpModule.59.28125

点击一个button后的结果,当然我这个页面是multipart/form-data方法的
11/2/2005 2:46:30 PM m_application_BeginRequest in the HttpModule.13.71875
11/2/2005 2:46:30 PM multipart/form-data; boundary=---------------------------7d51a51e25012c
11/2/2005 2:46:30 PM -----------------------------7d51a51e25012c
Content-Disposition: form-data; name="__VIEWSTATE"

dDwtNTMwNzcxMzI0Ozs+eJ8D5aYupVVxznfxhPOz74IwJsk=
-----------------------------7d51a51e25012c
Content-Disposition: form-data; name="m_file"; filename=""
Content-Type: application/octet-stream


-----------------------------7d51a51e25012c
Content-Disposition: form-data; name="Button1"

Button
-----------------------------7d51a51e25012c--

11/2/2005 2:46:30 PM m_application_EndRequest in the HttpModule.13.71875
只要我不关闭浏览器,那么boundary的值都是一定的,也就是说
contenttype给出了请求的格式:multipart/form-data; boundary=---------------------------7d51a51e25012c
就是说:这是一个文件上传请求,用---------------------------7d51a51e25012c来分隔数据,所以上而的数据就是那样的了。
于是,在每次处理提交上来的数据的时候,一定要先处理conetntTpye然后再来通过它来处理上传的数据。最后用--来结束上传数据。
数据名与内容用 "\r\n\r\n"来分隔。好了,下面我自己来写一个函数来处理上传的数据。

分析提交的数据,其中有一项基本的功能就是要从数据里取得表单数据,例如文本输入框等,虽然这可能都交给ASP.net的模块去做,但我们还是得自己处理一下,至少也应该提供一个函数来处理它。
SunriseUpload里用的是这样的一个函数来处理它的:
private string AnalysePreloadedEntityBody(byte[] preloadedEntityBody, string name)
{
string val = string.Empty;
string preloadedContent = Utils.GetContext().Request.ContentEncoding.GetString(preloadedEntityBody);
if (preloadedContent.Length > 0)
{
int startIndex = ((preloadedContent.IndexOf(("name=\"" + name + "\"")) + 11) + name.Length);
int endIndex = preloadedContent.IndexOf("\r\n", startIndex);
val = preloadedContent.Substring(startIndex, (endIndex - startIndex));
}
return val;
}
然而它会出一个小问题,就是如果我在一个文本输入域里输入了一个回车和换行,那么它将截断字符串,因此我也写了一下:
private string AnalysePreLoadedData(byte[] m_preLoadedData, string m_fiedName)
{
string m_returnData = string.Empty;
string preloadedContent = Aidance.GetContext().Request.ContentEncoding.GetString(m_preLoadedData);
if (preloadedContent.Length > 0)
{
int startIndex = ((preloadedContent.IndexOf(("name=\"" + m_fiedName + "\"")) + 11) + m_fiedName.Length);
// int endIndex = preloadedContent.IndexOf("\r\n", startIndex);
int endIndex = preloadedContent.IndexOf(m_boundary,startIndex);
m_returnData = preloadedContent.Substring(startIndex, (endIndex - startIndex));
}
return m_returnData;
}
这里的m_boundary是前面已经处理过的数据分隔标识。当然我还是觉得这样用字符串来处理不是很高效,但又不知道应该怎样做。

另一个不太明白的就是:
int m_flieSize = Convert.ToInt32(m_workerRequest.GetKnownRequestHeader(11));
我试了用循环来处理所有的m_workerRequest.GetKnownRequestHeader,最后还得到了URL及文件名,而11所得到的也不是决对的文件长度,只是很接近,而且差的数据是一个固定的,我猜这个差值应该是在表单里占用了。
但上传小文件(<48K)的,都将没有区别,它和上传所读取的数据是一致的。也就是说如果一个上传文件和表单数据可以在一次传输中完成,那么这个数据就等于读取数据。因此这里(m_workerRequest.GetKnownRequestHeader所得到的并不是上传文件的大小,但我们可以用它为粗略的判断一下文件的大小,因为毕竟当文件很大的时候(几百MB),表单里的一些数据可以忽略不计的。
最后说明的是,这里的index最多只能到38,在太就出错。。。。我不知道这个index到底是什么?

好了,不管了先,下面自己写这个RequestStream类。仿照原来的做法,类的参数,属性以及构造函数都没变(改了一下变量名,呵呵,用我喜欢的名字)。
using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Web;

namespace WebbTest
{
///
/// Summary description for RequestStream.
///

public class RequestStream
{
#region Field
private ArrayList m_contentBody;
private FileStatus m_fileStatus;
private FileStream m_fs;
private ArrayList m_readBody;
private ReadStatus m_readStatus;
private string m_originalFileName;
#endregion

public enum FileStatus : byte
{
// Fields
Close = 1,
Open = 0
}

public enum ReadStatus : byte
{
// Fields
NoRead = 0,
Read = 1
}

#region Propertis
public ArrayList ContentBody
{
get { return this.m_contentBody;}
}

public FileStream FileStream
{
get { return this.m_fs; }
}

public FileStatus FStatus
{
get { return this.m_fileStatus; }
}

public string OriginalFileName
{
get { return this.m_originalFileName; }
}

public ArrayList ReadBody
{
get { return this.m_readBody; }
}

public ReadStatus RStatus
{
get { return this.m_readStatus; }
}

#endregion

public RequestStream( byte[] preloadBytes,
byte[] boundaryBytes,
FileStream fileStream,
FileStatus fileStatus,
ReadStatus readStatus,
string uploadFolder,
bool writeToDisk,
HttpContext context,
string currFileName)
{
#region init variables
this.m_readBody = new ArrayList();
this.m_contentBody = new ArrayList();
//
this.m_fs = null;
this.m_originalFileName = string.Empty;
this.m_fileStatus = FileStatus.Close;
this.m_readStatus = ReadStatus.NoRead;
//
this.m_fs = fileStream;
this.m_originalFileName = currFileName;
this.m_fileStatus = fileStatus;
this.m_readStatus = readStatus;
#endregion

}
}
}

TNND,这个函数太长了,没能写完。。。。。明天再写。。。

我没有接着昨天的函数分析下去,理由是以下两点:
1、RequestStream的作用已经很明确了,可以跳过函数本身的算法,而去研究这例子里的其它内容。RequestStream的作用就

是把从HttpRequestContent里取得的数据分析出来,其中有一部份是文本,另一部份是上传的二进制数据。
当我们实例化一次RequestStream后,它就会把当前请求的数据分析完。确切的说是把已经从用户那读取的数据进行处理。
那么椄下来的任务就是处理后面还没有处理完的数据了。
2、作者的一些算法真的很勉强,太浪费资源了。或许这就是为什么BestCompy的AspNetUpload的效率要高出很多的原因,不用

说,一定是在数据处理的算法上有所突破,而大文件上传的本质是没有变的。。
好了,先把RequestStream类的构造函数放在一边,只用知道它是在帮助我们处理已经上传的数据就行了,为了分析后面的代

码,完成可以不理会已经上传的数据,直接分析后面的代码,,,当然,这样速度一定会比处理数据时快很多。。。

好了,回到HttpMoudle模块里:
在处理完RequestStream后,接下就这样的内容:
if (!workerRequest.IsEntireEntityBodyIsPreloaded())
{
//从请求里读取数据。

while(boundarySize = workerRequest.ReadEntityBody(boudaryBuffer, boudaryBuffer.Length)>0){
//再次处理读取的数据。这里再次读取的数据在数组boudaryBuffer里,这里我省略了它的申明。
//但要还要注意其它的事情,就是用户的链接是否还有效。。。作者在读取数据前判断了一次
//我觉得不用判断,因为读取数据为0的时候就说明请求已经完成了,至于是文件上传完了,还是中断了,可以在后面进

行判断,而不用在处理上传数据的时候来判断用户是否在链接状态。。。好了 ,不管了先。。。

};

}
如果还有数据要读取,也就是说,用户的数据还没有读取完,那么就要接着处理。我做了一个简单的输出测试,得到的结果:
11/3/2005 10:08:01 AM m_application_BeginRequest in the HttpModule.6.859375
11/3/2005 10:08:01 AM Total size:650001432;Current:253952
11/3/2005 10:08:01 AM Total size:650001432;Current:458752
11/3/2005 10:08:01 AM Total size:650001432;Current:663552
11/3/2005 10:08:01 AM Total size:650001432;Current:868352
11/3/2005 10:08:01 AM Total size:650001432;Current:1073152
11/3/2005 10:08:01 AM Total size:650001432;Current:1277952
11/3/2005 10:08:01 AM Total size:650001432;Current:1482752
11/3/2005 10:08:01 AM Total size:650001432;Current:1687552
11/3/2005 10:08:01 AM Total size:650001432;Current:1892352
11/3/2005 10:08:01 AM Total size:650001432;Current:2097152
11/3/2005 10:08:01 AM Total size:650001432;Current:2301952
11/3/2005 10:08:01 AM Total size:650001432;Current:2506752
11/3/2005 10:08:01 AM Total size:650001432;Current:2711552
11/3/2005 10:08:02 AM Total size:650001432;Current:2916352
11/3/2005 10:08:02 AM Total size:650001432;Current:3121152
11/3/2005 10:08:02 AM Total size:650001432;Current:3325952
11/3/2005 10:08:02 AM Total size:650001432;Current:3530752
11/3/2005 10:08:02 AM Total size:650001432;Current:3735552
......
11/3/2005 10:08:45 AM Total size:650001432;Current:341450752
11/3/2005 10:08:45 AM Total size:650001432;Current:341655552
11/3/2005 10:08:46 AM Total size:650001432;Current:341860352
呵呵,可以大概的估算一下,我这里上传的速度大概是7MB/S,当然这是因为我只上传数据而没有做任何数据处理,速度当然

会快一些。。(ASPNetUpload:同样的测试环境4MB/s,Lion的上传组件:2.5MB/s)而源文件编译后的上传组件为2.0MB/s这是

都只是大概的数据,可以说明AspNetUpload确实在算法上有所改进,而Lion的传组件可能改进的不多。
还有,我本来想在多线程里处理上传数据的,但出于以下几个原因,用多线程处理它的可能性不大:
1、ASP.net的运行环境本身就是多线程的。
2、问题的关键不再于服务器上组件的处理能力,与网络带宽很相关,所以就算在服务器上有很高的处理速度,而带宽跟不上

也是白搭。而实现上能达到5MB/s的通信量的带宽并不多。所以先不考虑用多线程来处理上传数据吧。。。
好了,最后的工作已经很明确了,
1、处理上传的文件数据,把它存在在临时文件夹里;
2、错误处理;
在此,对SunriseUpload.0.9.1源代码的分析就先放在这里,后期会对此做些补充。
最后,SunriseUpload.0.9.1源代码的下载地址:
2005.net的引用地址:
http://blog.aspcool.com/rogerwang/archive/2005/10/18/1841.html#3115
下载地址:
http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=fe16801f-2d2c-49fd-be31-d28135365379
最后说一下,getdotnet上有很多开源的内容可下载,有时间可以在上面搜索着试试看。。。。。

代码注释如下

License#region License

/**//*
* SunriseUpload - Asp.net Upload Component
*
* Copyright (C) 2004 mic
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* In case your copy of SunriseUpload does not include a copy of the license, you may find it online at
* http://www.gnu.org/copyleft/gpl.html
*
* You can find new release of this component at http://athena.9966.org/SunriseUpload .
*/

#endregion

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Web;

namespace Sunrise.Web.Upload
{
internal class RequestStream
{
Fields#region Fields

private ArrayList contentBody;
private FileStatus fileStatus;
private FileStream fs;
private ArrayList readBody;
private ReadStatus readStatus;
private string originalFileName;

#endregion

// Nested Types
public enum FileStatus : byte
{
// Fields
Close = 1,
Open = 0
}

public enum ReadStatus : byte
{
// Fields
NoRead = 0,
Read = 1
}

Properties#region Properties

public ArrayList ContentBody
{
get { return this.contentBody; }
}

public FileStream FileStream
{
get { return this.fs; }
}

public FileStatus FStatus
{
get { return this.fileStatus; }
}

public string OriginalFileName
{
get { return this.originalFileName; }
}

public ArrayList ReadBody
{
get { return this.readBody; }
}

public ReadStatus RStatus
{
get { return this.readStatus; }
}

#endregion

/**////
///
///

/// Already upload data.
///
/// Stream to output data
///
///
///
///
/// HttpContext,contaion the data uploaded by user.
/// current file name?
public RequestStream( byte[] preloadBytes,
byte[] boundaryBytes,
FileStream fileStream,
FileStatus fileStatus,
ReadStatus readStatus,
string uploadFolder,
bool writeToDisk,
HttpContext context,
string currFileName)
{
this.readBody = new ArrayList();
this.contentBody = new ArrayList();
this.fs = null;
this.originalFileName = string.Empty;
this.fileStatus = FileStatus.Close;
this.readStatus = ReadStatus.NoRead;
this.originalFileName = currFileName;
this.fs = fileStream;
this.fileStatus = fileStatus;
this.readStatus = readStatus;

int preloadPosition = 0;
while ((preloadPosition < preloadBytes.Length))
{
ArrayList list1 = new ArrayList();
#region
//说真的,我不是很喜欢这样的算法,在没明白上传数据的结构的时候,就不知道它在干什么!!!
//当然,如果在ASP里写过上传组件的人,应该都可以在ASP.net下写出很好的上传组件。
if (preloadBytes[preloadPosition] == 13)
{
//如果是回车,那么要处理回车后的数据。这是分析的重重点。。。
#region
int boundaryPosition = 0;
while (((preloadPosition < preloadBytes.Length) && (boundaryPosition < boundaryBytes.Length)))
{
//分析数据是否是分隔字符。
#region
if (preloadBytes[preloadPosition] != boundaryBytes[boundaryPosition])
{
break;
}
list1.Add(preloadBytes[preloadPosition]);
boundaryPosition++;
preloadPosition++;
#endregion
}

if (boundaryPosition == boundaryBytes.Length)
{
//如果遇到一个分隔符,那么就处理分隔符后面的数据
#region
if (this.fileStatus == FileStatus.Open)
{
//如果文件是打开的,那么要保存文件了,因为这里可能是一个文件的结束。
//显然,在第一次执行这里的时候,是不可能有文件保存的。
this.fs.Flush();
this.fs.Close();
this.fileStatus = FileStatus.Close;
this.originalFileName = string.Empty;
}
else if (this.readStatus == ReadStatus.NoRead)
{
//否则,如果记取标记为不可读,??
//设定为可读,以便后面处理??
this.readStatus = ReadStatus.Read;
context.Items["Sunrise_Web_Upload_FileStatus"] = this.fileStatus;
}

if ((preloadPosition + 2) < preloadBytes.Length)
{
//如果遇到一个分隔符后,还有数据,那么要处理后面的数据了,
//这里可能是一个文件的开始,也可能是文本数据的开始。
#region
list1.Add(preloadBytes[preloadPosition]);
preloadPosition++;
list1.Add(preloadBytes[preloadPosition]);
preloadPosition++;
//上面把两个字节的数据存入到preloadBytes里去,因为这是可用的文本信息。

ArrayList list2 = new ArrayList();
while ((preloadPosition < preloadBytes.Length))
{
//这里要先分析分隔符后的头,看它是文本还是上传文件。
list1.Add(preloadBytes[preloadPosition]);
#region
if (preloadBytes[preloadPosition] == 13)
{
//如果遇到一个回车,就是说一个contenHead结束了。。。
//list2里记录了分隔符后到正式数据开始之间的内容。
byte[] buffer = new byte[list2.Count];
list2.CopyTo(buffer);

//Get request content from buffer
string requestContent = Utils.GetContext().Request.ContentEncoding.GetString(buffer);
//分析这个contenHead,它是否是上传的文件。
//If this line is content head

if (requestContent.IndexOf("\"; filename=\"") > 0)
{
//如果是上传的文件,而不是文本数据。。。
//处理上传的文件数据。。。。。这里是最核心的内容了。。
//因为这里可能是一个文件的一部份数据,而不是所有的内容。。
//但也有可能是一个完整的文件数据。。。
preloadPosition++;
ArrayList list3 = new ArrayList();
//list3来记录文件数据。
#region
while ((preloadPosition < preloadBytes.Length))
{
//处理文件数据,当然要在读取数据小于上传数据的时候
//在文件数据前面,还有一段文件类型的识别,因此要先处理它,
//这是一段文本,因此要添加到contentBody里去,这里用list1来记录
//出了循环后,会添加到contentBody里去。
//同时,list3记录一文件的类型信息
list1.Add(preloadBytes[preloadPosition]);
#region
if (preloadBytes[preloadPosition] == 13)
{
//再次遇到一个回车的时候,就是文件类型信息结束的时候。。。。
#region
if ((preloadPosition + 3) < preloadBytes.Length)
{
//后面还有数据,这里才是真正的文件数据。
char[] spliter = new char[1]{';'};
//分析文件名及一些相关信息。。。
//这是一个例子,requestContent里的数据可能是这样的:
//Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
string[] contentArray = requestContent.Split(spliter);
string fileNameString = contentArray[2].Trim();
fileNameString = fileNameString.Substring(10, (fileNameString.Length - 11));
#region
if ((writeToDisk && (fileNameString != null)) && (fileNameString != string.Empty))
{
//如果设定为可以写入磁盘,而且给定的文件名存在,那么把数据写入磁盘,否则可以不做处理。
this.originalFileName = Path.GetFileName(fileNameString);
preloadPosition += 3;

//这里list3里记录的是什么?前面分析的好像有点问题。。。。
byte[] buffer2 = new byte[list3.Count];
list3.CopyTo(buffer2);
//没错,这里记录的是上传文件的类型。例如:Content-Type: image/x-png
string contentType = Utils.GetContext().Request.ContentEncoding.GetString(buffer2);
string guidFileName = (Guid.NewGuid().ToString() + Path.GetExtension(fileNameString));
string fileFullPath = uploadFolder;
//If upload folder deos not exist, use system temporary folder to hold the file.
if (fileFullPath == string.Empty)
{
fileFullPath = Path.GetTempPath();
}
//设定文件的临时文件名,用GUID来设定。
fileFullPath = Path.Combine(fileFullPath, guidFileName);
//Build the content head
StringBuilder sb = new StringBuilder();
string[] sbArray = new string[11];
//后面的注释为数据的可能值:或者是示例值:
sbArray[0] = "\r\n";
sbArray[1] = contentArray[0]; //Content-Disposition: form-data
sbArray[2] = ";";
sbArray[3] = contentArray[1]; //name="m_file"
sbArray[4] = "\r\n\r\n";
sbArray[5] = contentType.Trim(); //Content-Type: image/x-png
sbArray[6] = ";filename=\"";
sbArray[7] = fileNameString; //D:\WuCountry\Pictures\logo.png
sbArray[8] = "\";filepath=\"";
sbArray[9] = guidFileName; //文件的GUID文件名值。
sbArray[10] = "\"";
sb.Append(string.Concat(sbArray));

//Save boundary bytes to server file
//创建文件,准备写入文件流。。。并将该信息写入到HttpContent里去,
//因为有可能一次写不完一个文件,还要在下次读取数据的时候用它。
this.fs = new FileStream(fileFullPath, FileMode.Create);
context.Items["Sunrise_Web_Upload_FileStream"] = this.fs;
this.fileStatus = FileStatus.Open;
//为下次读取写入文件做准备
context.Items["Sunrise_Web_Upload_FileStatus"] = this.fileStatus;
//有可能为多个文件上传,这里先写入一个文件的信息到HttpContent里去,这样就可以取得上传文件信息了
Hashtable ht = ((Hashtable) context.Items["Sunrise_Web_Upload_FileList"]);
ht.Add(Path.GetFileNameWithoutExtension(guidFileName), fileFullPath);
context.Items["Sunrise_Web_Upload_FileList"] = ht;
//将信息记录在readBody里。。。
this.readBody.AddRange(boundaryBytes);
this.readBody.AddRange(Encoding.UTF8.GetBytes(sb.ToString().ToCharArray()));
sb.Remove(0, sb.Length);
break;
}
#endregion
preloadPosition++;
break;
}
#endregion
this.contentBody.AddRange(list1);
break;
}
#endregion
list3.Add(preloadBytes[preloadPosition]);
preloadPosition++;
}
#endregion
//处理完文件数据后,要跳出循环,让程序去查找下一个分隔符的位置。
break;
}
this.readStatus = ReadStatus.NoRead;
this.readBody.AddRange(list1);
break;
}
#endregion
list2.Add(preloadBytes[preloadPosition]);
preloadPosition++;
}


if (preloadPosition < preloadBytes.Length)
{
preloadPosition++;
continue;
}

this.contentBody.AddRange(list1);
preloadPosition++;
continue;
/**//*
if ((preloadBytes[preloadPosition] != 0x2d) || ((preloadPosition + 3) >= preloadBytes.Length))
{
preloadPosition++;
continue;
}
this.readBody.AddRange(boundaryBytes);
byte[] buffer3 = new byte[4] {0x2d, 0x2d, 13, 10};
this.readBody.AddRange(buffer3);
return;
*/
#endregion
}

this.contentBody.AddRange(list1);
#endregion
}
else if (preloadPosition < preloadBytes.Length)
{
//否则,如果已经分析的数据小于已经上传的数据,就继续处理数据。
#region
if (this.fileStatus == FileStatus.Open)
{
byte[] buffer4 = new byte[list1.Count];
for(int i = 0; i < list1.Count; i++)
{
buffer4[i] = ((byte) list1[i]);
}

this.fs.Write(buffer4, 0, buffer4.Length);
}
else if (this.readStatus == ReadStatus.NoRead)
{
this.readBody.AddRange(list1);
}

preloadPosition --;
#endregion
}
else
{
//否则,已经分析的数据等于上传数据,因此把数据添加到preloadBytes里去。
this.contentBody.AddRange(list1);
}
#endregion
}
else if (this.fileStatus == FileStatus.Open)
{
//这里处理文件数据

//否则,如果文件是打开的,那么可以写入到文件里去。
//这里要注意,文件的可写状态在前面设定,因为第一个字节一定不是回车,因此一开始,有很多数据都只是放在preloadBytes里去了,
//里面更多的是文本内容。。。。而实际上,preloadBytes里就是存放上传时的读取的文本内容。。。
this.fs.WriteByte(preloadBytes[preloadPosition]);
}
else if(this.readStatus == ReadStatus.NoRead)
{
//这里处理文本数据。

//如果不可读,那么只加入到preloadBytes里去。
//这里加入到preloadBytes里是为了在后面读取文本信息时用。。。
this.readBody.Add(preloadBytes[preloadPosition]);
}
#endregion
preloadPosition++;
}
}

/**////
/// Destructor method, release all resource
///

~ RequestStream()
{
this.readBody = null;
this.contentBody = null;
this.fs = null;
}
}
}

接着分析了几个小时的SunriseUpload.0.9.1的源码。
终于明白了作者的整体思路。在此就做一个总结。

首先,要想能上传很大的文件,我们就必须编写一个HttpModule来自己处理用户上传的信息。这个模块可以拦截用户所有的请求,因此有必须选择性的做一此判断。如果是mulitypart/form-data请求时,会有这样的一个ContentHeader在请求数据里:
multipart/form-data; boundary=---------------------------7d51a51e25012c
我们可以从m_application.Request.ContentType.ToLower()的方法取得这个数据,注意boundary后面的数据是随机的,它是用来区分上传变量名与数据的分隔符,因此为了能分析后面的数据,必须先把它取出来。
然后就是从用户的请求那里读取数据,第一次读取数据的时候,我们可以用:
m_workRequest.GetPreloadedEntityBody();
其中m_workRequest是HttpWorkerRequest的对象实例。如果用户提交的数据不是很长,那么一次性就可以读取完了。而我们最主要的目的就是为了分析这里的数据。
直接输出里面的数据可以得到类似这样的内容,为了方便说明,我加上了行号:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG

IHDR ] & |??á gAMA ˉè7?é(这里是上传的文件二进制数据,我删除了一些)
[10]-----------------------------7d51f321004ec
[11]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[12]Content-Type: image/x-png
[13]
[14]?PNG

IHDR ] & |??á gAMA ˉè7?é tEXtSof(这里是上传的文件二进制数据,我删除了一些)
[15]-----------------------------7d51f321004ec
[16]Content-Disposition: form-data; name="Button1"
[17]
[18]Button
[19]-----------------------------7d51f321004ec--
这里,乱码是上传的二进制文件(删除了一些,而且假设有两个文件上传)。可以看到,其中有一个__VIEWSTATE(02行)表单变量名,它是ASP.net自己维护的信息,我们就不说了。还有一个Button1(16行),它的值是Button,其它的是两个上传的二进制文件。注意,这里的分隔符比ContentType里的多两个字节,而这两个字节就是"--",数一下就知道了,后面的多两个。不知道为什么?我们可以看到,第09行的数据和第14行的数据就是我们上传的文件,这里我删除了大部分,只留了一点点做例子。看看作者的思想,作者想在这些数据到达页面以前,我们先把它处理一次,作者是这样处理的,他想让用我们的模块处理后的内容变成下面的样子,为了方便说明,我加上了行号:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="Sunrise_Web_Upload_UploadGUID"
[03]
[04]d922d57d-c1ef-4c02-a3d4-30fae78cb599
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="__VIEWSTATE"
[07]
[08]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[09]-----------------------------7d51f321004ec
[10]Content-Disposition: form-data; name="m_file"
[11]
[12]Content-Type: image/x-png;filename="D:\WuCountry\Pictures\logo.png";filepath="a661ab10-312e-4372-93e6-f37d662ca38f.png"
[13]-----------------------------7d51f321004ec
[14]Content-Disposition: form-data; name="m_file"
[15]
[16]Content-Type: image/x-png;filename="D:\WuCountry\Pictures\logo.png";filepath="sd328d7a-312e-4372-93e6-f37d662ca38f.png"
[17]-----------------------------7d51f321004ec
[18]Content-Disposition: form-data; name="Button1"
[19]
[20]Button
[21]-----------------------------7d51f321004ec--
其中多了一个Upload_GUID,它是用来唯一分一个上传任务的,我们可以不管它,主要是为了处理上传进度条。好了,这样一来,所有的数据都是文本的了,那么二进制的文件到什么地方去了呢?就是filepath="a661ab10-312e-4372-93e6-f37d662ca38f.png"这就记录了文件的位置,也是用GUID生成的唯一文件名,它存放在系统的临时目录里,也就是说,还在我们自己去把它COPY到想存放的目录,这是很容易的,用一个Move就行了。
好了,思路已经很明确了,那么就只用来处理数据了,也就是上一篇文章的核心ReqponseStream类的算法。
这里请注意,因为数据并不是一次提交上来的,上面的数据在任何一个地方都有可能出现断点问题,因此我们不防假设第一次数据断在源请求文件的第09行的某个位置,那么我们用同m_workRequest.GetPreloadedEntityBody();取得的数据可能就是这样的:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG

IHDR ]
而后我们用m_workRequest.ReadEntityBody(m_readBuffer,m_bufferSize);取得的数据可能是这样的,我们再假设第二个断点在14行
[09] ] & |??á gAMA ˉè7?é(这里是上传的文件二进制数据,我删除了一些)
[10]-----------------------------7d51f321004ec
[11]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[12]Content-Type: image/x-png
[13]
[14]?PNG

IHDR ] &
更多可能的是取得的数据全部是文件数据,也就是全部是二进制。那么应该怎样处理这个问题呢?通过研究作者的算法,作者是这样做的:
RequestStream类有一个m_contentTextBody(这是我自己取的名字),它用来处理完读取的数据后记录回文本信息,也就是说最后这里面的内容就是我们想生成的所有的文本内容。然后把文件存在在临时文件夹里,但由于文件可能没有传完,所以在RequestStream类里还必须记录上传文件的一些信息。作者想在第二次提交的数据处理的时候,再把前面的m_contentTextBody再传回RequestStream类来处理,也就是说第二次处理数据时,原本应该全部都是二进制数据了,但作者把数据改为这样的:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG

IHDR ] & (这一行是第二次读取时真正的数据,前面的8行都是添加上去的)
这样一来,就感觉又是一个新的请求了,可以当成是第一次请求那样处理数据。但这次不能重新打开新的文件流写入数据,而应该还用上一次的文件流来写入上一次的文件中。当遇到一个文件结束的时候,关闭前一个文件。如果遇到第二个文件就再来打开一个文件流来填写数据。
我觉得这是很不好的,因为每次把数据当成是第一次的样子来处理,这样很浪费资源,可以看的出来,每次都要多处理好多字节的数据。而在大文件上传的时候更是不能忽略这些数据了,而在算法里,把字节数组的复制也搞的很复杂。这是我觉得作者最不可取的地方。因为每次处理读取的数据,都会NEW一个RequestStream对象,这样太浪费资源了。(我决定重新改写这一算法。)
好了,最后一个任务就是把m_contentTextBody添加到RequestContent里去,否则我们不能在后来的处理中得到数据,因此还有一点麻烦,作者用到了C#的反射功能,得到IIS的应用程序域(我还不知道是不是),然后添加进数据,这是核心代码:
private byte[] InjectTextParts(HttpWorkerRequest request, byte[] textParts)
{
Type type;
BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Instance);
//Is there application host IIS6.0?
if (Utils.GetContext().Request.ServerVariables["SERVER_SOFTWARE"].Equals("Microsoft-IIS/6.0"))
{
type = request.GetType().BaseType.BaseType;
}
else
{
type = request.GetType().BaseType;
}
int dataLength = textParts.Length;
//Set values of working request
type.GetField("_contentAvailLength", flags).SetValue(request, dataLength);
type.GetField("_contentTotalLength", flags).SetValue(request, dataLength);
type.GetField("_preloadedContent", flags).SetValue(request, textParts);
type.GetField("_preloadedContentRead", flags).SetValue(request, true);
return textParts;
}
好了,还只剩下最后一个,就是上传进度条的处理问题了。在后面的文章里会继续分析吧,其实上我也开始在写自己的上传组件了,当然一些技术上的方法还得用作者的思想,但一些算法或者一些我觉得自己有突破的地方,我会做一些修改的。



<< Home

This page is powered by Blogger. Isn't yours?