在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")
{...}
}
}