问题 Httplistener和文件上传


我正在尝试从我的网络服务器检索上传的文件。当客户端通过webform(随机文件)发送文件时,我需要解析请求以获取文件并进一步处理它。 基本上,代码如下:

HttpListenerContext context = listener.GetContext();
HttpListenerRequest request = context.Request;
StreamReader r = new StreamReader(request.InputStream, System.Text.Encoding.Default);
// this is the retrieved file from streamreader
string file = null;

while ((line = r.ReadLine()) != null){
     // i read the stream till i retrieve the filename
     // get the file data out and break the loop 
}
// A byststream is created by converting the string,
Byte[] bytes = request.ContentEncoding.GetBytes(file);
MemoryStream mstream = new MemoryStream(bytes);

// do the rest

因此,我能够检索文本文件,但对于所有其他文件,它们已损坏。 有人能告诉我如何正确解析这些HttplistnerRequests(或提供轻量级替代)?


13187
2017-12-11 19:19


起源

我认为你在网络表单中使用enctype =“multipart / form-data”?如果是这样,你看起来就像是在简化你阅读内容的方式。 - Paul Wheeler


答案:


我认为你做的事情比你自己做的更难 HttpListener 而不是使用ASP.Net的内置设施。但是如果你必须这样做,这里有一些示例代码。注意:1)我假设你正在使用 enctype="multipart/form-data" 在你的 <form>。 2)此代码旨在与仅包含您的表单一起使用 <input type="file" /> 如果您想发布其他字段或多个文件,则必须更改代码。 3)这是概念/示例的证明,它可能有错误,并且不是特别灵活。

static void Main(string[] args)
{
    HttpListener listener = new HttpListener();
    listener.Prefixes.Add("http://localhost:8080/ListenerTest/");
    listener.Start();

    HttpListenerContext context = listener.GetContext();

    SaveFile(context.Request.ContentEncoding, GetBoundary(context.Request.ContentType), context.Request.InputStream);

    context.Response.StatusCode = 200;
    context.Response.ContentType = "text/html";
    using (StreamWriter writer = new StreamWriter(context.Response.OutputStream, Encoding.UTF8))
        writer.WriteLine("File Uploaded");

    context.Response.Close();

    listener.Stop();

}

private static String GetBoundary(String ctype)
{
    return "--" + ctype.Split(';')[1].Split('=')[1];
}

private static void SaveFile(Encoding enc, String boundary, Stream input)
{
    Byte[] boundaryBytes = enc.GetBytes(boundary);
    Int32 boundaryLen = boundaryBytes.Length;

    using (FileStream output = new FileStream("data", FileMode.Create, FileAccess.Write))
    {
        Byte[] buffer = new Byte[1024];
        Int32 len = input.Read(buffer, 0, 1024);
        Int32 startPos = -1;

        // Find start boundary
        while (true)
        {
            if (len == 0)
            {
                throw new Exception("Start Boundaray Not Found");
            }

            startPos = IndexOf(buffer, len, boundaryBytes);
            if (startPos >= 0)
            {
                break;
            }
            else
            {
                Array.Copy(buffer, len - boundaryLen, buffer, 0, boundaryLen);
                len = input.Read(buffer, boundaryLen, 1024 - boundaryLen);
            }
        }

        // Skip four lines (Boundary, Content-Disposition, Content-Type, and a blank)
        for (Int32 i = 0; i < 4; i++)
        {
            while (true)
            {
                if (len == 0)
                {
                    throw new Exception("Preamble not Found.");
                }

                startPos = Array.IndexOf(buffer, enc.GetBytes("\n")[0], startPos);
                if (startPos >= 0)
                {
                    startPos++;
                    break;
                }
                else
                {
                    len = input.Read(buffer, 0, 1024);
                }
            }
        }

        Array.Copy(buffer, startPos, buffer, 0, len - startPos);
        len = len - startPos;

        while (true)
        {
            Int32 endPos = IndexOf(buffer, len, boundaryBytes);
            if (endPos >= 0)
            {
                if (endPos > 0) output.Write(buffer, 0, endPos-2);
                break;
            }
            else if (len <= boundaryLen)
            {
                throw new Exception("End Boundaray Not Found");
            }
            else
            {
                output.Write(buffer, 0, len - boundaryLen);
                Array.Copy(buffer, len - boundaryLen, buffer, 0, boundaryLen);
                len = input.Read(buffer, boundaryLen, 1024 - boundaryLen) + boundaryLen;
            }
        }
    }
}

private static Int32 IndexOf(Byte[] buffer, Int32 len, Byte[] boundaryBytes)
{
    for (Int32 i = 0; i <= len - boundaryBytes.Length; i++)
    {
        Boolean match = true;
        for (Int32 j = 0; j < boundaryBytes.Length && match; j++)
        {
            match = buffer[i + j] == boundaryBytes[j];
        }

        if (match)
        {
            return i;
        }
    }

    return -1;
}

为了帮助您更好地理解上面的代码正在做什么,这里是HTTP POST的主体:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9lcB0OZVXSqZLbmv

------WebKitFormBoundary9lcB0OZVXSqZLbmv
Content-Disposition: form-data; name="my_file"; filename="Test.txt"
Content-Type: text/plain

Test
------WebKitFormBoundary9lcB0OZVXSqZLbmv--

我遗漏了不相关的标题。如您所见,您需要通过扫描来解析主体以查找开始和结束边界序列,并删除文件内容之前的子标题。遗憾的是,由于二进制数据的潜在可能性,您无法使用StreamReader。同样不幸的是每个文件没有Content-Length(请求的Content-Length标头指定了主体的总长度,包括边界,子标题和间距)。


14
2017-12-12 00:00



我在JPEG和文本文件上测试了这个并且都工作了。 - Paul Wheeler
您好,感谢您详细的回复!它确实起作用并帮助了我很多! (即使这不是最有效的方式) - cecemel
多数民众赞成我正在寻找的东西。谢谢 - fyasar
解决了我的问题。但如果流没有边界,则必须修改一下。非常感谢。 - mark vanzuela
@PaulWheeler使用上面的代码,我传输的目标文件总是比原始文件多2个字节。这对jpg文件无关紧要,它们仍然显示正常,但是zip文件说CRC不正确,解压缩时,某些文件总是损坏。对于所有文件,无论大小,它总是2个额外的字节。知道他们来自哪里? (在Win8.1上)。 - Mathias Conradt


问题是您正在以文本形式阅读文件。

您需要将文件读取为bytearray而不是使用 BinaryReader在 比使用更好,更容易使用 StreamReader

Byte[] bytes;
using (System.IO.BinaryReader r = new System.IO.BinaryReader(request.InputStream))
{
    // Read the data from the stream into the byte array
    bytes = r.ReadBytes(Convert.ToInt32(request.InputStream.Length));
}
MemoryStream mstream = new MemoryStream(bytes);

1
2017-12-11 21:58



你好,谢谢你的回复!但是,似乎request.InputStream.Length提供了一个不受支持的异常。此外,bytearray使作业复杂化,因为需要进行一些解析以从其他正文内容中提取文件。从这个角度来看,Streamrider似乎更适合目的。 - cecemel
@ user1092608:哦,那很不幸。问题是你不能真正使用streamreader的文本读取功能来提取二进制文件。您是否使用FileUpload控件或其他方法将文件嵌入流中? - competent_tech
这是一个相关的评论。现在客户端基本上使用默认的html <input type =“file”name =“datafile”..>表单。如果可能的话,我想避免使用fileUpload控件,以保持尽可能简单 - 尽管这仍然意味着 - 。在这个项目中,另一个实例(机器)应该能够在我的服务器上抛出随机文件以及一些元数据来处理它并将结果发回。 - cecemel
@ user1092608:在这种情况下,我会将文件的接收从处理分离为单独的方法。我认为从用户成功接收二进制文件的唯一方法是使用文件输入控件中的实际方法。收到文件后,您可以将其传递给公共文件处理机制。 - competent_tech


可能有虫子,彻底测试。这个获取所有发布,获取和文件。

using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace DUSTLauncher
{
    class HttpNameValueCollection
    {
        public class File
        {
            private string _fileName;
            public string FileName { get { return _fileName ?? (_fileName = ""); } set { _fileName = value; } }

            private string _fileData;
            public string FileData { get { return _fileData ?? (_fileName = ""); } set { _fileData = value; } }

            private string _contentType;
            public string ContentType { get { return _contentType ?? (_contentType = ""); } set { _contentType = value; } }
        }

        private NameValueCollection _get;
        private Dictionary<string, File> _files;
        private readonly HttpListenerContext _ctx;

        public NameValueCollection Get { get { return _get ?? (_get = new NameValueCollection()); } set { _get = value; } }
        public NameValueCollection Post { get { return _ctx.Request.QueryString; } }
        public Dictionary<string, File> Files { get { return _files ?? (_files = new Dictionary<string, File>()); } set { _files = value; } }

        private void PopulatePostMultiPart(string post_string)
        {
            var boundary_index = _ctx.Request.ContentType.IndexOf("boundary=") + 9;
            var boundary = _ctx.Request.ContentType.Substring(boundary_index, _ctx.Request.ContentType.Length - boundary_index);

            var upper_bound = post_string.Length - 4;

            if (post_string.Substring(2, boundary.Length) != boundary)
                throw (new InvalidDataException());

            var raw_post_strings = new List<string>();
            var current_string = new StringBuilder();

            for (var x = 4 + boundary.Length; x < upper_bound; ++x)
            {
                if (post_string.Substring(x, boundary.Length) == boundary)
                {
                    x += boundary.Length + 1;
                    raw_post_strings.Add(current_string.ToString().Remove(current_string.Length - 3, 3));
                    current_string.Clear();
                    continue;
                }

                current_string.Append(post_string[x]);

                var post_variable_string = current_string.ToString();

                var end_of_header = post_variable_string.IndexOf("\r\n\r\n");

                if (end_of_header == -1) throw (new InvalidDataException());

                var filename_index = post_variable_string.IndexOf("filename=\"", 0, end_of_header);
                var filename_starts = filename_index + 10;
                var content_type_starts = post_variable_string.IndexOf("Content-Type: ", 0, end_of_header) + 14;
                var name_starts = post_variable_string.IndexOf("name=\"") + 6;
                var data_starts = end_of_header + 4;

                if (filename_index == -1) continue;

                var filename = post_variable_string.Substring(filename_starts, post_variable_string.IndexOf("\"", filename_starts) - filename_starts);
                var content_type = post_variable_string.Substring(content_type_starts, post_variable_string.IndexOf("\r\n", content_type_starts) - content_type_starts);
                var file_data = post_variable_string.Substring(data_starts, post_variable_string.Length - data_starts);
                var name = post_variable_string.Substring(name_starts, post_variable_string.IndexOf("\"", name_starts) - name_starts);
                Files.Add(name, new File() { FileName = filename, ContentType = content_type, FileData = file_data });
                continue;

            }
        }

        private void PopulatePost()
        {
            if (_ctx.Request.HttpMethod != "POST" || _ctx.Request.ContentType == null) return;

            var post_string = new StreamReader(_ctx.Request.InputStream, _ctx.Request.ContentEncoding).ReadToEnd();

            if (_ctx.Request.ContentType.StartsWith("multipart/form-data"))
                PopulatePostMultiPart(post_string);
            else
                Get = HttpUtility.ParseQueryString(post_string);

        }

        public HttpNameValueCollection(ref HttpListenerContext ctx)
        {
            _ctx = ctx;
            PopulatePost();
        }


    }
}

1



您正在以字符串类型保存文件数据。小心,你正在处理二进制文件,你必须考虑大文件。 - fyasar