Httplistener 和文件上传

Posted

技术标签:

【中文标题】Httplistener 和文件上传【英文标题】:Httplistener and file upload 【发布时间】:2012-01-17 23:56:33 【问题描述】:

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

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(或提供轻量级的替代方案)吗?

【问题讨论】:

我认为您在网络表单中使用了 enctype="multipart/form-data"?如果是这样,您阅读内容的方式似乎过于简单了。 【参考方案1】:

我认为你通过HttpListener 而不是使用 ASP.Net 的内置设施来做这件事让自己变得比必要的更难。但如果你必须这样做,这里有一些示例代码。注意:1)我假设您在 <form> 上使用 enctype="multipart/form-data"。 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 标头指定了正文的总长度,包括边界、子标头和间距。

【讨论】:

您好,感谢您的详细回复!它确实有效,对我有很大帮助! (即使这不是最有效的方式) 这就是我要找的。谢谢 解决了我的问题。但是如果流没有边界,则必须进行一些修改。非常感谢。 @PaulWheeler 使用上面的代码,我传输的目标文件总是比原始文件多 2 个字节。这对于 jpg 文件无关紧要,它们仍然显示正常,但 zip 文件说 CRC 不正确,并且在解压缩时,某些文件总是损坏。对于所有文件,无论大小如何,它总是额外增加 2 个字节。知道他们来自哪里吗? (在 Win8.1 上)。 关于我之前的评论,请注意我编辑了您的答案/代码,根据我在***.com/questions/37744879/…中的发现【参考方案2】:

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

您需要将文件作为字节数组读取,并且使用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);

【讨论】:

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

可能有错误,请彻底测试。这个获取所有的帖子、获取和文件。

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();
        


    

【讨论】:

您保存的是字符串类型的文件数据。小心,你正在处理二进制文件,你必须考虑大文件。【参考方案4】:

我喜欢@paul-wheeler 的回答。但是我需要修改他们的代码以包含一些额外的数据(在本例中是目录结构)。

我正在使用此代码上传文件:

var myDropzone = $("#fileDropZone");
myDropzone.dropzone(
    
        url: "http://" + self.location.hostname + "/Path/Files.html,
        method: "post",
        createImageThumbnails: true,
        previewTemplate: document.querySelector('#previewTemplateId').innerHTML,
        clickable: false,
        init: function () 
            this.on('sending', function(file, xhr, formData)
                // xhr is XMLHttpRequest
                var name = file.fullPath;
                if (typeof (file.fullPath) === "undefined") 
                    name = file.name;
                

                formData.append('fileNameWithPath', name);
            );
        
    );

这是@paul-wheeler 修改后的代码。谢谢@paul-wheeler。

public class FileManager

    public static void SaveFile(HttpListenerRequest request, string savePath)
    
        var tempFileName = Path.Combine(savePath, $"DateTime.Now.Ticks.tmp");
        if (!Directory.Exists(savePath))
        
            Directory.CreateDirectory(savePath);
        

        var (res, fileName) = SaveTmpFile(request, tempFileName);
        if (res)
        
            var filePath = Path.Combine(savePath, fileName);
            var fileDir = filePath.Substring(0, filePath.LastIndexOf(Path.DirectorySeparatorChar));
            if (!Directory.Exists(fileDir))
            
                Directory.CreateDirectory(fileDir);
            

            if (File.Exists(filePath))
            
                File.Delete(filePath);
            

            File.Move(tempFileName, filePath);
        
    

    private static (bool, string) SaveTmpFile(HttpListenerRequest request, string tempFileName)
    
        var enc = request.ContentEncoding;
        var boundary = GetBoundary(request.ContentType);
        var input = request.InputStream;

        byte[] boundaryBytes = enc.GetBytes(boundary);
        var boundaryLen = boundaryBytes.Length;

        using (FileStream output = new FileStream(tempFileName, FileMode.Create, FileAccess.Write))
        
            var buffer = new byte[1024];
            var len = input.Read(buffer, 0, 1024);
            var startPos = -1;

            // Get file name and relative path
            var strBuffer = Encoding.Default.GetString(buffer);
            var strStart = strBuffer.IndexOf("fileNameWithPath") + 21;
            if (strStart < 21)
            
                Logger.LogError("File name not found");
                return (false, null);
            

            var strEnd = strBuffer.IndexOf(boundary, strStart) - 2;
            var fileName = strBuffer.Substring(strStart, strEnd - strStart);
            fileName = fileName.Replace('/', Path.DirectorySeparatorChar);

            // Find start boundary
            while (true)
            
                if (len == 0)
                
                    Logger.LogError("Find start boundary not found");
                    return (false, null);
                

                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);
                
            

            // Advance to data
            var foundData = false;
            while (!foundData)
            
                while (true)
                
                    if (len == 0)
                    
                        Logger.LogError("Preamble not Found");
                        return (false, null);
                    

                    startPos = Array.IndexOf(buffer, enc.GetBytes("\n")[0], startPos);
                    if (startPos >= 0)
                    
                        startPos++;
                        break;
                    
                    else
                    
                        // In case read in line is longer than buffer
                        len = input.Read(buffer, 0, 1024);
                    
                

                var currStr = Encoding.Default.GetString(buffer).Substring(startPos);
                if (currStr.StartsWith("Content-Type:"))
                
                    // Go past the last carriage-return\line-break. (\r\n)
                    startPos = Array.IndexOf(buffer, enc.GetBytes("\n")[0], startPos) + 3;
                    break;
                
            

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

            while (true)
            
                var endPos = IndexOf(buffer, len, boundaryBytes);
                if (endPos >= 0)
                
                    if (endPos > 0) output.Write(buffer, 0, endPos - 2);
                    break;
                
                else if (len <= boundaryLen)
                
                    Logger.LogError("End Boundaray Not Found");
                    return (false, null);
                
                else
                
                    output.Write(buffer, 0, len - boundaryLen);
                    Array.Copy(buffer, len - boundaryLen, buffer, 0, boundaryLen);
                    len = input.Read(buffer, boundaryLen, 1024 - boundaryLen) + boundaryLen;
                
            

            return (true, fileName);
        
    

    private static int IndexOf(byte[] buffer, int len, byte[] boundaryBytes)
    
        for (int i = 0; i <= len - boundaryBytes.Length; i++)
        
            var match = true;
            for (var j = 0; j < boundaryBytes.Length && match; j++)
            
                match = buffer[i + j] == boundaryBytes[j];
            

            if (match)
            
                return i;
            
        

        return -1;
    

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

【讨论】:

【参考方案5】:

您不能使用StreamReader,因为它旨在读取字节为UTF8 的流。相反,您想将流的内容读取到接收缓冲区,删除所有不需要的内容,获取上传文件的文件扩展名,提取上传文件的内容,然后将文件内容保存到新文件。我在这篇文章中展示的代码假定您的表单如下所示:

<form enctype="multipart/form-data" method="POST" action="/uploader">
    <input type="file" name="file">
    <input type="submit">
</form>

如您所见,代码仅用于处理只有文件的表单。由于无法从 application/x-www-form-urlencoded 表单中提取服务器上文件的内容,因此您必须包含“multipart/form-data”。

首先,对于这种处理上传文件的方法,您首先需要这段代码:

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

其次,您需要将request.InputStream 的内容读取到接收缓冲区或byte[]。我们通过使用浏览器发送的Content-Length 标头的长度创建一个byte[] 缓冲区来做到这一点。然后,我们将request.InputStream 的内容读入缓冲区。代码如下所示:

int len = int.Parse(request.Headers["Content-Length"]);
byte[] buffer = new byte[len];
request.InputStream.Read(buffer, 0, len);

流看起来有点像这样:

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

file contents here
------WebKitFormBoundary9lcB0OZVXSqZLbmv--

接下来,您需要获取上传文件的文件扩展名。我们可以使用以下代码来做到这一点:

string fileExtension = Encoding.UTF8.GetString(bytes).Split("\r\n")[1].Split("filename=\"")[1].Replace("\"", "").Split('.')[^1];

然后,我们需要获取文件的内容。我们通过删除开头的内容(-----WebKitFormBoundary、Content-Disposition、Content-Type 和空行)来做到这一点,然后删除请求正文的最后一行,加上一个额外的 @987654332 @ 在末尾。这是执行此操作的代码:

// note that the variable buffer is the byte[], and the variable bytes is the List<byte>
string stringBuffer = Encoding.UTF8.GetString(buffer);
List<byte> bytes = new List<byte>(buffer);
string[] splitString = stringBuffer.Split('\n');
int lengthOfFourLines = splitString[0].Length + splitString[1].Length + 
splitString[2].Length + splitString[3].Length + 4;
bytes.RemoveRange(0, lengthOfFourLines);
int lengthOfLastLine = splitString[^2].Length+2;
bytes.RemoveRange(bytes.Count - lengthOfLastLine, lengthOfLastLine);
buffer = bytes.ToArray();

最后,我们需要将内容保存到文件中。下面的代码生成一个带有用户指定文件扩展名的随机文件名,并将文件内容保存到其中。

string fname = "";
string[] chars = "q w e r t y u i o p a s d f g h j k l z x c v b n m Q W E R T Y U I O P A S D F G H J K L Z X C V B N M 1 2 3 4 5 6 7 8 9 0".Split(" ");
for (int i = 0; i < 10; i++)

    fname += chars[new Random().Next(chars.Length)];

fname += fileExtension;
FileStream file = File.Create(fname);
file.Write(buffer);
file.Close();

下面是全部代码:

public static void Main()

    var listener = new HttpListener();
    listener.Prefixes.Add("http://localhost:8080/");
    listener.Start();
    while(true)
    
        HttpListenerContext context = listener.GetContext();
        HttpListenerRequest request = context.Request;
        HttpListenerResponse response = context.Response;
        if(request.HttpMethod=="POST") SaveFile(request);
        response.OutputStream.Write(Encoding.UTF8.GetBytes("file successfully uploaded"));
        response.OutputStream.Close();
    

void SaveFile(HttpListenerRequest request)

    int len = (int)request.ContentLength64;
    Console.WriteLine(len);
    byte[] buffer = new byte[len];
    request.InputStream.Read(buffer, 0, len);
    string stringBuffer = Encoding.UTF8.GetString(buffer);
    Console.WriteLine(stringBuffer.Replace("\r\n","\\r\\n\n"));
    string fileExtension = stringBuffer.Split("\r\n")[1]
        .Split("filename=\"")[1]
        .Replace("\"", "")
        .Split('.')[^1]
    ;
    List<byte> bytes = new List<byte>(buffer);
    string[] splitString = stringBuffer.Split('\n');
    int lengthOfFourLines = splitString[0].Length + splitString[1].Length + splitString[2].Length + splitString[3].Length + 4;
    bytes.RemoveRange(0, lengthOfFourLines);
    int lengthOfLastLine = splitString[^2].Length+2;
    bytes.RemoveRange(bytes.Count - lengthOfLastLine, lengthOfLastLine);
    buffer = bytes.ToArray();
    
    string fname = "";
    string[] chars = "q w e r t y u i o p a s d f g h j k l z x c v b n m Q W E R T Y U I O P A S D F G H J K L Z X C V B N M 1 2 3 4 5 6 7 8 9 0".Split(" ");
    for (int i = 0; i < 10; i++)
    
        fname += chars[new Random().Next(chars.Length)];
    
    fname += "." + fileExtension;
    FileStream file = File.Create(fname);
    file.Write(buffer);
    file.Close();

另外,如果您想将上传的文件发送到客户端,这里有一个有用的函数可以将文件发送到客户端。

// Make sure you are using System.IO, and System.Net when making this function.
// Also make sure you set the content type of the response before calling this function.
// fileName is the name of the file you want to send to the client, and output is the response.OutputStream.
public static void SendFile(string fileName, Stream output)

    FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    fs.CopyTo(output);
    fs.Close();
    output.Close();

【讨论】:

【参考方案6】:
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;

    
var contentType = MediaTypeHeaderValue.Parse(context.Request.ContentType);
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
var multipartReader = new MultipartReader(boundary, context.Request.InputStream);
var section = (await multipartReader.ReadNextSectionAsync()).AsFileSection();

var fileName = section.FileName;
var fileStream = section.FileStream;

【讨论】:

您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于Httplistener 和文件上传的主要内容,如果未能解决你的问题,请参考以下文章

替代 HttpListener?

springboot文件上传: 单个文件上传 和 多个文件上传

文件上传表单 上传文件的细节 文件上传下载和数据库结合

文件上传漏洞原理是啥?

http 异步 接收 回传 数据文字和文件流

文件的上传(表单上传和ajax文件异步上传)