HTTP最通俗的理解,不用担忧记不住了

导语

一屏长文,更深刻的了解HTTP协议。对于入门前端不久的同窗来讲,可能学习前端,就从HTML,CSS,JS学起,而后再入手一个框架,但对于http的理解可能还仅在知道一些面试中关于http的考题或比较少在代码层面去真正理解一些理论的知识,看完本篇但愿你能对http有一个较为深刻的理解,而且能在开发中对你有所帮助javascript

进入HTTP

http经典图

浏览器输入URL后HTTP请求返回的完整过程

网络协议分层

经典五层模型
模型图html

后续小节咱们会涉及到的知识点就是应用层和传输层。前端

  • 物理层:主要做用就是定义物理设备如何传输数据
  • 数据链路层:在通讯的实体间创建数据链路链接
  • 网络层:在节点之间传输建立逻辑链路

传输层

它旨在向用户提供可靠的端到端的服务,数据传输过程可能涉及到分片分包等,以及传输过去如何组装等,这个无需让开发者来作,所以传输层向高层屏蔽了下层数据通讯的细节。正由于如此,理解传输层的细节可以让咱们实现一个性能更高HTTP实现方式java

应用层

它帮咱们实现了http协议,为应用层提供了不少服务,而且构建与TCP协议之上,屏蔽网络传输相关细节node

http的三次握手

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报文格式图

请求报文中首行包括一些请求方法 请求资源地址和http协议版本。
响应报文中首行包括协议版本、http状态码和状态码含义等数据库

HTTP方法

用来定义对于资源的操做json

  • HTTP方法:GET, POST,HEAD,OPTIONS,PUT,DELETE,TRACE和CONNECT
  • GET: 一般用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 而且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此能够节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通讯选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1协议中预留给可以将链接改成管道方式的代理服务器
  • TRACE: 回显服务器收到的请求,主要用于测试或诊断

参考:面试官(9):多是全网最全的http面试答案

Http Code码

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理
  • 201 Created 请求已经被实现,并且有一个新的资源已经依据请求的须要而创建
  • 202 Accepted 请求已接受,可是还没执行,不保证完成请求
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 206 Partial Content,进行范围请求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other,表示资源存在着另外一个 URL,应使用 GET 方法丁香获取资源
  • 304 not modified,表示服务器容许访问资源,但因发生请求未知足条件的状况
  • 307 temporary redirect,临时重定向,和302含义相同

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误
  • 401 unauthorized,表示发送的请求须要有经过 HTTP 认证的认证信息
  • 403 forbidden,表示对请求资源的访问被服务器拒绝
  • 404 not found,表示在服务器上没有找到请求的资源
  • 408 Request timeout, 客户端请求超时
  • 409 Confict, 请求的资源可能引发冲突

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误
  • 501 Not Implemented 请求超出服务器能力范围,例如服务器不支持当前请求所须要的某个功能,或者请求是服务器不支持的某个方法
  • 503 service unavailable,代表服务器暂时处于超负载或正在停机维护,没法处理请求
  • 505 http version not supported 服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本

经过node建立一个简单的node服务

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'

HTTP特性总览

浏览器就是最多见的客户端,浏览器为了保证数据传输的安全性,具备同源策略,所谓同源是指:域名、协议、端口相同

同源策略又能够分为如下两种:

  • DOM同源策略:禁止对不一样源页面DOM进行操做。这里主要场景就是iframe跨域的状况,不一样域名的iframe是限制互相访问的
  • XMLHttpRequest同源策略: 静止使用XHR对象向不一样源的服务器发起HTTP请求

了解了浏览器同源策略的做用,若是不一样源发出请求,就会产生跨域。可是在实际开发中,咱们不少时候须要突破这样的限制,方法有如下几种(后面会有方法实践):

  • JSONP: 利用script的src标签不受同源限制,动态建立script标签
  • CORS: 服务端设置access-allow-origin
  • 经过window.name跨域
  • 经过document.domain
  • 经过Html5的postMessage

跨域知识详细可参考前端跨域整理
经过代码来看下具体是怎么样的

cors跨域

建立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的终端)依然能收到请求,只是返回的内容在浏览器端没有接收到,所以跨域并非发不出请求,只是返回的内容被浏览器拦截了而已

CORS跨域限制以及预检请求验证

修改hello.html,js改成

fetch('http://192.168.0.106:8887/', {
    method: 'POST',
    headers: {
        'Test-Cors': '123'
    }
})

浏览器访问localhost:8888,出现

请求头不容许

缘由是什么呢,且听我慢慢道来
浏览器的请求在跨域的时候默认容许的方法为
GET、HEAD、POST,其余方法不容许,须要有预检请求

  • 容许的Content-Type为
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

其余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'

JSONP跨域

去掉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

  1. 可缓存性

public、private、no-cache、no-store

  • public指的是http返回的内容所通过的任何路径(包括代理服务器和客户端浏览器)当中均可以被缓存
  • private指的是只有发起请求的浏览器才能够缓存
  • no-cache能够在本地缓存,能够在代理服务器缓存,可是这个缓存要服务器验证才可使用
  • no-store 完全得禁用缓冲,本地和代理服务器都不缓冲,每次都从服务器获取
  1. 到期

指的缓存时间,最经常使用的就是max-age,单位是秒,指的就是缓存的有效期是多长时间
s-max-age这个是代理服务器的缓存时间,只在代理服务器生效

  1. 从新验证

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

响应header图

若是没有设置缓存,每次请求都会从服务器获取。须要验证能够自行测试下

缓存命中能够查看这张图

缓存命中图

协商缓存验证头(Last-Modified,Etag)

如今咱们并非真正须要验证资源,而是为了验证浏览器是否会把验证头带过来,所以咱们能够随便设个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

第一次请求带有Last-modify和Etag

再发送请求,能够看到在Request Headers中出现,if-Modified-since和if-None-Match

第二次请求带有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

cookie和session

http是不保存状态的协议,所以咱们须要一个身份能来证实访问服务器的是谁,这里咱们用到的就是cookie和session

  • cookie的特性:经过Set-Cookie设置、下次请求会自动带上、键值对形式,能够设置多个
  • cookie的属性:max-age和expires设置过时时间、HttpOnly没法经过document.cookie访问

接下来经过代码看下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自行验证下

HTTP长链接

长链接指的是在一次请求完成后,是否要关闭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的使用目前还没大面积

Redirect

当咱们经过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服务,掌握它固然也不可或缺

相关文章
相关标签/搜索