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

在web请求中ContentType一直没有过多关注,也知道ContentType设置为application/x-www-from-urlencodedmultipart/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")
                            {...}

                          }
                     }


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值