本文对HTTP协议的相关特性以及客户端-服务端在请求-响应过程中的涉及的问题进行略为深入的讨论,也是参考慕课网课程《HTTP协议原理+实践》进行整理记录的一篇学习笔记。
相关的示例代码:HTTP特性演示
CORS跨域请求
CORS跨域是由浏览器安全策略引起的,常用jsonp解决,也可以在服务端添加相应的允许跨域的header Access-Control-Allow-Origin
,这样就可以直接使用ajax直接发起跨域请求。
注意:
-
不管服务端有没有设置header,浏览器都会正常发送跨域请求,服务端也会接受返回内容,如果没有设置
Access-Control-Allow-Origin
,浏览器在解析内容后会自动拦截。这是浏览器的跨域限制问题,用curl工具就没关系。 -
浏览器允许在html中使用
<script src="">
标签来实现跨域请求,这也是jsonp的而基本原理。
但是,也不是所有情况都可以仅仅通过设置一个header来解决问题,CORS跨域还有一些限制以及预请求验证功能。
CORS中的限制:
-
允许方法:
- GET
- HEAD
- POST
-
允许Content-Type:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
-
其它限制
- 自定义请求头限制
其它情况下,必须发起预请求[OPTIONS方法]验证后才能正常发送,否则会报错。另外,可以设置 Access-Control-Max-Age
来限制预请求的时限,在这个时间内发起任何请求无需发起预请求的验证步骤。
演示场景:
使用浏览器从 localhost:8888
向 localhost:8887
发起GET跨域请求,同时在服务端作了一定的预请求限制方案:
client
<script>
fetch('http://localhost:8887', {
method: 'POST',
// 自定义header
headers: {
'X-Test-Cors': '123'
}
})
</script>
server
response.writeHead(200, {
'Access-Control-Allow-Origin': 'http://127.0.0.1:8888',
'Access-Control-Allow-Headers': 'X-Test-Cors',
'Access-Control-Allow-Methods': 'POST, PUT, DELETE',
'Access-Control-Max-Age': '1000'
})
补充:关于 Fetch API
缓存头Cache-Control
可缓存性:
-
public
: 任何地方都可以缓存 -
private
: 只有发起请求的浏览器可以缓存 -
no-cache
: 浏览器不允许缓存,但是代理服务器可以缓存 -
no-store
: 任何地方都不能可以缓存
设置缓存过期时间:
-
max-age=<seconds>
: 浏览器缓存时间 -
s-maxage=<seconds>
: 代理服务器缓存时间
演示场景:
在一个html文件中通过script标签去引用一个js文件,当客户端请求这个js文件路径时,服务端返回一段js代码,并对文件进行缓存。
if (request.url === '/script.js') {
response.writeHead(200, {
'Content-Type': 'text/javascript',
// 设置缓存头,value可以有多个,用逗号分隔
'Cache-Control': 'max-age=20'
})
response.end('console.log("script loaded")')
}
第一次访问从服务器加载,在有效时间内再次访问会从直接缓存中读取,如果这期间修改了文件内容,也不会去加载更新的内容。这是由于设置了Cache-Control之后不会去经过服务端的验证。一般情况下,我们希望浏览器去缓存一些静态资源文件,提高页面加载速度,但是也要考虑资源更新的问题。
注意:如果同时设置了 max-age 和 no-cache 之后,每一次浏览器发起请求,还是会先去服务端进行资源验证,验证之后如果确定这个资源可以使用缓存,才会去本地读取缓存,并不是直接从本地缓存中读取。
前端如何解决浏览器长缓存问题?
- 在打包静态资源的时候在对文件名加上一串哈希码,hash是通过文件内容进行计算的,如果内容有变化文件名也会随之改变,浏览器就会当成新的文件去请求。
资源验证
-
Last-Modified
配合If-Modified-Since
使用 -
Etag
配合If-None-Match
使用
如果对一个可缓存的文件设置了这两个Headers,那么在有效缓存实践内再次访问,客户端发起的请求头信息中就会新增两个Header,即 If-Modified-Since
和 If-None-Match
,它们的值分别为第一次访问时返回的 Last-Modified
和 Etag
的值,作用是向服务端验证这两个缓存是否有效。
但是这样设置仍然会从服务器上重新加载一次文件,而我们希望的是服务端告诉浏览器直接去读取本地缓存而已,这时候就需要去判断 If-None-Match
的值是否为 Etag
的值。
if (request.url === '/script.js') {
const etag = request.headers['if-none-match']
if (etag === '777') {
response.writeHead(304, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=2000000, no-cache',
'Last-Modified': '123',
'Etag': '777'
})
response.end()
} else {
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=2000000, no-cache',
'Last-Modified': '123',
'Etag': '777'
})
response.end('console.log("script loaded twice")')
}
}
注:304状态码的语义是 Not Modified
nginx代理缓存配置
nginx是一个单纯的http服务器,一般可以用于负载均衡或代理缓存。
示例:
proxy_cache_path cache levels=1:2 keys_zone=my_cache:10m;
server {
listen 80;
server_name test.com;
location / {
proxy_cache my_cache;
proxy_pass http://127.0.0.1:8888;
proxy_set_header Host $host;
}
}
levels=1:2 在指cache目录下创建相应层级的子目录存放缓存文件,而不是全部堆积在根目录下。my_cache 是自定义缓存名,10m是允许最大缓存大小为10MB。
如上,访问 test.com 后Nginx服务器会代理到访问 http://127.0.0.1:8888 启动的服务,同时会开启页面缓存,这份缓存是放在服务端的cache目录下,通过响应头 Cache-Control
的 s-maxage
属性来控制过期时间。
Cookie
要使用cookie只要在服务端的响应头中添加 Set-Cookie
属性,里面可以设置多个cookie,以键值对的形式存在。cookie保存在浏览器中,下一次客户端访问同一个域名的服务器时,请求头中会自动带上之前获取到的cookie。
Cookie属性:
-
max-age
和expires
设置过期时间 -
Secure
只在HTTPS的时候发送。 -
设置了
HttpOnly
就无法通过document.cookie
访问。
实例:
if (request.url === '/') {
const html = fs.readFileSync('test.html', 'utf8')
response.writeHead(200, {
'Content-Type': 'text/html',
'Set-Cookie': ['id=123; max-age=2', 'abc=456; domain=test.com']
})
response.end(html)
}
cookie只有在设置的域名下才可以访问,但也可以通过
domain
指定所有的二级域名也可以共享cookie。
HTTP长连接
Http 1.1 引入了长连接,请求头和响应头中都有 Connection: keep-alive
浏览器在访问Web服务时会并发地创建一定数量的TCP连接(Chrome是6个),然后在这些TCP连接的基础上去发起http请求的三次握手,无论加载多少资源文件,都会去复用前面创建的TCP连接,但是是有先后顺序的。我们可以在Chrome调试工具中打开 Connection ID 项来查看是否复用了同一个TCP连接。
Http 2 引入了信道复用,在TCP连接上可以并发地去发送http请求,也就是说连接网站时只需要1个TCP连接,减少了大量开销,整体访问速度会有很大提升。
数据协商
概念:在客户端发送给服务端一个请求时,客户端会声明这个请求的数据格式和相关限制,服务端后根据客户端的请求来区分应该返回怎样的数据。
数据协商分为请求和返回两部分。
请求 - Accept
设置 | 定义 |
---|---|
Accept | 指定数据类型,根据 MIME Types 声明可接受的服务端返回的数据格式。 |
Accept-Encoding | 指定数据传输的编码方式,限制服务端如何进行数据压缩。 |
Accept-Language | 指定希望展示的语言 |
User-Agent | 表示浏览器的相关信息(如区分移动端或PC端浏览器……) |
Accept只是客户端希望服务端返回的方式(通常会列举很多种),实际上服务端不一定返回要求的格式。
返回 - Content
设置 | 定义 |
---|---|
Content-Type | 声明服务端实际返回的数据格式 |
Content-Encoding | 声明压缩方式,如gzip |
Content-Language | 是否返回了相关的语言 |
const html = fs.readFileSync('test.html')
response.writeHead(200, {
'Content-Type': 'text/html',
// 'X-Content-Options': 'nosniff'
'Content-Encoding': 'gzip'
})
response.end(require('zlib').gzipSync(html))
zlib压缩后传输的数据(包含内容和头信息)大小会变小,只是为了减少数据传输时的开销,但是解压出来的body数据的大小还是不变的。
Html表单允许发送的 Content Type
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
补充:用ajax发送带有文件的表单数据
var form = document.getElementById('form')
form.addEventListener('submit', function (e) {
e.preventDefault()
var formData = new FormData(form)
fetch('/form', {
method: 'POST',
body: formData
})
})
submit事件是绑定在
<form>
元素上的。
重定向
如果资源转移到了其他URL,则需要告诉客户端相应的目标路径并完成跳转:
if (request.url === '/') {
response.writeHead(302, { // or 301
'Location': '/new'
})
response.end()
}
if (request.url === '/new') {
response.writeHead(200, {
'Content-Type': 'text/html',
})
response.end('<div>this is content</div>')
}
说明:
-
浏览器默认302跳转,302是临时重定向,每次访问都会经过服务器的跳转过程到达新的URL。
-
301是永久重定向,告诉浏览器下一次直接访问新的URL即可,实际上是放到了浏览器的缓存中直接读取,所以301不能反悔,因为用户浏览器的缓存是不可控的。
-
301重定向将SEO评分从旧地址直接转移到新地址,302重定向会被认为作弊。
CSP
CSP的全称为Content-Security-Policy,即“内容安全策略”。
作用:
- 限制资源获取
- 报告资源获取越权
网友评论