使用HTTPWebrequest上传文件(multipart/form-data)

321

有没有任何一个类、库或一些代码可以帮助我使用HTTPWebrequest上传文件?

编辑 2:

我不想上传到WebDAV文件夹或类似的地方。我想模拟一个浏览器,就像你将你的头像上传到论坛或通过Web应用程序中的表单上传文件一样。上传到使用multipart/form-data的表单。

编辑:

WebClient无法满足我的要求,因此我正在寻找一个使用HTTPWebrequest的解决方案。


12
这里有一篇关于此事的绝佳博客文章 - http://www.paraesthesia.com/archive/2009/12/16/posting-multipartform-data-using-.net-webrequest.aspx - hwiechers
1
@hwiechers:这对我真的很有用,不像其他答案。我只需要添加我的cookies。 - Tim Schmelter
1
如果您使用的是.NET >= 4.0,则可以查看此答案以获取不需要自定义对象的解决方案。 - Joshcodes
22个回答

263

参考了上述代码并进行了修改,因为它会抛出500内部服务器错误。其中存在\r\n位置错误和空格等问题。使用内存流进行重构,直接向请求流中写入。以下是结果:

    public static void HttpUploadFile(string url, string file, string paramName, string contentType, NameValueCollection nvc) {
        log.Debug(string.Format("Uploading {0} to {1}", file, url));
        string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
        byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

        HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(url);
        wr.ContentType = "multipart/form-data; boundary=" + boundary;
        wr.Method = "POST";
        wr.KeepAlive = true;
        wr.Credentials = System.Net.CredentialCache.DefaultCredentials;

        Stream rs = wr.GetRequestStream();

        string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
        foreach (string key in nvc.Keys)
        {
            rs.Write(boundarybytes, 0, boundarybytes.Length);
            string formitem = string.Format(formdataTemplate, key, nvc[key]);
            byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
            rs.Write(formitembytes, 0, formitembytes.Length);
        }
        rs.Write(boundarybytes, 0, boundarybytes.Length);

        string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
        string header = string.Format(headerTemplate, paramName, file, contentType);
        byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
        rs.Write(headerbytes, 0, headerbytes.Length);

        FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read);
        byte[] buffer = new byte[4096];
        int bytesRead = 0;
        while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) {
            rs.Write(buffer, 0, bytesRead);
        }
        fileStream.Close();

        byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
        rs.Write(trailer, 0, trailer.Length);
        rs.Close();

        WebResponse wresp = null;
        try {
            wresp = wr.GetResponse();
            Stream stream2 = wresp.GetResponseStream();
            StreamReader reader2 = new StreamReader(stream2);
            log.Debug(string.Format("File uploaded, server response is: {0}", reader2.ReadToEnd()));
        } catch(Exception ex) {
            log.Error("Error uploading file", ex);
            if(wresp != null) {
                wresp.Close();
                wresp = null;
            }
        } finally {
            wr = null;
        }
    }

并且示例用法:

    NameValueCollection nvc = new NameValueCollection();
    nvc.Add("id", "TTR");
    nvc.Add("btn-submit-photo", "Upload");
    HttpUploadFile("http://your.server.com/upload", 
         @"C:\test\test.jpg", "file", "image/jpeg", nvc);

它可以扩展为处理多个文件,或者针对每个文件调用多次。但是根据您的需求进行选择。


6
我尝试了这段代码,但它无法上传JPEG文件,也没有报错?这怎么可能。 - Orhan Cinar
2
我添加了一个wr.CookieContainer来保留之前调用的cookies。 - JoaquinG
8
如果你想要将其扩展以处理多个文件,请注意:只有最后一个边界会获得额外的两个破折号:" \r\n--" + boundary + "--\r\n"。否则其他附加文件将被截断。 - Peter Drier
1
+1. 附注:如果您在使用紧凑框架时使用此功能,则需要将Web请求AllowWriteStreamBuffering设置为true; - Mitch Wheat
1
这个程序能正确地转义吗?特别是 name=\"{0}\" 部分。如果我有一个带引号的字符串,这个程序还能正常工作吗?例如 <input name='string"with"quotes' />不过一般来说,name部分通常很简单,很少会出现引号。所以对于大多数正常情况,不会有任何问题。 - mithuntnt
显示剩余11条评论

156

我正在寻找像这样的东西,找到了:http://bytes.com/groups/net-c/268661-how-upload-file-via-c-code(已修改以确保正确性):

public static string UploadFilesToRemoteUrl(string url, string[] files, NameValueCollection formFields = null)
{
    string boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x");

    HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
    request.ContentType = "multipart/form-data; boundary=" +
                            boundary;
    request.Method = "POST";
    request.KeepAlive = true;

    Stream memStream = new System.IO.MemoryStream();

    var boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" +
                                                            boundary + "\r\n");
    var endBoundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" +
                                                                boundary + "--");


    string formdataTemplate = "\r\n--" + boundary +
                                "\r\nContent-Disposition: form-data; name=\"{0}\";\r\n\r\n{1}";

    if (formFields != null)
    {
        foreach (string key in formFields.Keys)
        {
            string formitem = string.Format(formdataTemplate, key, formFields[key]);
            byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
            memStream.Write(formitembytes, 0, formitembytes.Length);
        }
    }

    string headerTemplate =
        "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\n" +
        "Content-Type: application/octet-stream\r\n\r\n";

    for (int i = 0; i < files.Length; i++)
    {
        memStream.Write(boundarybytes, 0, boundarybytes.Length);
        var header = string.Format(headerTemplate, "uplTheFile", files[i]);
        var headerbytes = System.Text.Encoding.UTF8.GetBytes(header);

        memStream.Write(headerbytes, 0, headerbytes.Length);

        using (var fileStream = new FileStream(files[i], FileMode.Open, FileAccess.Read))
        {
            var buffer = new byte[1024];
            var bytesRead = 0;
            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
            {
                memStream.Write(buffer, 0, bytesRead);
            }
        }
    }

    memStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
    request.ContentLength = memStream.Length;

    using (Stream requestStream = request.GetRequestStream())
    {
        memStream.Position = 0;
        byte[] tempBuffer = new byte[memStream.Length];
        memStream.Read(tempBuffer, 0, tempBuffer.Length);
        memStream.Close();
        requestStream.Write(tempBuffer, 0, tempBuffer.Length);
    }

    using (var response = request.GetResponse())
    {
        Stream stream2 = response.GetResponseStream();
        StreamReader reader2 = new StreamReader(stream2);
        return reader2.ReadToEnd();
    }
}

9
FYI...你可以重构掉中间的MemoryStream,直接写入请求流。关键是确保在完成后关闭请求流,这将自动设置请求体的内容长度! - John Clayton
5
当我删除了一个额外的空格后,那个方法对我有效了。"r\n Content-Type: application/octet-stream" 需要改为 "\r\nContent-Type: application/octet-stream"。 - Karl B
1
我还发现在头部末尾的双重\r\n可能会导致问题。删除其中一个就解决了我的问题。 - Hugo Estrada
2
好的,这段代码对我来说不起作用,但是Christian的代码第一次就完美地运行了 - https://dev59.com/NHRB5IYBdhLWcg3wn4jc#2996904 -- 我是在http://cgi-lib.berkeley.edu/ex/fup.html上进行测试的。 - CVertex
3
我知道这是一个早已回答的问题,但我本周才尝试做这个。使用当前的.NET框架,您只需要3行代码即可完成所有操作...WebClient client = new WebClient(); byte[] responseBinary = client.UploadFile(url, file); string result = Encoding.UTF8.GetString(responseBinary); - soutarm
显示剩余8条评论

118

这是可能的,而无需外部代码、扩展或“低级”HTTP操作(只需要从NuGet获取Microsoft.Net.Http包)。以下是一个示例:

// Perform the equivalent of posting a form with a filename and two files, in HTML:
// <form action="{url}" method="post" enctype="multipart/form-data">
//     <input type="text" name="filename" />
//     <input type="file" name="file1" />
//     <input type="file" name="file2" />
// </form>
private async Task<System.IO.Stream> UploadAsync(string url, string filename, Stream fileStream, byte [] fileBytes)
{
    // Convert each of the three inputs into HttpContent objects

    HttpContent stringContent = new StringContent(filename);
    // examples of converting both Stream and byte [] to HttpContent objects
    // representing input type file
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    // Submit the form using HttpClient and 
    // create form data as Multipart (enctype="multipart/form-data")

    using (var client = new HttpClient())
    using (var formData = new MultipartFormDataContent()) 
    {
        // Add the HttpContent objects to the form data

        // <input type="text" name="filename" />
        formData.Add(stringContent, "filename", "filename");
        // <input type="file" name="file1" />
        formData.Add(fileStreamContent, "file1", "file1");
        // <input type="file" name="file2" />
        formData.Add(bytesContent, "file2", "file2");

        // Invoke the request to the server

        // equivalent to pressing the submit button on
        // a form with attributes (action="{url}" method="post")
        var response = await client.PostAsync(url, formData);

        // ensure the request was a success
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }
        return await response.Content.ReadAsStreamAsync();
    }
}

2
可以使用Microsoft.Net.Http NuGet包与4.0一起使用。请参见:https://dev59.com/qGgu5IYBdhLWcg3wso-j。 - amolbk
2
这最终成为了一种非常简单的方法,可以做一些相当强大的事情,包括为每个表单部分设置自定义标头。 - JasonRShaver
11
@php-jquery-programmer,这是通用的示例代码,因此参数具有通用名称。将“param1”视为“your_well_named_param_here”,请重新考虑您的-1。 - Joshcodes
3
您建议使用param1的替代方案是什么? - Joshcodes
2
给我一个建议,将其更改为“bro”。“文件名”对你有用吗? - Joshcodes
显示剩余13条评论

18

根据上面提供的代码,我添加了对多个文件的支持,并且可以直接上传流而不需要本地文件。

要将文件上传到特定的URL(包括一些POST参数),请执行以下操作:

RequestHelper.PostMultipart(
    "http://www.myserver.com/upload.php", 
    new Dictionary<string, object>() {
        { "testparam", "my value" },
        { "file", new FormFile() { Name = "image.jpg", ContentType = "image/jpeg", FilePath = "c:\\temp\\myniceimage.jpg" } },
        { "other_file", new FormFile() { Name = "image2.jpg", ContentType = "image/jpeg", Stream = imageDataStream } },
    });

为了进一步提高效果,可以从给定的文件本身确定文件名和MIME类型。

public class FormFile 
{
    public string Name { get; set; }

    public string ContentType { get; set; }

    public string FilePath { get; set; }

    public Stream Stream { get; set; }
}

public class RequestHelper
{

    public static string PostMultipart(string url, Dictionary<string, object> parameters) {

        string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
        byte[] boundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        request.ContentType = "multipart/form-data; boundary=" + boundary;
        request.Method = "POST";
        request.KeepAlive = true;
        request.Credentials = System.Net.CredentialCache.DefaultCredentials;

        if(parameters != null && parameters.Count > 0) {

            using(Stream requestStream = request.GetRequestStream()) {

                foreach(KeyValuePair<string, object> pair in parameters) {

                    requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
                    if(pair.Value is FormFile) {
                        FormFile file = pair.Value as FormFile;
                        string header = "Content-Disposition: form-data; name=\"" + pair.Key + "\"; filename=\"" + file.Name + "\"\r\nContent-Type: " + file.ContentType + "\r\n\r\n";
                        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(header);
                        requestStream.Write(bytes, 0, bytes.Length);
                        byte[] buffer = new byte[32768];
                        int bytesRead;
                        if(file.Stream == null) {
                            // upload from file
                            using(FileStream fileStream = File.OpenRead(file.FilePath)) {
                                while((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                                    requestStream.Write(buffer, 0, bytesRead);
                                fileStream.Close();
                            }
                        }
                        else {
                            // upload from given stream
                            while((bytesRead = file.Stream.Read(buffer, 0, buffer.Length)) != 0)
                                requestStream.Write(buffer, 0, bytesRead);
                        }
                    }
                    else {
                        string data = "Content-Disposition: form-data; name=\"" + pair.Key + "\"\r\n\r\n" + pair.Value;
                        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
                        requestStream.Write(bytes, 0, bytes.Length);
                    }
                }

                byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
                requestStream.Write(trailer, 0, trailer.Length);
                requestStream.Close();
            }
        }

        using(WebResponse response = request.GetResponse()) {
            using(Stream responseStream = response.GetResponseStream())
            using(StreamReader reader = new StreamReader(responseStream))
                return reader.ReadToEnd();
        }


    }
}

如果内容类型为multipart/related,会有任何改变吗? - Som Bhattacharyya
非常好的答案。我使用相同的代码成功上传了音频文件。谢谢伙计。 - Naveed Khan

16

我的ASP.NET上传常见问题解答中有一篇文章涵盖了这个话题,包括示例代码:使用HttpWebRequest/WebClient进行RFC 1867 POST请求上传文件。与上面的代码相比,这段代码不会将文件加载到内存中,支持上传多个文件、支持设置表单值、设置凭据和cookie等。

编辑:看起来Axosoft已经将该页面下线了。谢谢你们。

可以通过archive.org继续访问该页面。


谢谢你的链接,Chris。我已经将另一个链接实现到我的库中,并添加了支持(除了内存之外)。同时已经转换为VB.NET :) - dr. evil
谢谢,Chris。这帮了我很多! - flipdoubt
第一流的解决方案!非常感谢。 - Bob Denny
谢谢你提醒我!不幸的是,我无法控制那个网站。我在archive.org上找到了该页面(包括源代码),并相应地更新了链接。 - Chris Hynes

13

类似这样的代码已经接近实现:(未经测试的代码)

byte[] data; // data goes here.

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Credentials = userNetworkCredentials;
request.Method = "PUT";
request.ContentType = "application/octet-stream";
request.ContentLength = data.Length;
Stream stream = request.GetRequestStream();
stream.Write(data,0,data.Length);
stream.Close();
response = (HttpWebResponse)request.GetResponse();
StreamReader reader = new StreamReader(response.GetResponseStream());
temp = reader.ReadToEnd();
reader.Close();

谢谢,但我不需要WebDAV或类似的解决方案,我已经澄清了我的回答。请查看编辑。 - dr. evil

8

VB示例(从另一篇帖子上转换的C#示例):

Private Sub HttpUploadFile( _
    ByVal uri As String, _
    ByVal filePath As String, _
    ByVal fileParameterName As String, _
    ByVal contentType As String, _
    ByVal otherParameters As Specialized.NameValueCollection)

    Dim boundary As String = "---------------------------" & DateTime.Now.Ticks.ToString("x")
    Dim newLine As String = System.Environment.NewLine
    Dim boundaryBytes As Byte() = Text.Encoding.ASCII.GetBytes(newLine & "--" & boundary & newLine)
    Dim request As Net.HttpWebRequest = Net.WebRequest.Create(uri)

    request.ContentType = "multipart/form-data; boundary=" & boundary
    request.Method = "POST"
    request.KeepAlive = True
    request.Credentials = Net.CredentialCache.DefaultCredentials

    Using requestStream As IO.Stream = request.GetRequestStream()

        Dim formDataTemplate As String = "Content-Disposition: form-data; name=""{0}""{1}{1}{2}"

        For Each key As String In otherParameters.Keys

            requestStream.Write(boundaryBytes, 0, boundaryBytes.Length)
            Dim formItem As String = String.Format(formDataTemplate, key, newLine, otherParameters(key))
            Dim formItemBytes As Byte() = Text.Encoding.UTF8.GetBytes(formItem)
            requestStream.Write(formItemBytes, 0, formItemBytes.Length)

        Next key

        requestStream.Write(boundaryBytes, 0, boundaryBytes.Length)

        Dim headerTemplate As String = "Content-Disposition: form-data; name=""{0}""; filename=""{1}""{2}Content-Type: {3}{2}{2}"
        Dim header As String = String.Format(headerTemplate, fileParameterName, filePath, newLine, contentType)
        Dim headerBytes As Byte() = Text.Encoding.UTF8.GetBytes(header)
        requestStream.Write(headerBytes, 0, headerBytes.Length)

        Using fileStream As New IO.FileStream(filePath, IO.FileMode.Open, IO.FileAccess.Read)

            Dim buffer(4096) As Byte
            Dim bytesRead As Int32 = fileStream.Read(buffer, 0, buffer.Length)

            Do While (bytesRead > 0)

                requestStream.Write(buffer, 0, bytesRead)
                bytesRead = fileStream.Read(buffer, 0, buffer.Length)

            Loop

        End Using

        Dim trailer As Byte() = Text.Encoding.ASCII.GetBytes(newLine & "--" + boundary + "--" & newLine)
        requestStream.Write(trailer, 0, trailer.Length)

    End Using

    Dim response As Net.WebResponse = Nothing

    Try

        response = request.GetResponse()

        Using responseStream As IO.Stream = response.GetResponseStream()

            Using responseReader As New IO.StreamReader(responseStream)

                Dim responseText = responseReader.ReadToEnd()
                Diagnostics.Debug.Write(responseText)

            End Using

        End Using

    Catch exception As Net.WebException

        response = exception.Response

        If (response IsNot Nothing) Then

            Using reader As New IO.StreamReader(response.GetResponseStream())

                Dim responseText = reader.ReadToEnd()
                Diagnostics.Debug.Write(responseText)

            End Using

            response.Close()

        End If

    Finally

        request = Nothing

    End Try

End Sub

使用这段代码在向Net Core控制器发帖时,总是得到一个空文件。 public async Task<IActionResult> Savefile(IFormFile file) - mrapi

7

2
它应该使用HTTPWebrequest, 我知道WebClient,但对于这个项目来说不好用。 - dr. evil

6
取上述内容并进行修改,接受一些标题值和多个文件。
    NameValueCollection headers = new NameValueCollection();
        headers.Add("Cookie", "name=value;");
        headers.Add("Referer", "http://google.com");
    NameValueCollection nvc = new NameValueCollection();
        nvc.Add("name", "value");

    HttpUploadFile(url, new string[] { "c:\\file1.txt", "c:\\file2.jpg" }, new string[] { "file", "image" }, new string[] { "application/octet-stream", "image/jpeg" }, nvc, headers);

public static void HttpUploadFile(string url, string[] file, string[] paramName, string[] contentType, NameValueCollection nvc, NameValueCollection headerItems)
{
    //log.Debug(string.Format("Uploading {0} to {1}", file, url));
    string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
    byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

    HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(url);

    foreach (string key in headerItems.Keys)
    {
        if (key == "Referer")
        {
            wr.Referer = headerItems[key];
        }
        else
        {
            wr.Headers.Add(key, headerItems[key]);
        }
    }

    wr.ContentType = "multipart/form-data; boundary=" + boundary;
    wr.Method = "POST";
    wr.KeepAlive = true;
    wr.Credentials = System.Net.CredentialCache.DefaultCredentials;

    Stream rs = wr.GetRequestStream();

    string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
    foreach (string key in nvc.Keys)
    {
        rs.Write(boundarybytes, 0, boundarybytes.Length);
        string formitem = string.Format(formdataTemplate, key, nvc[key]);
        byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
        rs.Write(formitembytes, 0, formitembytes.Length);
    }
    rs.Write(boundarybytes, 0, boundarybytes.Length);

    string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
    string header = "";

    for(int i =0; i<file.Count();i++)
    {
        header = string.Format(headerTemplate, paramName[i], System.IO.Path.GetFileName(file[i]), contentType[i]);
        byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
        rs.Write(headerbytes, 0, headerbytes.Length);

        FileStream fileStream = new FileStream(file[i], FileMode.Open, FileAccess.Read);
        byte[] buffer = new byte[4096];
        int bytesRead = 0;
        while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
        {
            rs.Write(buffer, 0, bytesRead);
        }
        fileStream.Close();
        rs.Write(boundarybytes, 0, boundarybytes.Length);
    }
    rs.Close();

    WebResponse wresp = null;
    try
    {
        wresp = wr.GetResponse();
        Stream stream2 = wresp.GetResponseStream();
        StreamReader reader2 = new StreamReader(stream2);
        //log.Debug(string.Format("File uploaded, server response is: {0}", reader2.ReadToEnd()));
    }
    catch (Exception ex)
    {
        //log.Error("Error uploading file", ex);
            wresp.Close();
            wresp = null;
    }
    finally
    {
        wr = null;
    }
}

直到我修改了最后一个边界条目,这才对我起作用。确保最后一个文件之后的边界以两个破折号结尾\r\n--" + boundary + "--\r\n - Ben Ripley

4

我最近也遇到了这个问题 - 另一种处理方法是利用WebClient是可继承的事实,并从那里更改基础的WebRequest:

http://msdn.microsoft.com/en-us/library/system.net.webclient.getwebrequest(VS.80).aspx

我更喜欢C#,但如果你被迫使用VB,结果可能会像这样:

Public Class BigWebClient
    Inherits WebClient
    Protected Overrides Function GetWebRequest(ByVal address As System.Uri) As System.Net.WebRequest
        Dim x As WebRequest = MyBase.GetWebRequest(address)
        x.Timeout = 60 * 60 * 1000
        Return x
    End Function
End Class

'Use BigWebClient here instead of WebClient

+1 仍然,Web客户端过于不可定制化,因此实现它会很尴尬,但这是一个非常有趣的方法,我不知道它是可能的。 - dr. evil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接