ITPub博客

首页 > Linux操作系统 > Linux操作系统 > Asp.Net 上传大文件专题(3)--从请求流中获取数据并保存为文件[下]

Asp.Net 上传大文件专题(3)--从请求流中获取数据并保存为文件[下]

原创 Linux操作系统 作者:iDotNetSpace 时间:2009-06-03 17:07:25 0 删除 编辑

     3.4 读取剩余的请求
      前面我们已经提到过ReadEntityBody (Byte[] buffer, Int32 size)方法,该方法可以用来读取客户端的请求数据。我们想要读取剩余部分的请求数据,就是要使用这个方法来从异名管道中循环取出请求。 [buffer:将数据读入的字节数组;size:最多读取的字节数;如果所被读取的剩余请求字节长度小于size,那么该方法会将多余的size大小的字节数组用0填充,这样会损失不必要的性能,因此我们在使用该方法前最好先判断下剩余的HTTP请求大小与size的关系。据其他前辈们测试该方法大多数读取的数据长度都在8192左右,所以size不必定的很大。]
      
读取剩余请求
while (iLeave > iReadStepSize && request.IsClientConnected())
{//首先判断剩余的请求大小是否大于iReadStepSize

    iRead = request.ReadEntityBody(bReadStepByte, iReadStepSize);
    /**//*读取最大字节数为iReadStepSize的用户请求到bReadStepByte数组中*/
    
     对文件内容的处理

    iLeave -= iRead;
}
if (iLeave > 0 && request.IsClientConnected())
{
    iRead = request.ReadEntityBody(bReadStepByte, iLeave);
    /**//*最后还剩一部分,因为小于iReadStepSize,所以写在循环外*/
  
      对文件内容的处理
}

      3.5 从请求中截取上传数据,并将除去文件数据后的请求写入缓存
      因为文件上传的特殊编码方式采用分隔符来分隔不同的内容,所以我们只要利用分隔符便能精确的获取文件数据所在的区间,然后将其中的数据截取。看起来是不是很简单呢?
      我们先来看一下文件上传的HTTP请求内容的实际存在形式(因为内容比较多,我选取其中一部分,其中的序号是为了方便说明加上去的):

 

 01 -----------------------------7d87d1cc0a88
 02 Content-Disposition: form-data; name="tbVideoName"
 03
 04 vnm
 05 -----------------------------7d87d1cc0a88
 06 Content-Disposition: form-data; name="file"; filename="C:\Documents and Settings\stg609\妗岄潰\浣冲彞.txt"
 07 Content-Type: text/plain
 08
 09 这里是我上传的文本内容
 10 -----------------------------7d87d1cc0a88
 11 Content-Disposition: form-data; name="__EVENTVALIDATION"
 12
 13 /wEWBgK0g/7JCQLrqYKOBgKj5pr/CAKBmOPQBQLY14yNBQKmmtpNX1cOVXyqN8xEER3ZXbnXzsUwVVo=
 14 -----------------------------7d87d1cc0a88--
      由上面的内容我们可以发现(1)分隔符为“-----------------------------7d87d1cc0a88”,其中的数字是随机的,并不固定,但是在同一个请求中都是相同的;(2)02、06、07、11等类似信息被称谓实体头,我们可以发现一个请求流中包含了所有控件(包括隐藏域);(3)实体内容与实体头之前用一个空行分开,也就是说有两个换行符,如02和04、07和09等;(4)最后一个内容的分隔符较一般的分隔符多出"--"。另外也可以发现,如果是文件上传,则实体头内会有"filename=文件路径"的信息。

      我们要做的便是将上述请求的09行的内容截取出来,而其它不变。但是要完整的截取文件数据并没有这么简单,而且还不止一个文件。因此我们要考虑很多因素,之前看过其它人的代码,感觉比较简短,但是似乎没有完全考虑到某些因素。
      我认为考虑因素应该包括如下几点[可能考虑的并不全面,或者考虑的太多,希望大家多提意见]:
      a、因为我们以"filename="字符串为标识查找文件开始的位置,所以要判断该字符串是否为于两个数据流中
      b、"filename="之后开始到这之后的第一个换行符之前是文件名,所以为了正确获取文件名,要判断这之后的换行符是否与"filename="在同一个数据流内
      c、这之后是表示文件类型,在其后又有两个换行符,然后才是真正的文件数据首位置。为获取正确的开始位置,要判断两个换行符是否与"filename="在一个数据流内
      d、当开始读取数据,我们要知道什么时候文件结束,所以要查找分隔符,所以要确保分隔符在一个数据流内
      e、我们还要知道什么时候已经结束整个请求,所以要查找结束分隔符,因此要判断结束分隔符是否在一个数据流内
      至于如何确保标识符在一个数据流内,参考了一些前辈的做法。一般就是利用临时数组对可能分隔于两个数据流内的标识符进行保存。然后拼接在下一次数据流之前与下一次数据流一起做为整体进行处理。

      为了便于理解,我自己搞了个图,大家看看:

 

       通过上面这图,大家应该对整体的流程有了一定的认识,我们在编码中只需要按照上面的流程编码就基本上可以保证上传文件数据的完整性。
       我就不讲解全部代码了,到时候全系列写完之后,提供大家下载。要讲的主要有如下几点:
       1)如何获取分隔符
       通过reflector反编译System.Web.HttpRequest可以找到GetMultipartBoundary()方法,这便是用来查找分隔符用的。代码如下:
       
GetMultipartBoundary()
private byte[] GetMultipartBoundary()
{
    string attributeFromHeader = GetAttributeFromHeader(this.ContentType, "boundary");
    if (attributeFromHeader == null)
    {
        return null;
    }
    attributeFromHeader = "--" + attributeFromHeader;
    return Encoding.ASCII.GetBytes(attributeFromHeader.ToCharArray());
}

      2)如何知道文件数据开始
      由HTTP请求内容中可以看到,文件开始处数据与实体头之前差距两个换行符,所以我们首先找到“filename=”,然后找到“Content-Type: ”,最后找到两个换行符的末尾,这便是文件开始处位置
 
      3)如何将数据写入文件
      首先根据文件名,我们可以在文件数据开始前创建一个文件。然后通过FileStream这个I/O流的Write方法将字节数组写入之前创建的文件。 [注意使用完FileStream必须将它关闭,否则所写入的文件一直处于被占用的情况,那其它程序将无法使用]


FileStream fs = File.Create(HttpContext.Current.Server.MapPath("TempUpload/" +  strFileName));
//以上是创建一个文件
fs.Write(FileByte, FileStartPos, FileReadedLength);
//以上是将读取到的部分文件数据写入该文件


      4)如何保留除去文件数据的请求
      这个其实和我们读取文件数据正好相反,因为文件数据的读取实际上就是将文件数据从起始处开始保存一直到文件数据结束。而除去文件数据的请求则是从请求的第一个数据开始保存,当文件数据开始后,则不保存,直到文件数据结束后,又继续保存。因为除去文件数据后的请求比较小,所以我们可以直接用一个字节数组进行保存。


将除去文件数据的其余HTTP请求写入缓存
    /**////


    /// 将除去文件数据的其余HTTP请求写入缓存
    ///

    /// 要被写入缓存的数组
    /// 被写入数组的起始位置
    /// 被写入数组的长度
    /// HTTP缓存
    private void WriteHttpRequestWithoutFileData(byte[] bWriteByte,int iStartIndex, int iLength,ref byte[] bHttpRequestByte)
    {
        if (bHttpRequestByte == null)
        {
            bHttpRequestByte = new byte[iLength];
            Array.Copy(bWriteByte, iStartIndex, bHttpRequestByte, 0, iLength);//将文件数据之前的所有内容放入缓存用于稍后封装HTTP请求
        }
        else
        {
            byte[] newbyte = new byte[iLength];
            Array.Copy(bWriteByte, iStartIndex, newbyte, 0, iLength);
            bHttpRequestByte = UnionByte(newbyte, bHttpRequestByte);
        }
    }

    private byte[] UnionByte(byte[] bnewbyte, byte[] boldbyte)
    {
        int iLen = 0;
        
        if (bnewbyte != null)
        {//要添加到原数组中的字节如果不为空
            if (boldbyte != null)
            {
                iLen = boldbyte.Length;
            }
            byte[] unionbyte = new byte[bnewbyte.Length + iLen];
            if (boldbyte != null)
            {
                boldbyte.CopyTo(unionbyte, 0);
            }
            bnewbyte.CopyTo(unionbyte, iLen);
            return unionbyte;
        }
        else
        {
            return boldbyte;
        }
    }

      3.6 重新封装HTTP请求
      这部分要利用反射实现,由于本人对反射这块还不是很熟悉,所以代码借鉴高山来客在Asp.NET大文件上传组件开发总结(四)---封送数据给Asp.NET页面中的代码:
      InjectTextParts
    private void InjectTextParts(HttpRequest request, byte[] textParts)
    {
        BindingFlags flags1 = BindingFlags.NonPublic | BindingFlags.Instance;
        Type type1 = request.GetType();
        FieldInfo info1 = type1.GetField("_rawContent", flags1);
        FieldInfo info2 = type1.GetField("_contentLength", flags1);

        if ((info1 != null) && (info2 != null))
        {
            Assembly web = Assembly.GetAssembly(typeof(HttpRequest));
            Type hraw = web.GetType("System.Web.HttpRawUploadedContent");
            object[] argList = new object[2];
            argList[0] = textParts.Length + 1024;
            argList[1] = textParts.Length;

            CultureInfo currCulture = CultureInfo.CurrentCulture;
            object httpRawUploadedContent = Activator.CreateInstance(hraw,
                                                                     BindingFlags.NonPublic | BindingFlags.Instance,
                                                                     null,
                                                                     argList,
                                                                     currCulture,
                                                                     null);

            Type contentType = httpRawUploadedContent.GetType();
            contentType.GetField("_completed", flags1).SetValue(httpRawUploadedContent, true);//不加上这句就会有问题

              FieldInfo dataField = contentType.GetField("_data", flags1);
            dataField.SetValue(httpRawUploadedContent, textParts);
         
            FieldInfo lengthField = contentType.GetField("_length", flags1);
            lengthField.SetValue(httpRawUploadedContent, textParts.Length);
            FieldInfo fileThresholdField = contentType.GetField("_fileThreshold", flags1);
            fileThresholdField.SetValue(httpRawUploadedContent, textParts.Length + 1024);
            info1.SetValue(request, httpRawUploadedContent);
            info2.SetValue(request, textParts.Length);

        }
    }
      4 在aspx页面中加入一个,并修改Form的enctype = " multipart/form-data " , 测试一下吧

      好了,到这差不多就基本完成了,但是这样只是实现了文件的上传,并没有发挥HTTP模块真正的魅力。接下来一篇将向大家介绍如何实现进度条的显示。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/12639172/viewspace-605017/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2008-01-04

  • 博文量
    2376
  • 访问量
    5297011