您当前的位置: 首页 >  http

理论加实战,彻底搞懂HTTP知识

蔚1 发布时间:2020-03-11 23:31:00 ,浏览量:5

HTTP 理论知识为辅助,通过通俗的代码去深入理解 HTTP 知识的来龙去脉,帮助入门前端不久的同学彻底摆脱面试的关于 HTTP 问题,而不只是停留在表面,并且需要死记硬背。后期还包括 Nginx 反向代理

不论前端同学还是后端同学,掌握好 HTTP 原理都是编码之路必不可少的。

本文包括以下几点

  • 网络五层模型
  • 三次握手,四次挥手
  • HTTP 报文
  • 跨域
  • 缓存
  • Nginx

希望大家有所收获并能提出您对本文宝贵的意见。

导语

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

进入 HTTP http 经典图

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

网络协议分层

经典五层模型模型图

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

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

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

应用层

它帮我们实现了 http 协议,为应用层提供了很多服务,并且构建与 TCP 协议之上,屏蔽网络传输相关细节

http 的三次握手

http 只有请求和响应的概念,创建连接是属于 TCP 的操作,而连接的请求和响应是在 tcp 连接之上的。这是新手很容搞混的一点。在 http1.1 中连接可以保持,这样的好处是因为 http 三次握手是有开销的。http2.0 中请求可以在同一个 tcp 连接中并发,也是大大节省了建立连接的开销。具体后续将详讲,现在说回 http 三次握手,如下图

首先客户端发送一个要创建连接的数据包请求到服务端,包含一个标志位 SYN=1 和 seq=Y。然后服务端会开启一个 TCP 的 socket 端口,返回一个标志位 SYN=1,确认位 ACK=x+1 和 seq=y 的数据包最后客户端再发送一个 ACK=Y+1,Seq=Z 的数据包到服务端

这就是 HTTP 的三次握手全过程,三次握手的原因是防止服务端开启一些无用连接,因为网络连接是有延迟的,如果没有第三次连接,由于网络延迟,客户端关闭了连接,而服务端一直在等待客户端请求发送过来,这就造成了资源浪费,有了三次握手,就能确认请求发送和响应请求没有问题。

HTTP 报文

HTTP 报文格式图

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

HTTP 方法

用来定义对于资源的操作

  • 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': '*'})

跨域请求成功 注意 :当我们没有加跨域请求头的时候,可以发现服务端(也就是运行 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 为这就是一个简单的 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 这个是代理服务器的缓存时间,只在代理服务器生效
  2. 重新验证must-revalidate 如果设置的缓存已经过期了,必须去原服务端请求,然后重新验证数据是否已经过期proxy-revalidate 应用于代理服务器缓存理论说完了,接下来我们通过实战看看修改 test.html,js 部分修改为

修改 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

                                  

新建 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('
hello world
') }}).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 服务,掌握它当然也不可或缺

阅读全文: http://gitbook.cn/gitchat/activity/5e6903269038e351455d036d

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

关注
打赏
1688896170
查看更多评论

蔚1

暂无认证

  • 5浏览

    0关注

    4645博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0820s