在 HTTP 协议的请求头中,有一个很重要的属性:Content-Type,它的作用是:
-
作为请求:告诉服务器,客户端发送的数据类型。
-
作为响应:告诉客户端,服务器响应的数据类型。
客户端或者服务器拿到数据后,通过该数据类型,就可以正确的解析数据。
由于 GET 请求不存在请求体部分,它的参数都是拼接在 URL 尾部,浏览器把数据转换成一个字符串(key1=value1&key2=value2…),然后把这个字符串追加到 URL 后面,用 ? 分割,因此 GET 请求的请求头不需要设置 Content-Type 字段。
所以下面我们所讲的都是针对 POST 请求而言。
Content-Type 属性的值有很多种,下面列举几种最常见的:
application/x-www-form-urlencoded
这是最常见的数据类型,前端表单默认使用的就是这种类型。
比如有这么一个登录表单:
<form action="login.do" enctype="application/x-www-form-urlencoded" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">登录</button>
</form>
注意 “enctype” 属性,该属性规定在发送到服务器之前,浏览器应该如何对表单数据进行编码。默认是 “application/x-www-form-urlencoded”。
当我们点击 “登录” 按钮时,浏览器会将所有内容进行编码,拼接成 “key=value” 的格式,也就是键值对的格式。多个键值对之间用 “&” 分割。
当我们提交时,在 Chrome 浏览器中,可以看到请求体的数据格式是这样子的:
可以看到浏览器帮我们拼接成了键值对的格式,key 就是输入框的 name,而 value 就是我们输入的内容。
当服务器接收到数据后,根据 Content-Type 属性获取到数据类型,就能正确的解析数据。
接下来是后台代码:
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
public String login(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password) {
return "index.jsp";
}
当 Spring MVC 收到客户端发送来的数据时,根据 Content-Type 声明的数据类型,通过使用 HandlerAdapter 配置的 HttpMessageConverters 来解析请求体中的数据,然后绑定到相应的 bean 上,bean 的类型、个数和顺序跟我们方法声明的参数是一样的。这样后台就接收到了数据。
application/json
这也是一种很常见的数据类型,这种类型的数据是序列化后的 JSON 字符串。
比如有这么一个登录表单:
<form>
<input type="text" name="username" />
<input type="password" name="password" />
<button type="button">登录</button>
</form>
后台代码如下:
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody
public String login(@RequestBody User user) {
return "login success.";
}
处理 application/json 编码的数据时,必须使用 @RequestBody 注解,Spring MVC 通过使用 HandlerAdapter 配置的 HttpMessageConverters 来解析请求体中的数据,然后绑定到 @RequestBody 注解的对象上。
AJAX 代码如下:
var username = $("input[name='username']").val();
var password = $("input[name='password']").val();
var data = {
username: username,
password: password
}
$.ajax({
type: "post",
url: "/login.do",
contentType: "application/json",
data: JSON.stringify(data),
success: function(data){
},
error: function(data){
}
});
JQuery 的 AJAX 默认传递的是 application/x-www-form-urlencoded 类型的数据,如果想传递 JSON 类型,则需指定 contentType: application/json。
由于 application/json 类型的数据是序列化后的 JSON 字符串,所以我们也必须手动把 JSON 对象序列化成字符串,我们可通过 JSON.stringify(data) 方法进行序列化。
通过 Chrome 浏览器可以看到请求体的数据格式是这样子的:
multipart/form-data
在最初的 http 协议中,没有上传文件方面的功能。后来为了支持文件上传,提高二进制文件的传输效率,新增了 multipart/form-data 数据类型。
比如有这么一个登录表单:
<form action="login.do" enctype="multipart/form-data" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="file" name="avatar">
<button type="submit">登录</button>
</form>
表单的 enctype 属性必须指定为 multipart/form-data,否则不能上传文件。
后台代码如下:
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
public String login(String username, String password, MultipartFile avatar) {
return "index.jsp";
}
MultipartFile 这个类是用来接受前台传过来的文件。
重点是 multipart/form-data 数据类型的请求体,这种数据类型的请求体跟前面两个完全不一样。
通过 Chrome 浏览器我们可以看到请求体的数据格式,请看下图:
首先先看 Content-Type 属性,它的值是:multipart/form-data; boundary=—-WebKitFormBoundaryxJ5HRAtPAwUo1RsG
multipart/form-data 是我们在表单里指定的数据类型,这个是必须的。
那么这个 boundary=—-WebKitFormBoundaryxJ5HRAtPAwUo1RsG 又是什么呢?
boundary 是边界符,作用是用来分割多个表单项和文件。它是浏览器自动帮我们加上的,边界符的值不是固定的,可以随便取,边界符也是必须的。
通过上图,我们来分析一下请求体的数据结构。
首先先看一个公式:
分割符 = "--" + 边界符
结束符 = "--" + 边界符 + "--"
根据上面的公式,我们可以得出分割符和结束符的值:
分割符:------WebKitFormBoundaryxJ5HRAtPAwUo1RsG
结束符:------WebKitFormBoundaryxJ5HRAtPAwUo1RsG--
先分析第一个表单项:
------WebKitFormBoundary8vr5fnZ5TkqDW6kZ
Content-Disposition: form-data; name="username"
zhangsan
第一行是分割符,但是仅仅有分割符是不够的,这样的数据格式还是不对,服务器还是无法解析数据。通过查资料得知,分割符必须以回车符(\r)+换行符(\n)结尾。
也就是说,第一行的内容其实是:分割符 + 回车符 + 换行符。
------WebKitFormBoundaryxJ5HRAtPAwUo1RsG\r\n
接下来看第二行,第二行的作用是告诉服务器对应字段(表单)的相关信息,第二行也必须以 “回车符 + 换行符” 结尾。所以第二行的内容其实是:
Content-Disposition: form-data; name="username"\r\n
接下来看第三行,第三行虽然没有显示任何内容,但第三行的内容其实是 “回车符 + 换行符”,只是不显示而已。
也就是说第三行的内容其实是:
\r\n
接下来看第四行,第四行是我们输入的表单值了,同样必须以 “回车符 + 换行符” 结尾。
也就是说第四行的内容其实是:
zhangsan\r\n
综上所述,一个完整的文本格式的数据结构应该是这样的:
------WebKitFormBoundary8vr5fnZ5TkqDW6kZ\r\nContent-Disposition: form-data; name="username"\r\n\r\nzhangsan\r\n
接下来再分析一下文件的内容:
------WebKitFormBoundary8vr5fnZ5TkqDW6kZ
Content-Disposition: form-data; name="avatar"; filename=""
Content-Type: application/octet-stream
文件只比文本多了一行 “Content-Type: application/octet-stream” 内容,但其实两者的数据结构其实是一样的:
------WebKitFormBoundary8vr5fnZ5TkqDW6kZ\r\nContent-Disposition: form-data; name="avatar"; filename=""\r\nContent-Type: application/octet-stream\r\n\r\n(文件的二进制数据)\r\n
最后一行是结束符,结束符也必须以 “回车符 + 换行符” 结尾。
所以最后一行的内容其实是:
------WebKitFormBoundary8vr5fnZ5TkqDW6kZ--\r\n
综上所述,请求体的完整数据结构如下图: