UN4L5——HTTP上传数据

POST必备知识

重点知识点是

  1. Get​和Post​的区别
  2. ContentType​的重要类型

注意:
HTTP通讯中客户端发送给服务端的Get和Post请求都需要服务端和客户端约定一些规则进行处理
比如传递的参数的含义,数据如何处理等等,都是需要前后端程序制定对应规则来进行处理的
只是我们目前没有后端开发的HTTP服务器,所以我们传递过去的参数和数据没有得到对应处理
我们目前只针对HTTP资源服务器上传下载数据进行学习,他们的通讯原理是一致的,都是通过HTTP通讯交换数据

Get和Post的区别

我们上节课学习的下载数据,主要使用的就是Get​请求类型,我们在上传数据时将会使用Post​请求类型
这两个请求类型他们的主要区别是:

  1. 主要用途

    • Get​ — 一般从指定的资源请求数据,主要用于获取数据
    • Post​ — 一般向指定的资源提交想要被处理的数据,主要用于上传数据
  2. 相同点:Get​和Post​都可以传递一些额外的参数数据给服务端

  3. 不同点:

    1. 在传递参数时,Post相对Get更加的安全,因为Post看不到参数

      • Get​传递的参数都包含在连接中(URL资源定位地址),是暴露式的:?参数名=参数值&参数名=参数值...
      • Post​传递的参数放在请求数据中,不会出现在URL中,是隐藏式的
    2. Get​在传递数据时有大小的限制,因为它主要是在连接中拼接参数,而URL的长度是有限制的(最大长度一般为2048个字符)
      Post​在传递数据时没有限制

    3. 在浏览器中Get​请求能被缓存,Post​不能缓存

    4. 传输次数可能不同

      • Get​:建立连接——>请求行、请求头、请求数据一次传输——>获取响应——>断开连接
      • Post​:建立连接 ——>传输可能分两次——>请求行,请求头第一次传输——>请求数据第二次传输——>获取响应——>断开

对于前端来说,其实Get​和Post​都是能够获取和传递数据的,后端只要处理对应逻辑返回响应信息即可
但是由于他们的这些特点,我们在实际使用时建议Get用于获取,Post用于上传
如果想要传递一些不想暴露在外部的参数信息,建议使用Post​,它更加的安全

Post如何携带额外参数

关键点:如果要传递键值对形式的消息: ContentType设置为 application/x-www-form-urlencoded键值对类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HttpWebRequest request = HttpWebRequest.Create("http://192.168.1.106/Http_Server") as HttpWebRequest;
request.Method = WebRequestMethods.Http.Post;
request.Timeout = 2000;
//设置上传的内容类型
request.ContentType = "application/x-www-form-unlencoded";
//我们要上传的数据
string str = "Name=MrTang&ID=2"; //参数如何传是可变的,取决于后端程序员的编写逻辑
byte[] bytes = Encoding.UTF8.GetBytes(str);
//在上传之前一定要设置内容的长度
request.ContentLength = bytes.Length;
//上传数据
Stream stream = request.GetRequestStream();
stream.Write(bytes, 0, bytes.Length);
stream.Close();
//发送数据,得到相应结果
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
print(response.StatusCode);

值得一提的是,如果顺利,服务器会收到消息,但是发送过去的参数的具体处理需要服务端的后端程序员编写相应处理逻辑才能起效果

ContentType的常用类型

ContentType​的构成:

模板:httpWebRequest.ContentType = 内容类型;charset=编码格式;boundary=边界字符串

例:httpWebRequest.ContentType = text/html;charset=utf-8;boundary=自定义字符串

一般通用的Http服务器对于不同的内容类型会有通用的处理规则,如何处理数据就是通过这里的httpWebRequest.ContentType​决定
不过,具体如何处理消息还是要和服务端的后端程序员共同决定

边界字符串用于表示数据的开头和结尾,在特定的内容类型下需要使用

其中内容类型有:

  • 文本类型text​:

    • text/plain 没有特定子类型就是它(重要)
    • text/html
    • text/css
    • text/javascript
  • 图片类型image​:

    • image/gif
    • image/png
    • image/jpeg
    • image/bm
    • image/webp
    • image/x-icon
    • image/vnd.microsoft.icon
  • 音频类型audio​:

    • audio/midi
    • audio/mpeg
    • audio/webm
    • audio/ogg
    • audio/wav
  • 视频类型video​:

    • video/webm
    • video/ogg
  • 二进制类型application​:

    • application/octet-stream 没有特定子类型就是它(重要)
    • application/x-www-form-urlencoded 传递参数时使用键值对形式(重要)
    • application/pkcs12
    • application/xhtml+xml
    • application/xml
    • application/pdf
    • application/vnd.mspowerpoint
  • 复合内容multipart​:

    • multipart/form-data 复合内容,有多种内容组合(重要)
    • multipart/byteranges​ 特殊的复合文件
  • 关于ContentType更多内容可以前往:Content-Type - HTTP | MDN (mozilla.org)

  • 关于媒体类型可以前往:MIME 类型(IANA 媒体类型) - HTTP | MDN (mozilla.org)

ContentType中对于我们来说重要的类型

  1. 通用2进制类型:application/octet-stream
  2. 通用文本类型:text/plain
  3. 键值对参数:application/x-www-form-urlencoded
  4. 复合类型:multipart/form-data​(传递的信息有多种类型组成,比如有键值对参数,有文件信息等等,上传资源服务器时需要用该类型)

上传数据

HTTP上传文件相对比较麻烦,需要按照指定的规则进行内容拼接达到上传文件的目的
其中相对重要的知识点是:上传文件时的规则

1
2
3
4
5
6
--边界字符串
Content-Disposition: form-data; name\="file";filename\="传到服务器上使用的文件名"
Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
空行
(这里直接写入传入的内容)(换行)
--边界字符串--(换行)

关于其更多的规则,可以前往官网查看详细说明

上传文件到HTTP资源服务器需要遵守的规则

  1. ContentType​设置为复合类型,并设置边界字符串

    httpWebRequest.ContentType = "multipart/form-data; boundary=边界字符串";

  2. 保证服务器允许上传

  3. 写入流前需要先设置ContentLength​内容长度

  4. 上传的数据必须按照格式写入流中

    对于通用的服务器,我们需要按照如下格式发送:

    1
    2
    3
    4
    5
    6
    --边界字符串
    Content-Disposition: form-data; name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"
    Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
    空一行
    (这里直接写入传入的内容的字节数组)(字节数组写完了别忘了换行)
    --边界字符串--(这里别忘了换行)

    具体一点来说,假设我们要上传一个图片文件,这里我们需要先序列化下面这种格式的字符串 (注意!换行需要使用 \r\n!!!)

    上传文件,头信息中:Content-Disposition​对应的值是定死的:form-data​,即:从数据来;name​对应的值也是定死的:file

    1
    2
    3
    string head = "--边界字符串\r\n" +        //这里的边界字符串是在ContentType属性设置的
    "Content-Disposition:form-data; name=\"file\";fileName=\"传到服务器上使用的文件名\"\r\n" +
    "Content-Type:application/octet-stream\r\n\r\n"; //传2进制文件,所以这里使用2进制,这里多出来的一个\r\n是为了空一行

    然后将这个头字符串序列化出来的字节数组先写入到上传流内,然后再把图片文件以文件流的形式写入到上传流内

    最后,再序列化下面的尾字符串,它用来表示结束的边界,再将序列化出来的字节数组写入到上传流内,上传就结束了

    1
    byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--MrTang--\r\n");        //前面的\r\n是给文件数据换行

上传文件具体逻辑

注意,上传文件时,http服务器需要开放上传权限,才能真正上传成功,否则即使我们将数据上传了,服务器也不会接受处理
但是完全放开上传权限又是极为不安全的(任何人都可能上传垃圾信息),因此http服务器只需要对特定账户开放上传权限

如果设定了对特定账户开放上传权限,我们就需要在上传消息前先设置需要发送身份验证表头,再设置通信凭证,以通过服务器验证

新创建一个Http请求对象,传入上传的文件要保存在服务器的哪个路径下 (不要传入多余的文件名!!!作业时排查了半天错误的笔者提示道)
设置对应的操作命令,以及请求超时时间,对于设置了上传账户的服务器,需要先设置需要发送身份验证表头,再设置通信凭证
设置ContentType​为"multipart/form-data;..."​同时还要设置消息的边界字符串

然后,先编写头部信息字符串,再编写尾部消息字符串,都序列化为字节数组,字符串的格式参考:上传文件到HTTP资源服务器需要遵守的规则

然后,开启本地文件流读取要上传的文件,再获取上传流,开始上传逻辑
先将头部信息字符串字节数组写入到上传流内,在一点一点的从本地文件流获取数据写入到上传流,最后将尾部消息字符串写入到上传流
最后执行GetResponse()​获取http响应,根据响应得到状态码判断上传是否成功

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
void Start()
{
//创建Http请求,创建时传入上传的文件要保存在服务器的哪个路径下
HttpWebRequest request = HttpWebRequest.Create("http://192.168.1.106/Http_Server/") as HttpWebRequest;
//设置操作命令和消息类型,以及请求超时时间
request.Method = WebRequestMethods.Http.Post;
request.ContentType = "multipart/form-data;boundary=MrTang";
request.Timeout = 500000;
//设置发送身份验证表头和通信凭证
request.PreAuthenticate = true;
request.Credentials = new NetworkCredential("MrTang", "MrTang123");
//编写头部信息字符串
string head = "--MrTang\r\n" +
"Content-Disposition:form-data; name=\"file\";fileName=\"http上传文件.png\"\r\n" +
"Content-Type:application/octet-stream\r\n\r\n";
//头部字符串形式的规则信息字节数组
byte[] headBytes = Encoding.UTF8.GetBytes(head);
//尾部字符串形式的边界信息字节数组
byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--MrTang--\r\n");
//将文件序列化,依次将头部字符串字节数组、文件数据字节数组、尾部边界信息字节数组写入到上传文件流内
using (FileStream localFileStream = File.OpenRead(Application.streamingAssetsPath + "/test.png"))
{
//设置上传长度,是头部字符串消息字节数组长度+文件数据字节数组长度+尾部边界字符串消息字节数组的总消息长度
request.ContentLength = headBytes.Length + localFileStream.Length + endBytes.Length;
Stream upLoadStream = request.GetRequestStream();
//先写入头部字符串形式的规则信息字节数组
upLoadStream.Write(headBytes, 0, headBytes.Length);
//再写入文件数据
byte[] bytes = new byte[2048];
int contentLength = localFileStream.Read(bytes, 0, bytes.Length);
while (contentLength != 0)
{
upLoadStream.Write(bytes, 0, contentLength);
contentLength = localFileStream.Read(bytes, 0, bytes.Length);
}
//最后写入尾部字符串形式的边界信息字节数组
upLoadStream.Write(endBytes, 0, endBytes.Length);
upLoadStream.Close();
localFileStream.Close();
}
//上传数据,获取响应
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
if (response.StatusCode == HttpStatusCode.OK)
print("上传通信成功");
else
print("上传失败!" + response.StatusCode);
}