http 协议是 第七层协议,其在前、后端、移动端都很常用,通常都用 json 传递,但其实也有很多传递方式,本文将对 Content-Type 一探究竟。
常有的有GET、POST、PUT(全量编辑)、PATCH(增量编辑)、DELETE、HEAD(仅头部)、OPTIONS等。
HEAD 类似 GET,但因为只返回 header 所以速度更快。 浏览器常用此方式检查资源是否变化,若未变化则复用缓存,否则用 GET 重新请求资源。
参考:HTTP HEAD Request Method
Content-Type 用于指明 http 协议的媒体类型(传文本,文件,图片,还是视频等),在 chrome 的 F12 开发者工具可以看到 Content-Type,如下图所示:
其由三部分组成:
其中 media type 有如下值域:
# Application
application/EDI-X12
application/EDIFACT
application/javascript
application/octet-stream
application/ogg
application/pdf
application/xhtml+xml
application/x-shockwave-flash
application/json
application/ld+json
application/xml
application/zip
application/x-www-form-urlencoded
# Audio
audio/mpeg
audio/x-ms-wma
audio/vnd.rn-realaudio
audio/x-wav
# Image
image/gif
image/jpeg
image/png
image/tiff
image/vnd.microsoft.icon
image/x-icon
image/vnd.djvu
image/svg+xml
# Multipart
multipart/mixed
multipart/alternative
multipart/related (using by MHTML (HTML mail).)
multipart/form-data
# Text
text/css
text/csv
text/html
text/javascript (obsolete)
text/plain
text/xml
# Video
video/mpeg
video/mp4
video/quicktime
video/x-ms-wmv
video/x-msvideo
video/x-flv
video/webm
在 postman 中即可选 multipart/form-data 类型,其中可同时传图片、文件和文本,示例如下:
我们来看一个简单的 form 表单:
<form action="/submit" method="POST" enctype="multipart/form-data">
<input type="text" name="username"><br>
<input type="text" name="password"><br>
<button>提交button>
form>
当提交的时候,查看浏览器的网络请求:
请求头:
POST /submit HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------340073633417401055292887335273
Content-Length: 303
请求体:
-----------------------------340073633417401055292887335273
Content-Disposition: form-data; name="username"
张三
-----------------------------340073633417401055292887335273
Content-Disposition: form-data; name="password"
123456
-----------------------------340073633417401055292887335273--
具体格式是这样的:
...
Content-Type: multipart/form-data; boundary=${boundary}
--${boundary}
...
...
--${boundary}--
这就是 multipart/form-data 的传输过程了,但是这里面有三个大坑:
请求头 Content-Type 里面的 boundary 分隔符比请求体用的分隔符少了两个杠(-)
从请求头中取分隔符之后,一定要在之前加两个 - 再对请求体进行分割
请求头 Content-Length 的换行用的是 \r\n 而不是 \n
请求体的真实面目是下面的字符串:
“-----------------------------340073633417401055292887335273\r\nContent-Disposition: form-data; name=“username”\r\n\r\n张三\r\n-----------------------------340073633417401055292887335273\r\nContent-Disposition: form-data; name=“password”\r\n\r\n123456\r\n-----------------------------340073633417401055292887335273–\r\n”
请求头 Content-Length 的值表示字节的长度,而不是字符串的长度
因为字节的长度跟编码无关,而字符串的长度往往跟编码有关,举个例子,在 utf8 编码下:
console.log('a1'.length) // 2
console.log(Buffer.from('a1').length) // 2
console.log('张三'.length) // 2
console.log(Buffer.from('张三').length) // 6
如果仅仅是基本的字符串类型,完全可以用 www-form-urlencoded 来进行传输,multipart/form-data 强大的地方是其能够传输二进制文件的能力,我们看一下如果包含二进制文件的话应该如何处理。我们增加一个 file 类型的 input,上传一张图片作为头像,发现请求体多出了一部分:
-----------------------------114007818631328932362459060915
Content-Disposition: form-data; name="avatar"; filename="1.jpg"
Content-Type: image/jpeg
xxxxxx文件的二进制数据xxxxx
可以发现,文件类型的 part 跟之前字符串的格式有所不同了,head 部分有两个头字段,多出一个 Content-Type 头,而且 Content-Disposition 头多出来 filename 字段,body 部分是文件的二进制数据。
了解这这些规律之后,接下来就可以在服务端对 multipart/form-data 进行解码了:
const http = require('http')
const fs = require('fs')
http
.createServer(function (req, res) {
// 获取 content-type 头,格式为: multipart/form-data; boundary=--------------------------754404743474233185974315
const contentType = req.headers['content-type']
const headBoundary = contentType.slice(contentType.lastIndexOf('=') + 1) // 截取 header 里面的 boundary 部分
const bodyBoundary = '--' + headBoundary // 前面加两个 - 才是 body 里面真实的分隔符
const arr = [], obj = {}
req.on('data', (chunk) => arr.push(chunk))
req.on('end', function () {
const parts = Buffer.concat(arr).split(bodyBoundary).slice(1, -1) // 根据分隔符进行分割
for (let i = 0; i < parts.length; i++) {
const { key, value } = handlePart(parts[i])
obj[key] = value
}
res.end(JSON.stringify(obj))
})
})
.listen(3000)
// 对分隔出来的每一部分单独处理,如果是二进制的就保存到文件,是字符串就返回键值对:
function handlePart(part) {
const [head, body] = part.split('\r\n\r\n') // buffer 分割
const headStr = head.toString()
const key = headStr.match(/name="(.+?)"/)[1]
const match = headStr.match(/filename="(.+?)"/)
if (!match) {
const value = body.toString().slice(0, -2) // 把末尾的 \r\n 去掉
return { key, value }
}
const filename = match[1]
const content = part.slice(head.length + 4, -2) // 文件二进制部分是 head + \r\n\r\n 再去掉最后的 \r\n
fs.writeFileSync(filename, content)
return { key, value: filename }
}
// 这里面涉及到 buffer 的分割,nodejs 中并没有提供 split 方法,可根据 slice 方法自己实现
Buffer.prototype.split = function (sep) {
let sepLength = sep.length, arr = [], offset = 0, currentIndex = 0
while ((currentIndex = this.indexOf(sep, offset)) !== -1) {
arr.push(this.slice(offset, currentIndex))
offset = currentIndex + sepLength
}
arr.push(this.slice(offset))
return arr
}
参考
Get 方法通常用 Content-Type = application/x-www-form-urlencoded
方式,而在 url 中有两种方式:
www.ppp.com/qqq/?a=x&b=y&c=z
使用,其语义是参数,且可传递多个参数www.ppp.com/qq q
使用,其语义是 RESTful 来表示资源,通常放置 id、name 这些信息等在 postman 软件,即可设置为 application/json 方式,这是 web 端文本常用的消息格式,示例如下: