一屏长文,更深刻的了解HTTP协议。对于入门前端不久的同窗来讲,可能学习前端,就从HTML,CSS,JS学起,而后再入手一个框架,但对于http的理解可能还仅在知道一些面试中关于http的考题或比较少在代码层面去真正理解一些理论的知识,看完本篇但愿你能对http有一个较为深刻的理解,而且能在开发中对你有所帮助javascript
浏览器输入URL后HTTP请求返回的完整过程
经典五层模型html
后续小节咱们会涉及到的知识点就是应用层和传输层。前端
它旨在向用户提供可靠的端到端的服务,数据传输过程可能涉及到分片分包等,以及传输过去如何组装等,这个无需让开发者来作,所以传输层向高层屏蔽了下层数据通讯的细节。正由于如此,理解传输层的细节可以让咱们实现一个性能更高HTTP实现方式java
它帮咱们实现了http协议,为应用层提供了不少服务,而且构建与TCP协议之上,屏蔽网络传输相关细节node
http只有请求和响应的概念,建立链接是属于TCP的操做,而链接的请求和响应是在tcp链接之上的。这是新手很容搞混的一点。在http1.1中链接能够保持,这样的好处是由于http三次握手是有开销的。http2.0中请求能够在同一个tcp链接中并发,也是大大节省了创建链接的开销。具体后续将详讲,如今说回http三次握手,以下图nginx
首先客户端发送一个要建立链接的数据包请求到服务端,包含一个标志位SYN=1和seq=Y。
而后服务端会开启一个TCP的socket端口,返回一个标志位SYN=1,确认位ACK=x+1和seq=y的数据包
最后客户端再发送一个ACK=Y+1,Seq=Z的数据包到服务端 web
这就是HTTP的三次握手全过程,三次握手的缘由是防止服务端开启一些无用链接,由于网络链接是有延迟的,若是没有第三次链接,因为网络延迟,客户端关闭了链接,而服务端一直在等待客户端请求发送过来,这就形成了资源浪费,有了三次握手,就能确认请求发送和响应请求没有问题。面试
请求报文中首行包括一些请求方法 请求资源地址和http协议版本。
响应报文中首行包括协议版本、http状态码和状态码含义等数据库
用来定义对于资源的操做json
2XX 成功
3XX 重定向
4XX 客户端错误
5XX 服务器错误
server.js
const http = require('http') http.createServer(function(request, response) { console.log('request come',request.url) response.end('hello world') }).listen(8888) console.log('server.listening on 8888')
终端进入到server.js文件下,执行node server.js 浏览器输入localhost:8888,便可看见'hello world'
浏览器就是最多见的客户端,浏览器为了保证数据传输的安全性,具备同源策略,所谓同源是指:域名、协议、端口相同
同源策略又能够分为如下两种:
了解了浏览器同源策略的做用,若是不一样源发出请求,就会产生跨域。可是在实际开发中,咱们不少时候须要突破这样的限制,方法有如下几种(后面会有方法实践):
跨域知识详细可参考前端跨域整理
经过代码来看下具体是怎么样的
建立server.js
const http = require('http') const fs = require('fs') http.createServer(function (request, response) { console.log('request come', request.url) const html = fs.readFileSync('test.html','utf8') response.writeHead(200, { 'Content-Type': 'text/html' }) response.end(html) }).listen(8888)
server.js同目录下建立hello.html,js代码以下(地址换成本身电脑ip地址)
var xhr = new XMLHttpRequest() xhr.open('GET','http://0.0.0.0:8887') xhr.send()
同目录下建立server2.js
const http = require('http') http.createServer(function (request, response) { console.log('request come',request.url) response.end('hello world') }).listen(8887) console.log('server listening on 8887')
分别启动server.js和server2.js,并在浏览器输入localhost:8888
解决方案:在server2.js中加入
response.writeHead(200, { 'Access-Control-Allow-Origin': '*' })
跨域请求成功
<font color='red'> 注意 </font>:当咱们没有加跨域请求头的时候,能够发现服务端(也就是运行server2.js的终端)依然能收到请求,只是返回的内容在浏览器端没有接收到,所以跨域并非发不出请求,只是返回的内容被浏览器拦截了而已
修改hello.html,js改成
fetch('http://192.168.0.106:8887/', { method: 'POST', headers: { 'Test-Cors': '123' } })
浏览器访问localhost:8888,出现
缘由是什么呢,且听我慢慢道来
浏览器的请求在跨域的时候默认容许的方法为
GET、HEAD、POST,其余方法不容许,须要有预检请求
其余Type也须要预检请求
其余限制包括header 详见[默认容许header](),XMLHttpRequestUpload对象均没有注册任何事件监听器以及请求中没有使用ReadableStream对象。后两个实际接触很少,能够不深究
说回预检请求,先看下图
注意:新版chorme浏览器改了,在network里面看不到了,换个浏览器
若是咱们须要这个请求头,在server2.js中的response.writeHead里面添加 'Access-Control-Allow-Headers': 'X-Test-Cors'
同理,若是须要添加容许的方法,能够添加 'Access-Control-Allow-Headers': 'Delete,PUT'
若是咱们但愿在某一段时间内发送的跨域请求再也不发送预检请求,能够在response.writeHead中设置 'Access-Control-Max-Age': '100'
去掉server.js中的请求头,并修改hello.html中js为 <script src="http://192.168.0.107:8887/"></script>
这就是一个简单的jsonp跨域,具体的能够参考上面的跨域文章
为了减小请求,加快页面访问速度。开发者能够根据须要对资源进行缓存。分为强缓存和协商缓存,经过http首部字段进行设置
Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,若是还没到失效时间就直接使用缓存
可是该方法存在一个问题:服务器时间与客户端时间可能不一致。所以该字段已经不多使用
cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件,缓存在484200S内均有效。若是同时存在cache-control和Expires,浏览器老是优先使用cache-control
last-modified是第一次请求资源时,服务器返回的字段,表示最后一段更新的时间。下一次浏览器
请求资源时就发送if-modified-since字段。服务器用本地last-modified时间与if-modified-since
时间比较,若是不一致则认为缓存已过时并返回新资源给浏览器;若是时间一致则发送304状态码,让浏览器
继续使用缓存
Etag 资源的实体标识(哈希字符串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发送变化
若是变化则返回新资源,不然返回304
接下来咱们详细看下Cache-Control
public、private、no-cache、no-store
指的缓存时间,最经常使用的就是max-age,单位是秒,指的就是缓存的有效期是多长时间
s-max-age这个是代理服务器的缓存时间,只在代理服务器生效
must-revalidate若是设置的缓存已通过期了,必须去原服务端请求,而后从新验证数据是否已通过期
proxy-revalidate应用于代理服务器缓存
理论说完了,接下来咱们经过实战看看
修改test.html,js部分修改成 <script src="./script.js"></script>
修改server.js
const http = require('http') const fs = require('fs') http.createServer(function (request, response) { console.log('request come',request.url) if (request.url == '/') { const html = fs.readFileSync('test.html', 'utf8') response.writeHead(200, { 'Content-Type': 'text/html' }) response.end(html) } if (request.url == '/script.js') { response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control':'max-age=2020', // 'Last-Modified': '2020', //'Etag': '20200217' }) response.end('console.log("script loaded")') } }).listen(8888) console.log('server start on the 8888')
打开开发者工具,咱们能够看到scripts第一次加载以后,再请求就会从缓存中获取,看下图黄色圈中部分,注意须要把红色勾选去掉
再看下响应的header
若是没有设置缓存,每次请求都会从服务器获取。须要验证能够自行测试下
缓存命中能够查看这张图
如今咱们并非真正须要验证资源,而是为了验证浏览器是否会把验证头带过来,所以咱们能够随便设个Last-Modified和Etag,在server.js中修改response.writeHead
response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control':'max-age=2020, no-cache', 'Last-Modified': '2020', 'Etag': '20200217' })
启动服务,下图是第一次请求,能够看到响应头里面有Last-Modify和Etag
再发送请求,能够看到在Request Headers中出现,if-Modified-since和if-None-Match
到这里尚未结束,当咱们验证缓存完,若是尚未过时,咱们但愿直接拿缓存,可是咱们再看下咱们的response
由图发现response中仍是有资源返回,而且code码是200,这是为啥呢,缘由很简单,咱们在服务端尚未对if-Modified-since和if-None-Match进行处理,咱们把server.jshttp.createServer修改成
http.createServer(function (request, response) { console.log('request come',request.url) if (request.url == '/') { const html = fs.readFileSync('test.html', 'utf8') response.writeHead(200, { 'Content-Type': 'text/html' }) response.end(html) } if (request.url == '/script.js') { const etag = request.headers['if-none-match'] if (etag === '20200217') { response.writeHead(304, { 'Content-Type': 'text/javascript', 'Cache-Control': 'max-age=2020,no-cache', 'Last-Modified': '2020', 'Etag': '20200217' }) response.end('') } else { response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'max-age=2020,no-cache', 'Last-Modified': '2020', 'Etag': '20200217' }) response.end('console.log("script loaded twice")') } } }).listen(8888)
无论是否须要传资源,咱们都要在最后response.end,否则本次请求一直没有结束。修改完以后,咱们能够看到请求code码变成了304,时间缩短了,可是在response中仍是有资源,这又是什么状况,这时候咱们确实成功验证了缓存,并拿取的是缓存资源,在浏览器的response中,浏览器会自动把拿到的缓存资源显示出来,并无在服务器获取。若是须要验证,能够自行在第一个response.end中添加其余内容,再看浏览器接口的response
刚才让浏览器去作协商缓存,是由于咱们设置了no-cahce,咱们把no-cache删除,浏览器应该是直接拿缓存(由于咱们设置的max-age=2020),验证以前,咱们得在刚才打开的页面去清楚浏览器的缓存,而后删除代码中的no-cache,重复刷新,均可以看到script.js 是from mermory cache。no-store也可再自行验证下
最后再提一下关于last-modify和Etag,last-modify咱们能够在把数据库取出的时候,拿取一个时间,最为数据的update time.Etag的话,数据取出的时候作个数据签名,存入Etag
http是不保存状态的协议,所以咱们须要一个身份能来证实访问服务器的是谁,这里咱们用到的就是cookie和session
接下来经过代码看下cookie,在server.js中修改response.writeHead
{ 'Content-Type': 'text/html', 'Set-Cookie': ['id=123;max-age=2','time=2020'] }
启动服务后,能够在application中的cookie看到两个cookie或者network中的接口中。id=123这个cookie设置了过时时间,过一下子再刷新能够看到id=123这个cookie消失了
前面说过,cookie跨域不共享,可是若是我想一级域名下的二级域名共享cookie,这时候我能够经过设置document.domain来实现,具体以下
{ 'Content-Type': 'text/html', 'Set-Cookie': ['id=123;max-age=2','time=2020;domain=test.com'] }
修改后,可添加host自行验证下
长链接指的是在一次请求完成后,是否要关闭TCP链接。若是TCP链接一直开着,会有必定资源消耗,可是若是还有请求,就能够继续在本次TCP链接上发送,这样能够不用再三次握手,节省了时间。实际状况中,网站并发量比较大,所以是保持长链接的,而且长链接是能够设置超时时间的,若是在这个时间里都没有发送请求了,那么链接就会关闭
接下来咱们能够分析下实际场景,以百度首页为例,打开开发者面板,而后network中,右击name属性,勾选Connection ID
咱们看到大部分链接都有复用,在http1.1中,一个域名下最大TCP链接数为6个(Chorme),所以刚开始的时候会一下建立6个链接,后面的请求会复用这些链接。
经过代码来验证下这部份内容,首先建立一个test.html
<body> <img src="/test1.jpg" alt=""> <img src="/test2.jpg" alt=""> <img src="/test3.jpg" alt=""> <img src="/test4.jpg" alt=""> <img src="/test5.jpg" alt=""> <img src="/test6.jpg" alt=""> <img src="/test7.jpg" alt=""> <img src="/test8.jpg" alt=""> </body>
新建server.js
const http = require('http') const fs = require('fs') http.createServer(function (request, response) { console.log('request come',request.url) const html = fs.readFileSync('test.html', 'utf8') const img = fs.readFileSync('timg.jpg') if (request.url === '/') { response.writeHead(200, { 'Content-Type': 'text/html', // 'Connection': 'close' }) response.end(html) } else { response.writeHead(200, { 'Content-Type': 'image/jpg', // 'Connection': 'close' }) response.end(img) } }).listen(8888) console.log('server start on the 8888')
启动服务
能够看下Waterfall,网络请求分时过程。若是须要关闭长链接,Connection的值能够写为close
这里再简单提下http2.0如今使用信道复用技术,只须要建立一个TCP链接,全部同域下请求均可以并发。若是要使用http2.0,须要保证请求时https协议,而且后端须要作较大的改变,所以如今http2.0的使用目前还没大面积
当咱们经过url去访问一个资源的时候,该资源已经再也不url指定的位置了,服务器应通知客户端该资源如今所处的位置,浏览器再去请求该资源。
经过代码来看下,新建一个server.js
const http = require('http') const fs = require('fs') http.createServer(function (request, response) { console.log('request comme', request.url) if (request.url === '/') { response.writeHead(302, { 'Location': '/new' }) response.end() } if (request.url === '/new') { response.writeHead(200, { 'Content-Type': 'text/html' }) response.end('<div>hello world</div>') } }).listen(8888) console.log('server listening on 8888')
此处测试是在同域的状况下,因此只写了一个路由,若是不相同,则把真正的地址替换/new
.启动服务,输入localhost:8888以后,会直接跳转到资源真正的位置,而且在network中也可查看发现,除了图标,有两个请求。
代码中咱们写的code码是302,若是咱们改为200,就会发现没有办法重定向。302是临时重定向,301是永久重定向,前面咱们已经说过。若是咱们把上面的302code码改为301,咱们会在终端中发现,除了第一次,无论咱们后面再输入localhost:8888多少次,终端打印请求都只有重定向后的请求,只是由于浏览器记住了原地址被永久重定向了,因此,不会向原路径发起请求。在实际开发中,应当谨慎使用永久重定向,由于一旦永久重定向了,会在浏览器尽量长的时间保留定向后的资源路径而不会请求原路径
本次分享目的是经过代码来把原来咱们知道的一些知识点能够再深刻一些,梳理好Http知识的前因后果。但愿能对一些小伙伴有所帮助,若是你们喜欢个人行文风格的话,我接下来将带入web 服务器Nginx的一些实战,在实际开发中咱们会用nginx作代理和一些cache,所以做为一个http服务,掌握它固然也不可或缺