从HTTP的multipart/form-data分析看C#后台 HttpWebRequest文件上传

在web请求中ContentType一直没有过多关注,也知道ContentType设置为application/x-www-from-urlencoded或multipart/form-data 后和接口层后台获取参数值有重要关系。但都没有稍深入研究,这次研究了一番,记录于此!

首先为何常见的web前端文件上传使用的是multipart/form-data 而非x-www-from-urlencoded.

使用webuploader等上传插件或postman等工具发起一个文件上传的请求,再使用Fiddler或Chrome Network等工具查看网络请求。看下面截图

文件上传时Content-Type为 multipart/form-data,

对照C#后台代码的写法类似下面这样

          // 边界符
          var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
          webRequest.ContentType = "multipart/form-data; boundary=" + boundary;


再看请求体的内容

上面的参数内容格式非常有规律:分析大概特性是:

1:一行“------WebKitFormBoundary{$xxx}”;

 所以会看到C#的代码写法如下:(此处为何是ASCII 呢,因为http协议以ASCII码传输?)

                    // 开始边界符
                    var beginBoundary = Encoding.ASCII.GetBytes("--" + boundary + "\r\n");
                    using (var stream = new MemoryStream())
                    {
                        // 写入开始边界符
                        stream.Write(beginBoundary, 0, beginBoundary.Length);
                    }


2: 一行固定内容:Content-Disposition:form-data; name="xxx", 然后空一行,再写入内容值;

以上传文件为例后台C#代码对应如下

    // 组装文件头数据体 到内存流中
                string fileHeaderTemplate = string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: application/octet-stream\r\n\r\n", parameter.FileNameKey, parameter.FileNameValue);
                byte[] fileHeaderBytes = parameter.Encoding.GetBytes(fileHeaderTemplate);
                memoryStream.Write(fileHeaderBytes, 0, fileHeaderBytes.Length);

其实上面的格式来源Http协议规范,此处转载他人blog 内容:

Rfc1867中可查

1.      上传文件请求头:

Content-type: multipart/form-data; boundary=---+boundary(注1)

--------------------+boundary(注2)

Content-Disposition: form-data; name=file;filename=test.txt;

Content-Type: text/plain;

------------------+boundary--(注3)

a)   注1一般用系统时间(一串数字)来做为boundary值

b)   注1和注2前面的------不可省,它是做为分隔符存在的

c)    注2必须单独一行

d)   两个content-type,第一个告诉客户端我要用表单上传文件,第二个表示上传的文件类型

如果不知道文件类型的话,可以设为application/octet-stream,以二进制流的形式上传下载

e)   --{boundary}   http协议的Form的分隔符,表示结束的话在其后面加—,如注3

另外在每一段信息描述后要跟一个\r\n再跟文件数据,文件数据后面也要跟一个\r\n

f)    分隔符的标志:---------7d是 IE特有的标志,Mozila为-------------71.

g)   多文件上传必须用multipart/mixed

https://blog.csdn.net/sinat_38364990/article/details/70867357

参考原创文章:https://blog.csdn.net/five3/article/details/7181521

再看Http Content-Type=multipart/form-data 的好处是 文件的 表单参数可以通过 ---{boundary}很多的区分和识别,而www-from-urlencoded 是key value的组合形式,无法区分文件内容和表单值,故上传文件需要使用mutipart/form-data.

引用如下英文stackoverflow上的回答来辅助了解:

https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data

If you want to send the following data to the web server:

    name = John
    age = 12

using application/x-www-form-urlencoded would be like this:

name=John&age=12

As you can see, the server knows that parameters are separated by an ampersand &. If & is required for a parameter value then it must be encoded.

So how does the server know where a parameter value starts and ends when it receives an HTTP request using multipart/form-data?

Using the boundary, similar to &.

For example:

    --XXX
    Content-Disposition: form-data; name="name"
     
    John
    --XXX
    Content-Disposition: form-data; name="age"
     
    12
    --XXX--

In that case, the boundary value is XXX. You specify it in the Content-Type header so that the server knows how to split the data it receives.

So you need to:

    Use a value that won't appear in the HTTP data sent to the server.

    Be consistent and use the same value everywhere in the request message.

最后看一个C# 后台使用HttpWebRequest上传文件附件的例子:

https://www.cnblogs.com/GodX/p/5604944.html

/// <summary>
/// Http上传文件类 - HttpWebRequest封装
/// </summary>
public class HttpUploadClient
{
    /// <summary>
    /// 上传执行 方法
    /// </summary>
    /// <param name="parameter">上传文件请求参数</param>
    public static string Execute(UploadParameterType parameter)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            // 1.分界线
            string boundary = string.Format("----{0}", DateTime.Now.Ticks.ToString("x")),       // 分界线可以自定义参数
                beginBoundary = string.Format("--{0}\r\n", boundary),
                endBoundary = string.Format("\r\n--{0}--\r\n", boundary);
            byte[] beginBoundaryBytes = parameter.Encoding.GetBytes(beginBoundary),
                endBoundaryBytes = parameter.Encoding.GetBytes(endBoundary);
            // 2.组装开始分界线数据体 到内存流中
            memoryStream.Write(beginBoundaryBytes, 0, beginBoundaryBytes.Length);
            // 3.组装 上传文件附加携带的参数 到内存流中
            if (parameter.PostParameters != null && parameter.PostParameters.Count > 0)
            {
                foreach (KeyValuePair<string, string> keyValuePair in parameter.PostParameters)
                {
                    string parameterHeaderTemplate = string.Format("Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}\r\n{2}", keyValuePair.Key, keyValuePair.Value, beginBoundary);
                    byte[] parameterHeaderBytes = parameter.Encoding.GetBytes(parameterHeaderTemplate);
 
                    memoryStream.Write(parameterHeaderBytes, 0, parameterHeaderBytes.Length);
                }
            }
            // 4.组装文件头数据体 到内存流中
            string fileHeaderTemplate = string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: application/octet-stream\r\n\r\n", parameter.FileNameKey, parameter.FileNameValue);
            byte[] fileHeaderBytes = parameter.Encoding.GetBytes(fileHeaderTemplate);
            memoryStream.Write(fileHeaderBytes, 0, fileHeaderBytes.Length);
            // 5.组装文件流 到内存流中
            byte[] buffer = new byte[1024 * 1024 * 1];
            int size = parameter.UploadStream.Read(buffer, 0, buffer.Length);
            while (size > 0)
            {
                memoryStream.Write(buffer, 0, size);
                size = parameter.UploadStream.Read(buffer, 0, buffer.Length);
            }
            // 6.组装结束分界线数据体 到内存流中
            memoryStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
            // 7.获取二进制数据
            byte[] postBytes = memoryStream.ToArray();
            // 8.HttpWebRequest 组装
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(new Uri(parameter.Url, UriKind.RelativeOrAbsolute));
            webRequest.Method = "POST";
            webRequest.Timeout = 10000;
            webRequest.ContentType = string.Format("multipart/form-data; boundary={0}", boundary);
            webRequest.ContentLength = postBytes.Length;
            if (Regex.IsMatch(parameter.Url, "^https://"))
            {
                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
                ServicePointManager.ServerCertificateValidationCallback = CheckValidationResult;
            }
            // 9.写入上传请求数据
            using (Stream requestStream = webRequest.GetRequestStream())
            {
                requestStream.Write(postBytes, 0, postBytes.Length);
                requestStream.Close();
            }
            // 10.获取响应
            using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse())
            {
                using (StreamReader reader = new StreamReader(webResponse.GetResponseStream(), parameter.Encoding))
                {
                    string body = reader.ReadToEnd();
                    reader.Close();
                    return body;
                }
            }
        }
    }
    static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
    {
        return true;
    }
}

实际项目中用的一处方法,贴于此,供参考

                     // 边界符
                    var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
                    webRequest.Method = "POST";
                    webRequest.Timeout = 60000;
                    webRequest.ContentType = "multipart/form-data; boundary=" + boundary;
                    // 开始边界符
                    var beginBoundary = Encoding.ASCII.GetBytes("--" + boundary + "\r\n");
                    // 结束结束符
                    var endBoundary = Encoding.ASCII.GetBytes("--" + boundary + "--\r\n");
                    var newLineBytes = Encoding.UTF8.GetBytes("\r\n");
                    using (var stream = new MemoryStream())
                    {
                        // 写入开始边界符
                        stream.Write(beginBoundary, 0, beginBoundary.Length);
                        // 写入文件
                        var fileHeader = "Content-Disposition: form-data; name=\"file\"; filename=\"test.pdf\"\r\n" +
                                         "Content-Type: application/octet-stream\r\n\r\n";
                        var fileHeaderBytes = Encoding.UTF8.GetBytes(string.Format(fileHeader, fileName));
                        stream.Write(fileHeaderBytes, 0, fileHeaderBytes.Length);
                        stream.Write(fileData, 0, length);
                        stream.Write(newLineBytes, 0, newLineBytes.Length);
                        // 写入字符串
                        var keyValue = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}\r\n";
                        foreach (string key in parameters.Keys)
                        {
                            var keyValueBytes = Encoding.UTF8.GetBytes(string.Format(keyValue, key, parameters[key]));
                            stream.Write(beginBoundary, 0, beginBoundary.Length);
                            stream.Write(keyValueBytes, 0, keyValueBytes.Length);
                        }
                        // 写入结束边界符
                        stream.Write(endBoundary, 0, endBoundary.Length);
                        webRequest.ContentLength = stream.Length;
                        stream.Position = 0;
                        var tempBuffer = new byte[stream.Length];
                        stream.Read(tempBuffer, 0, tempBuffer.Length);
                        using (Stream requestStream = webRequest.GetRequestStream())
                        {
                            requestStream.Write(tempBuffer, 0, tempBuffer.Length);
                            using (var response = webRequest.GetResponse())
                            using (StreamReader httpStreamReader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                            {
                                string result = httpStreamReader.ReadToEnd();
                                if (!string.IsNullOrWhiteSpace(result) && result.Trim().ToLower() == "success")
                                {...}
     
                              }
                         }
---------------------
原文:https://blog.csdn.net/elie_yang/article/details/80710059
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值