服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

2022-08-04 09:41张传宁 C#

这篇文章主要介绍了C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器,需要的朋友可以参考下

系列目录 【已更新最新开发文章,点击查看详细】

类似于以下场景,将表单中的用户信息(包含附件)上传到服务器并保存到数据库中,

?
1
2
3
4
5
6
7
8
9
10
<form id="form1" runat="server" action="usermanagehandler.ashx" method="post" enctype="multipart/form-data">
 <div>
  名称: <input type="text" name="uname" class="uname" /><br/>
  邮件: <input type="text" name="email" class="email" /><p/>
  附件1: <input type="file" name="file1" class="file" /><p/>
  附件2: <input type="file" name="file2" class="file" /><p/>
  附件3: <input type="file" name="file3" class="file" /><p/>
    <input type="submit" name="submit" value="提交" />
 </div>
</form>

如果是在传统的管理系统或者网站中,上传到发布的iis站点下,使用asp.net的上传控件结合后台的 httpcontext.request.files的相关类与方法很简单的即可实现上述功能。

?
1
2
3
httpfilecollection files = httpcontext.current.request.files;
 httppostedfile postedfile = files["fileupload"];
 postedfile.saveas(postedfile.filename);

随着云端应用的发展与普及,第三方应用平台或者开发平台部署在云服务器上,例如阿里云、腾讯云、七牛云、青云等。第三方对外开放的应用平台大都是提供restful api供开发者调用以上传(本地或者远端文件)或下载业务数据进行业务开发。

multipart/form-data 数据格式介绍

1、使用postman模拟上述功能(不上传附件)

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

点击【code】按钮,打开如下窗体

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

2、只上传一个附件

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

此点击【提交】按钮,form提交请求数据,fiddler抓包时看到的请求如下(无关的请求头在本文中都省略掉了):

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

3、上传多个附件,一个普通文本,一个office word文档,一个jpg图片

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

此点击【提交】按钮,form提交请求数据,fiddler抓包时看到的请求如下(无关的请求头在本文中都省略掉了):

C# http系列之以form-data方式上传多个文件及键值对集合到远程服务器

 

http 请求中的 multipart/form-data,它会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有content-type来表名文件类型;content-disposition,用来说明字段的一些信息;

由于有 boundary 隔离,所以 multipart/form-data 既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。

具体格式描述为:

(1)boundary:用于分割不同的字段,为了避免与正文内容重复。以2个横线“--”开头,最后的字段之后以2个横线“--”结束。

(2)content-type: 指明了数据是以 multipart/form-data 来编码。

(3)消息主体里按照字段个数又分为多个结构类似的部分,

  • 每部分都是以--boundary开始,
  • 紧接着是内容描述信息,
  • 然后是回车(换一行),
  • 最后是字段具体内容(文本或二进制)。
  • 如果传输的是文件,还要包含文件名和文件类型信息。
  • 消息主体最后以--boundary--标示结束。

关于 multipart/form-data 的详细定义,请查看 rfc1867与 rfc2045 。

这种方式一般用来上传文件,各大服务端语言对它也有着良好的支持。

上面提到的这两种 post 数据的方式,都是浏览器原生支持的,而且现阶段标准中原生 <form> 表单也只支持这两种方式(通过 <form> 元素的enctype属性指定,默认为application/x-www-form-urlencoded)。

c# 通用方法实现 multipart/form-data 方式上传附件与请求参数

清楚了 multipart/form-data 的数据请求格式之后,使用c#的 httpwebrequest 与httpwebresponse 类来模拟上述场景,具体代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/// <summary>
/// http请求(包含多分部数据,multipart/form-data)。
/// 将多个文件以及多个参数以多分部数据表单方式上传到指定url的服务器
/// </summary>
/// <param name="url">请求目标url</param>
/// <param name="filefullnames">待上传的文件列表(包含全路径的完全限定名)。如果某个文件不存在,则忽略不上传</param>
/// <param name="kvdatas">请求时表单键值对数据。</param>
/// <param name="method">请求的方法。请使用 webrequestmethods.http 的枚举值</param>
/// <param name="timeout">获取或设置 <see cref="m:system.net.httpwebrequest.getresponse" /> 和
///      <see cref="m:system.net.httpwebrequest.getrequeststream" /> 方法的超时值(以毫秒为单位)。
///      -1 表示永不超时
/// </param>
/// <returns></returns>
public httpresult uploadformbymultipart(string url, string[] filefullnames, namevaluecollection kvdatas = null, string method = webrequestmethods.http.post, int timeout = -1)
{
 #region 说明
 /* 阿里云文档:https://www.alibabacloud.com/help/zh/doc-detail/42976.htm
     (c#示例中仅仅是把文件中的文本内容当做 formdata 中的项,与文件流是不一样的。本方法展示的是文件流,更通用)
  */
 
 /* 说明:multipart/form-data 方式提交文件
  *  (1) header 一定要有 content-type: multipart/form-data; boundary={boundary}。
  *  (2) header 和bod y之间由 \r\n--{boundary} 分割。
  *  (3) 表单域格式 :content-disposition: form-data; name="{key}"\r\n\r\n
  *     {value}\r\n
  *     --{boundary}
  *  (4)表单域名称大小写敏感,如policy、key、file、ossaccesskeyid、ossaccesskeyid、content-disposition。
  *  (5)注意:表单域 file 必须为最后一个表单域。即必须放在最后写。
  */
 #endregion
 
 #region contenttype 说明
 /* 该contenttype的属性包含请求的媒体类型。分配给contenttype属性的值在请求发送content-typehttp标头时替换任何现有内容。
  
  要清除content-typehttp标头,请将contenttype属性设置为null。
  
  * 注意:此属性的值存储在webheadercollection中。如果设置了webheadercollection,则属性值将丢失。
  *  所以放置在headers 属性之后设置
  */
 #endregion
 
 #region method 说明
 /* 如果 contentlength 属性设置为-1以外的任何值,则必须将 method 属性设置为上载数据的协议属性。 */
 #endregion
 
 #region httpwebrequest.cookiecontainer 在 .net3.5 与 .net4.0 中的不同
 /* 请参考:https://www.crifan.com/baidu_emulate_login_for_dotnet_4_0_error_the_fisrt_two_args_should_be_string_type_0_1/ */
 #endregion
 
 httpresult httpresult = new httpresult();
 
 #region 校验
 
 if (filefullnames == null || filefullnames.length == 0)
 {
  httpresult.status = httpresult.status_fail;
 
  httpresult.refcode = (int)httpstatuscode2.user_file_not_exists;
  httpresult.reftext = httpstatuscode2.user_file_not_exists.getcustomattributedescription();
 
  return httpresult;
 }
 
 list<string> lstfiles = new list<string>();
 foreach (string filefullname in filefullnames)
 {
  if (file.exists(filefullname))
  {
   lstfiles.add(filefullname);
  }
 }
 
 if (lstfiles.count == 0)
 {
  httpresult.status = httpresult.status_fail;
 
  httpresult.refcode = (int)httpstatuscode2.user_file_not_exists;
  httpresult.reftext = httpstatuscode2.user_file_not_exists.getcustomattributedescription();
 
  return httpresult;
 }
 
 #endregion
 
 string boundary = createformdataboundary();          // 边界符
 byte[] beginboundarybytes = encoding.utf8.getbytes("--" + boundary + "\r\n");  // 边界符开始。【☆】右侧必须要有 \r\n 。
 byte[] endboundarybytes = encoding.utf8.getbytes("\r\n--" + boundary + "--\r\n"); // 边界符结束。【☆】两侧必须要有 --\r\n 。
 byte[] newlinebytes = encoding.utf8.getbytes("\r\n"); //换一行
 memorystream memorystream = new memorystream();
 
 httpwebrequest httpwebrequest = null;
 try
 {
  httpwebrequest = webrequest.create(url) as httpwebrequest; // 创建请求
  httpwebrequest.contenttype = string.format(httpcontenttype.multipart_form_data + "; boundary={0}", boundary);
  //httpwebrequest.referer = "http://bimface.com/user-console";
  httpwebrequest.method = method;
  httpwebrequest.keepalive = true;
  httpwebrequest.timeout = timeout;
  httpwebrequest.useragent = getuseragent();
 
  #region 步骤1:写入键值对
  if (kvdatas != null)
  {
   string formdatatemplate = "content-disposition: form-data; name=\"{0}\"\r\n\r\n" +
          "{1}\r\n";
 
   foreach (string key in kvdatas.keys)
   {
    string formitem = string.format(formdatatemplate, key.replace(stringutils.symbol.key_suffix, string.empty), kvdatas[key]);
    byte[] formitembytes = encoding.utf8.getbytes(formitem);
 
    memorystream.write(beginboundarybytes, 0, beginboundarybytes.length); // 1.1 写入formdata项的开始边界符
    memorystream.write(formitembytes, 0, formitembytes.length);   // 1.2 将键值对写入formdata项中
   }
  }
  #endregion
 
  #region 步骤2:写入文件(表单域 file 必须为最后一个表单域)
 
  const string filepartheadertemplate = "content-disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\n" +
            "content-type: application/octet-stream\r\n\r\n";
 
  int i = 0;
  foreach (var filefullname in lstfiles)
  {
   fileinfo fileinfo = new fileinfo(filefullname);
   string filename = fileinfo.name;
 
   string fileheaderitem = string.format(filepartheadertemplate, "file", filename);
   byte[] fileheaderitembytes = encoding.utf8.getbytes(fileheaderitem);
 
   if (i > 0)
   {
    // 第一笔及第一笔之后的数据项之间要增加一个换行
    memorystream.write(newlinebytes, 0, newlinebytes.length);
   }
   memorystream.write(beginboundarybytes, 0, beginboundarybytes.length);  // 2.1 写入formdata项的开始边界符
   memorystream.write(fileheaderitembytes, 0, fileheaderitembytes.length); // 2.2 将文件头写入formdata项中
 
   int bytesread;
   byte[] buffer = new byte[1024];
 
   filestream filestream = new filestream(filefullname, filemode.open, fileaccess.read);
   while ((bytesread = filestream.read(buffer, 0, buffer.length)) != 0)
   {
    memorystream.write(buffer, 0, bytesread);        // 2.3 将文件流写入formdata项中
   }
 
   i++;
  }
 
  memorystream.write(endboundarybytes, 0, endboundarybytes.length);    // 2.4 写入formdata的结束边界符
 
  #endregion
 
  #region 步骤3:将表单域(内存流)写入 httpwebrequest 的请求流中,并发起请求
  httpwebrequest.contentlength = memorystream.length;
 
  stream requeststream = httpwebrequest.getrequeststream();
 
  memorystream.position = 0;
  byte[] tempbuffer = new byte[memorystream.length];
  memorystream.read(tempbuffer, 0, tempbuffer.length);
  memorystream.close();
 
  requeststream.write(tempbuffer, 0, tempbuffer.length);  // 将内存流中的字节写入 httpwebrequest 的请求流中
  requeststream.close();
  #endregion
 
  httpwebresponse httpwebresponse = httpwebrequest.getresponse() as httpwebresponse; // 获取响应
  if (httpwebresponse != null)
  {
   //getheaders(ref httpresult, httpwebresponse);
   getresponse(ref httpresult, httpwebresponse);
   httpwebresponse.close();
  }
 }
 catch (webexception webexception)
 {
  getwebexceptionresponse(ref httpresult, webexception);
 }
 catch (exception ex)
 {
  getexceptionresponse(ref httpresult, ex, method, httpcontenttype.multipart_form_data);
 }
 finally
 {
  if (httpwebrequest != null)
  {
   httpwebrequest.abort();
  }
 }
 
 return httpresult;
}

请严格注意代码中注释部分,尤其是以boundary 作为分界线的部分,一点格式都不能错误,否则就无法提交成功。

根据上述方法,可以衍生出几个重载方法:

上传单文件与多个键值对

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// http请求(包含多分部数据,multipart/form-data)。
/// 将文件以及多个参数以多分部数据表单方式上传到指定url的服务器
/// </summary>
/// <param name="url">请求目标url</param>
/// <param name="filefullname">待上传的文件(包含全路径的完全限定名)</param>
/// <param name="kvdatas">请求时表单键值对数据。</param>
/// <param name="method">请求的方法。请使用 webrequestmethods.http 的枚举值</param>
/// <param name="timeout">获取或设置 <see cref="m:system.net.httpwebrequest.getresponse" /> 和
///      <see cref="m:system.net.httpwebrequest.getrequeststream" /> 方法的超时值(以毫秒为单位)。
///      -1 表示永不超时
/// </param>
/// <returns></returns>
public httpresult uploadformbymultipart(string url, string filefullname, namevaluecollection kvdatas = null, string method = webrequestmethods.http.post, int timeout = -1)
{
 string[] filefullnames = { filefullname };
 
 return uploadformbymultipart(url, filefullnames, kvdatas, method, timeout);
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// http请求(包含多分部数据,multipart/form-data)。
/// 将文件以多分部数据表单方式上传到指定url的服务器
/// </summary>
/// <param name="url">请求目标url</param>
/// <param name="filefullname">待上传的文件(包含全路径的完全限定名)</param>
/// <param name="kvdatas">请求时表单键值对数据。</param>
/// <param name="method">请求的方法。请使用 webrequestmethods.http 的枚举值</param>
/// <param name="timeout">获取或设置 <see cref="m:system.net.httpwebrequest.getresponse" /> 和
///      <see cref="m:system.net.httpwebrequest.getrequeststream" /> 方法的超时值(以毫秒为单位)。
///      -1 表示永不超时
/// </param>
/// <returns></returns>
public httpresult uploadformbymultipart(string url, string filefullname, dictionary<string, string> kvdatas = null, string method = webrequestmethods.http.post, int timeout = -1)
{
 var nvc = kvdatas.tonamevaluecollection();
 return uploadformbymultipart(url, filefullname, nvc, method, timeout);
}

系列目录 【已更新最新开发文章,点击查看详细】

总结

以上所述是小编给大家介绍的c# http系列之以form-data方式上传多个文件及键值对集合到远程服务器,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

原文链接:https://www.cnblogs.com/SavionZhang/p/11419778.html

延伸 · 阅读

精彩推荐