全文主要参考如下gituhub项目,在此基础上补充了一些自己的东西:

https://github.com/febobo/web-interview

项目网站:web前端面试 - 面试官系列

一个非常全面,细致的前端面试题库,十分的推荐,深刻体会到github的强大之处了。

http是什么与https有什么区别

http

定义

http,即超文本传输协议,常被用于在浏览器网站服务器之间的通信,以明文方式发送内容,不提供任何方式的数据加密。

特点

  • 无状态

    HTTP协议无法根据之前的状态来处理本次请求。

  • 灵活

    HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

状态码

  • 4xx(客户端错误)

    • 404:服务器中不存在请求的资源。
    • 403:请求的权限不足
    • 401:身份认证失败,该用户不存在
    • 400(bad request):请求存在语法错误,通常是因为请求的方式不符合接口文档规范
  • 5xx(服务端错误)

    • 500:服务器错误,但是未给出具体原因
    • 502:上游服务器返回了错误的响应
    • 504:上游服务器响应超时
  • 3xx(重定向)

    • 304:服务器提示浏览器读取缓存(重定向到缓存)
    • 302:临时重定向,第一次请求返回一个临时请求url,第二次请求访问这个临时url,产生两次请求
    • 301:永久重定向,第一次请求返回一个永久请求url,第二次请求访问这个永久url,产生两次请求
  • 2xx(请求成功)

    • 200(成功):请求已成功,并返回响应头响应体

    • 201:创建用户成功(由0到1的过程)

    • 204:服务器成功处理了请求,但是没有返回任何内容04通常表示“没有”的概念;通常用于响应DELETE请求,表示资源已被成功删除,但没有返回具体内容。

版本区别

  • HTTP1.0

    默认使用短连接(请求头中的Connection字段的值默认是close)每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理并响应数据后立即断开TCP连接。

    比如,解析html文件,当发现文件中存在资源文件的时候,这时候又创建单独的链接

    最终导致,一个html文件的访问,包含了多次的请求和响应,每次请求都需要创建连接、关系连接

    频繁的建立,断开连接,明显造成了性能上的缺陷,如果需要建立长连接,需要设置一个非标准的Connection字段

    1
    Connection: keep-alive

    也就是说其实http1.0也能实现长连接,但是需要手动修改请求头。

  • HTTP1.1

    与HTTP1.0的区别在于:

    • 默认支持长连接

      即在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。这样,在加载html文件的时候,文件中多个请求和响应就可以在一个连接中传输,减少了加载时间

    • 下次请求不需要等待上次请求响应

      但是服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。

    • 添加新的请求方法比如putdeleteoptions(预检请求),添加了新的请求头比如If-None-Match,If-Modified-Since

  • http2.0

    对比项HTTP/1.1HTTP/2.0
    协议格式文本格式(明文可读)二进制帧(高效解析)
    连接方式多个 TCP 连接(6-8 并行)单一 TCP 连接多路复用
    队头阻塞存在(请求按顺序处理)消除(帧可乱序传输)
    头部开销重复传输,未压缩HPACK 压缩(高频字段用静态表索引)
    资源推送需客户端显式请求服务器可主动推送
    优先级控制有限(依赖浏览器调度)显式流优先级设置

https

https是基于http的,在http的基础上使用了TLS/SSL加密,详见文章hexo博客搭建的一些思考 | 三葉的博客

GET请求和POST请求的区别

  • 用途不同

    get请求被用来获取数据,post请求被用来提交数据

  • 编码方式

    GET请求只能进行url编码,而POST支持多种编码方式。

    url编码是将url中不能包含的字符(如汉字,空格,特殊符号等),转换成浏览器和服务器都能识别的格式,通常使用百分号加上2位16进制数来表示每个字符的acsii编码,因此url编码也叫百分号编码。url编码有利于保证url的合法性。

  • 携带信息

    GET请求一般把携带的参数放在URL上,不安全,而且url上携带的参数长度限制,而POST请求一般把携带的参数放在请求体上,没有长度限制,也相对更为安全。

    然而,从传输的角度来说,他们都是不安全的,因为HTTP 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文。只有使用HTTPS才能加密安全。

http常用请求头

  • Accept:客户端能够接受的响应数据类型

    1
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  • Authorization:用于超文本传输协议的认证信息

    1
    Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • Cache-Control: 不仅仅可以在请求中携带,也可以在响应中携带

    • 在请求中携带:
      • no-cache:强制进行协商缓存
      • no-store:客户端 直接忽略缓存,每次都向服务器发送完整请求,服务器必须返回 200 OK 和完整资源内容,不会返回 304
      • max-age=<seconds>:客户端愿意接受一个已经存在的缓存(愿意接受强缓存),只要它的年龄不超过指定秒数
    • 在响应中携带:
      • no-cache:允许浏览器缓存资源,但是不设置资源的有效时间,这样就使得浏览器无法进行强缓存,每次都得进行协商缓存。效果就等同于设置max-age=0
      • no-store:浏览器不会将响应内容保存到本地缓存中,中间代理(如代理服务器或 CDN)也不会缓存该响应。常见场景包括:敏感数据(如银行账户信息、个人隐私数据)的传输;动态生成的内容(如实时股票价格、聊天记录),这些内容每次请求都可能不同,简单的来说就敏感的内容和动态变化的内容都没有必要缓存
      • max-age=<seconds>:指定资源被认为新鲜的最大时间长度,在此期间内无需再次验证资源
  • Content-Length:表示请求体的长度

  • Content-Type:告知服务器请求体的数据类型

  • If-Modified-Since:在协商缓存中被用来询问服务器是否应该使用缓存。

    1
    If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT //值为上次返回的Last-Modified, 询问自该时间点后资源是否发生改变
  • If-None-Match:在协商缓存中被用来询问服务器是否应该使用缓存。

    1
    2
    If-None-Match: "737060cd8c284d8af7ad3082f209582d"
    //值为上次返回的Etag,就是文件内容的哈希值,文件内容改变,这个值就会改变。
  • Cookie:携带cookie,cookie具体的介绍前往js面试一文中。

    1
    2
    3
    buvid3=57161BE6-C8C6-DA39-581F-E6FCB97E737107226infoc;
    b_nut=1708906907;
    i-wanna-go-back=-1;//就是这样的键值对格式,一个键值对就是一个cookie,其实每个cookie包含的信息远不止如此

浏览器的缓存策略

浏览器的缓存方式主要分为两大类,强缓存和协商缓存

  • 强缓存

    当浏览器请求某个资源的时候,如果浏览器缓存中存在该资源,且没有过期,那么将直接使用缓存,而不会发送请求(缓存是否过期是根据缓存的Expires/Cache-Control字段来判断的)。

    特点是客户端查看缓存资源的特定字段,来判断是否使用缓存。

    1
    2
    3
    4
    5
    //设置缓存的过期时间,即10s后
    //将10s后的日期对象,转化成格林尼治标准时间
    res.setHeader('Expires', new Date(new Date().getTime() + 1000 * 10).toGMTString())
    //设置缓存有效时间为10s
    res.setHeader('Cache-Control','max-age=10')
  • 协商缓存

    是指在缓存中的资源过期后,浏览器通过在请求头中设置If-Modified-Since或者If-None-Match字段,询问服务器是否应该使用缓存;如果服务器发现资源未改变,则响应304提示浏览器使用缓存,否则响应200返回新的资源,以及最近一次修改该资源的时间即Last-Modified,或者最新的ETag值。

    If-Modified-Since的值,为上次返回的Last-Modified值,If-None-Match的值为上次返回的ETag值;Last-Modified 表示本地文件最后修改日期;Etag,就是由文件内容得出的哈希值,文件内容改变,这个值就会改变。

    协商缓存好比食物的保质期过了,于是询问还能不能吃。

如何理解OSI七层模型

  • 应用层

    该层协议定义了应用进程之间的交互规则,包括DNS协议,HTTP协议,电子邮件系统采用的 SMTP协议等。

    在应用层交互的数据单元我们称之为报文

  • 表示层

    该层提供的服务主要包括数据压缩数据加密以及数据描述,使应用程序不必担心在各台计算机中表示和存储的内部格式差异

  • 会话层

    会话层就是负责建立、管理和终止表示层实体之间的通信会话

    该层提供了数据交换的定界和同步功能,包括了建立检查点恢复方案的方法

  • 传输层

    传输层的主要任务是为两台主机进程之间的通信提供服务。其中,主要的传输层协议是TCPUDP

  • 网络层

    负责主机主机的通信,ip地址就工作在这一层,常见的设备比如路由器。在发送数据时,网络层把传输层产生的报文或用户数据报封装成分组或者包,向下传输到数据链路层。

    在网络层使用的协议是无连接的网际协议(Internet Protocol)和许多路由协议

  • 数据链路层

  • 物理层

比较重要的就是应用层,传输层,网络层,数据链路层,物理层,对这些模型的解释主要还是偏概念,较难以理解,了解就好。

如图所示PC0-PC5,这6台主机通过交换机Switch0相连,这6台主机构成一个广播域,也可以说是一个内部网络,一个局域网(LAN)

这6台电脑之间的通信不需要经过路由器,也就是说“不需要联网”。

Switch0又和一个路由器Router0相连,这就把这个内部网络互联网连接了起来,允许这些主机访问其他主机或者服务器,比如B站服务器。

这台路由器常常被叫做默认网关。路由器会给连接到它的每台主机(也包括自身)动态分配(DHCP,动态主机配置协议)一个私有ip地址,如图所示。

当PC0中的浏览器要发送一个请求时,这个请求在应用层被称作报文,然后被交给传输层传输层报文源端口目标端口封装成,交给网络层网络层源ip地址目标ip地址封装成

我们还需要知道这个目标ip地址对应的mac地址,才能封装成,才能在数据链路层的交换机上进行转发 ,那改如何获得目标ip对应的mac地址呢?

我们先查看本地主机arp缓存表,查找这个ip地址对应的mac地址,如果缓存表中没有对应的记录,我们就需要进行arp广播

我们将广播mac地址指定为目标mac地址,就得到一个arp广播帧

如果目标ip地址本地网络目标ip地址对应的主机在收到arp广播帧后会返回自己的MAC地址(单播),然后PC0数据包封装成,通过交换机转发给这台主机即可。

如果目标ip地址不在本地网络,广播arp请求则无响应,此时修改广播帧的目标ip为默认网关ip,再发送一次arp广播

默认网关即路由器收到arp广播帧后,发现是在询问自己的MAC地址(目标ip=默认网关ip),并记住客户端MAC地址客户端ip地址的对应关系(存储在arp缓存),并返回自己的MAC地址(单播),然后主机PC0就能将数据封装成,转发给默认网关

要注意的是,PCO转发给默认网关的帧的目标IP地址,就是真正的目标ip地址,比如B站服务器的IP地址

默认网关,即路由器在接收到转发的,确认是发送给自己的帧后,解封,替换中的源ip地址(私有ip地址)为路由器对应的公有ip地址,路由器还会给这个私有ip地址分配一个对外端口号,我们把用这个端口号替换源端口号,并记录对应关系(NAT);

有了对外端口号,就能实现公有IP地址私有ip地址1对多映射,有人肯能会问,为什么还要替换端口号呢?直接有原来的端口号来标识不同的私有ip不行吗?确实不行,因为不同的私有ip(不同的主机),可以使用相同的端口号。

路由器再根据包里的目标ip地址进行路由转发服务器所在的默认网关如果知道这个目标ip地址对应的是那台服务器(知道目标ip地址对应的服务器mac地址),则把数据包封装成,经交换机转发给对应的服务器,如果不知道则进行arp广播

对应的服务器收到后进行逐层解封,根据目标MAC地址确认这个帧是发给自己的,根据目标ip地址确认这个包是发给自己的,根据目标端口确定要与哪个应用程序交互,再把报文交给这个应用程序处理。

详细参考:互联网数据传输原理 |OSI七层网络参考模型_哔哩哔哩_bilibili

如何理解TCP/IP协议

TCP/IP协议不仅仅指的是TCPIP两个协议,而是指一个由FTPSMTPTCPUDPIP等协议构成的协议簇

只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以通称为TCP/IP协议簇。

如何理解UDP 和 TCP?

TCP

TCP(Transmission Control Protocol),传输控制协议,是一种可靠、面向字节流的通信协议,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段后交给网络层传输。

UDP

UDP(User Datagram Protocol),用户数据报协议,是一个简单的面向数据报的通信协议。即对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层。

区别与联系

  • 添加端口号

    都是工作在传输层的协议,都能给报文添加端口号(源端口,目标端口),把报文封装成

  • 首部开销

    TCP报文段首部内容更多,首部更大,UDP报文段首部开销小。

  • 报文分段

    如果报文很大,TCP会将报文分段,并给这些报文段编号。而UDP则始终把应用层的报文当做一个整体,不进行分片,直接封装。

  • 连接建立

    TCP是面向连接的,即发送数据之前需要建立连接,保证数据可靠性,而UDP是无连接的,直接发送数据,不保证数据可靠性

  • 流量与拥塞控制

    TCP具有流量控制拥塞控制,而UDP没有。

总结

TCP 应用场景适用于对效率要求低对准确性要求高或者要求有连接的场景,而UDP 适用场景为对效率要求高,对准确性要求低的场景,各有优缺。

关于TCP和UDP的具体介绍,参考本博客内的《计算机网络》一文

TCP连接如何建立,如何断开

建立tcp连接的建立与断开这一过程可以简记为:三次握手,四次挥手

三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包

主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备

过程如下:

  • 第一次握手:客户端给服务端发一个 SYN(同步)报文,并指明客户端的初始化序列号seq,此时客户端处于 SYN_SENT 状态
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,为了确认客户端的 SYN,将客户端的 seq+1作为ack的值,此时服务器处于SYN_RCVD的状态
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的seq+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于ESTABLISHED状态,此时,双方已建立起了连接。

为什么不是二次握手

  • 如果2次握手就建立连接,假如因为网络问题,客户端发送了不止一次SYN包,那么就会建立多个连接,占用不必要的资源。

  • 假如客户端发送一个SYN请求,但是由于网络超时,客户端放弃了建立连接,不再等待服务端响应,如果此时使用两次握手建立连接,服务器接受到超时的同步请求后就错误的打开了一个连接,而这个连接是客户端不需要的。

  • 如果只进行2次握手,服务端无法确保客户端的接收能力正常,因为服务端的请求得不到响应

三次握手就好比一个生活中的情景对话:

C:我要建立同步连接

S:你真的要建立同步连接吗?

C:是的我要建立同步连接

这样增加一次握手就是增加了一次确认的过程,确保本次连接确实是被需要的。

四次挥手

IP

什么是IP

IP(Internet Protocol),也叫互联网协议。它定义了如何将数据分割成数据包、地址编码、路由选择以及如何重新组装这些数据包以恢复原始信息。IP 提供的是无连接的服务,这意味着每个数据包独立处理,不需要事先建立专用的通信通道。这也意味着 IP 不保证数据包按顺序到达或不会丢失。

什么是IP地址

IP地址是IP (Internet Protocol,互联网协议)为每个连接到网络的设备分配的一个逻辑地址,用来唯一标识每一台设备,这使得设备之间能够互相识别并进行通信。IP地址的格式可分为ipv4(32位),ipv6(128位 )。

IPv4格式IP地址分类

ipv4格式的IP地址被分为私有IP地址公有IP地址,私有ip地址的出现,主要是为了解决ipv4格式的IP地址数量不足的问题。

私有ip地址只能在局域网中通信,而公有ip地址才能在互联网通信,一个公有ip地址常常对应多个私有ip地址

路由器内部的DHCP会自动为设备分配私有ip地址,而公有ip地址由运营商分配。

ipv4格式的地址另一种常见的分类方式是分为A,B,C四大类

  • A类

    对应的子网掩码为255.0.0.0,即前8位用来表示网络号,后24位用来表示主机号,前一位比特必须是0(限定前几位比特是为了能快速识别是哪类IP地址),表示 IP 地址范围为 0.0.0.0127.255.255.255

  • B类

    对应的子网掩码为255.255.0.0,即前16位用来表示网络号,后16位用来表示主机号,前两位比特必须是10,表示 IP 地址范围为 128.0.0.0191.255.255.255

  • C类

    对应的子网掩码为255.255.255.0,即前24位用来表示网络号,后8位用来表示主机号,前三位比特必须是110,最为常见,表示 IP 地址范围为 192.0.0.0223.255.255.255

  • 特殊地址

    • 127.0.0.0

      对任何ip地址的访问都会访问这个ip地址

    • 广播地址

      主机号全为1的地址。

    • 网络地址

      主机号全为0的地址,不能用来标识某一台主机。

DNS协议

DNS(Domain Names System),域名系统,是互联网一项服务,是把域名转换成对应的IP地址的服务器。

什么是域名

域名可以理解为给ip地址起的别名,方便记忆。域名一般由三部分构成,由.连接,从右到左分别是顶级域名,权威域名,本地域名。

域名查询方式

  • 递归查询

    如果 A 请求 B,那么 B 作为请求的接收者一定要给 A 想要的答案,可以理解为帮人帮到底,但是这样对B服务器(域名服务器)的压力就很大,域名服务器不返回下级域名服务器ip地址,而是负责的帮忙询问,只返回普通服务器ip地址。

  • 迭代查询

    如果接收者 B 没有请求者 A 所需要的准确内容,接收者 B 将告诉请求者 A,如何去获得这个内容,但是自己并不去发出请求。

域名缓存

计算机中DNS的记录分成了两种缓存方式:

  • 浏览器缓存:浏览器在获取网站域名的实际 IP 地址后会对其进行缓存,减少网络请求的损耗。
  • 操作系统缓存:操作系统的缓存其实是用户自己配置hosts 文件

DNS解析过程

  • 当我们访问一个网站,就需要通过DNS解析获取它的ip地址,首先搜索浏览器的 DNS 缓存,缓存中维护一张域名IP 地址的对应表
  • 若缓存没有命中,则继续搜索操作系统的 DNS 缓存
  • 若操作系统缓存仍然没有命中,操作系统将向本地域名服务器(默认域名服务器)发送一次DNS查询请求,询问这个域名的ip地址,本地域名服务器采用递归查询自己的 DNS 缓存(因为域名是多级结构嘛),查找成功则返回结果。
  • 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询
    • 首先本地域名服务器向根域名服务器发起请求,询问根域名服务器,返回顶级域名服务器ip地址到本地服务器
    • 本地域名服务器拿到这个顶级域名服务器的ip地址后,就向其发起请求,返回权威域名服务器ip地址
    • 本地域名服务器根据权威域名服务器的ip地址向其发起请求,最终得到该域名对应的 IP 地址
  • 本地域名服务器将得到的 IP 地址返回给操作系统,同时自身将 IP 地址缓存
  • 操作系统将 IP 地址返回给浏览器,同时自身将 IP 地址缓存
  • 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存

域名解析参考:【实操演示】域名DNS解析设置 | 第一次设置域名解析?看这个就明白了 | 什么是域名解析 | 如何设置_哔哩哔哩_bilibili

如何理解CDN

CDN也叫做内容分发网络,通过在多地设置CDN服务器,并存储源服务器文件副本,并把请求路由到最近或者最合适的服务器,从而起到减少网络时延的作用,特点是就近取材,内容缓存

除了减少网络时延,提高网页的加载速度,CDN技术还能解决源服务器宕机导致的服务瘫痪。

CDN在域名解析过程中起作用,配置了cdn加速的域名,解析后返回的不再是固定的源服务器ip,而是距离请求设备最近的或者最合适的cdn服务器的ip。cdn服务器的选择工作由CDN专用DNS服务器实现。

接入CDN加速后的域名解析流程,拿www.sanye.blog这个域名举例:

  • 先查询本地浏览器DNS缓存,是否有www.sanye.blog这个域名对应的ip地址,如果没有,再查询操作系统DNS缓存
  • 如果操作系统DNS缓存命中,则把对应的ip地址返回给浏览器,否则操作系统向本地dns服务器发送一次dns查询请求。
  • 本地dns服务器递归查询本地记录,如果查找到对应的ip地址则返回给操作系统,否则询问上级服务器
  • 本地dns服务器进行一系列迭代查询,最终询问sanye.blog权威域名服务器,并获得一条CNAME记录。
  • 这条CNAME记录表明www.sanye.blog指向一个cdn加速域名
  • 本地DNS服务器自动继续解析这个加速域名,这个解析过程涉及到了cdn专用域名服务器,最终得到一台最合适的cdn服务器的ip地址,返回给操作系统。

cdn专用域名服务器的工作流程:

  • 查看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点
  • 看用户所在的运营商网络,找相同网络的边缘节点
  • 检查边缘节点的负载情况,找负载较轻的节点
  • 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等

结合上面的因素,得到最合适的cdn服务器(边缘结点),然后把这个节点的ip地址返回给本地dns服务器,本地dns服务器将这个ip地址返回给操作系统,操作系统则把这个ip返回给浏览器,最终用户就能够就近访问CDN的缓存代理。

其中有两个衡量CDN服务质量的指标:

  • 命中率:用户访问的资源恰好在缓存系统里,可以直接返回给用户,命中次数与所有访问次数之比
  • 回源率:缓存里没有,必须用代理的方式回源站取,回源次数与所有访问次数之比。必须指定cdn服务器的回源地址,也就是源服务器地址,不然cdn服务器都不知道去哪儿拷贝资源。

命中率 + 回源率 = 1,命中率越高,表明加速效果越好。

CDN原理具体参考:【白话科普】用动画告诉你 CDN是如何工作的 | CDN是什么 | 如何让你的网站网站快速打开 | CDN原理 | 服务器自由_哔哩哔哩_bilibili

地址栏输入 URL 敲下回车后发生了什么

所有步骤如下:

  • URL解析
  • DNS 查询
  • 建立TCP 连接
  • 发送HTTP 请求
  • 响应请求
  • 页面渲染

URL解析

判断输入的url是否合法,不合法则根据关键字进行搜索,合法则对这个URL进行结构分析

一个合法的URL包括协议域名/ip端口号(默认为80或者443),资源路径查询参数

DNS查询

通过DNS查询获得域名部分对应的ip地址

在之前文章中讲过DNS的查询,这里就不再讲述了。

建立TCP连接

然后再根据得到的ip地址,通过三次握手,与目标服务器建立TCP连接。

在之前文章中讲过TCP连接的建立,这里就不再讲述了

发送http请求

连接建立后,浏览器发送http请求报文

http请求报文结构从上至下依次是:

  • 请求行

    包括请求方法资源路径协议以及协议版本号

  • 请求头

    http请求常见请求头再前面有介绍。

  • 请求体

    用来存放请求携带的数据

响应请求

当服务器接收到浏览器的请求之后,就会对请求进行处理,执行一些逻辑操作,并返回一个http响应报文,在响应体中携带一个html文件。

http响应报文结构从上至下依次是:

  • 响应行
  • 响应头
  • 响应体

结构其实和请求报文结构很类似,就不举例说明了。

在服务器响应之后,由于现在http默认开始长连接keep-alive,一般只有当页面关闭之后,tcp连接才会经过四次挥手完成断开。

页面渲染

当浏览器接收响应报文后,会对这个报文进行解析:

  • 查看响应头的信息,根据不同的指示做对应处理,比如重定向,存储cookie,解压gzip,缓存资源等等
  • 特别是查看响应头的 Content-Type的值,根据不同的资源类型采用不同的解析方式。
  • 查看响应体,发现返回的是一个html文件,便开始进行页面渲染。

关于页面的渲染过程如下:

  • 解析HTML,构建 DOM 树
  • 解析 CSS ,生成 CSS 规则树
  • 虽然DOM树和CSSOM树能够并行构建,但两者都需要准备就绪后才能生成最终的渲染树。了解这一点有助于更好地设计网站结构和优化加载性能。
  • 合并 DOM 树和 CSS 规则,生成 render 树
  • 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
  • 绘制 render 树( paint ),绘制页面像素信息
  • 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上

说说你对websocket的理解

是什么

WebSocket,是一种网络传输协议,位于OSI模型的应用层。可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅

协议名

引入wswss分别代表明文和密文的websocket协议,且默认端口使用80或443,几乎与http一致

1
2
3
ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx

握手

类似HTTP请求建立TCP连接需要握手,WebSocket也要有一个握手过程,然后才能正式收发数据。

与HTTP的区别与联系

联系

  • 和http协议一样,都是一种网络传输协议,都工作在应用层,收发数据都基于tcp连接

区别

  • 更强的实时性(全双工)

    相对于HTTP请求需要等待客户端发起请求服务端才能响应(半双工),延迟明显更少。

  • 保持连接状态(有状态)

    创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证,也可以说,websocket是有状态的。

  • 较少的控制开销

    数据包头部协议较小,不同于http每次请求需要携带完整的头部。

使用方式

服务端

安装

1
npm i ws

创建一个websocket服务

1
2
3
const wss = new ws.Server({port:8080},()=>{
console.log('开启服务成功')
})

监听客户端连接

1
2
3
wss.on('connection',(s)=>{
console.log('用户连接了')
})

监听用户发送消息

1
2
3
4
5
6
7
8
9
wss.on('connection',(socket)=>{
console.log('用户连接成功')
//注意这里使用的不是wss
//socket可以理解为某个用户对象
socket.on('message',(e)=>{
console.log(e.toString())
socket.send('这是服务端发送的数据')
})
})

实现广播

1
2
3
4
5
socket.on('message',(s)=>{
wss.clients.forEach(client=>{
client.send(e.toString())
})
})

监听用户断开连接

1
2
3
socket.on('close', () => {  
console.log('Client disconnected');
});

检测websocket状态

1
2
3
4
5
6
7
8
9
let timer  = null
timer = setInterval(()=>{
//检测每个用户的连接状态
wss.clients.forEach(client=>{
if(client.readyState === ws.OPEN){
client.send('连接正常')
}
})
},5000)

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const ws = require('ws')
//创建一个websocket服务
const wss = new ws.Server({port:8080},()=>{
console.log('开启服务成功')
})
//连接的用户数
let num = 0
//监听客户端连接
wss.on('connection',(socket)=>{
//socket可以理解为与服务端建立连接的客户端对象,客户端建立连接的时候,拿到客户端对象
//用户数加1
console.log('在线用户数:',++num)
//给连接到服务器的客户端添加消息监听
socket.on('message',(e)=>{
//wss.clients可以理解为所有与服务端连接的socket数组,任意用户收到消息,广播给全部用户
wss.clients.forEach(client=>{
//client的类型和socket差不多吧
client.send("这是来自服务端的消息:"+e.toString())
})
})
//给连接到服务器的客户端添加连接关闭的监听
socket.on('close', () => {
console.log('Client disconnected','在线用户数:',--num);
});
})
let timer = null
timer = setInterval(()=>{
//检测每个用户的连接状态
wss.clients.forEach(client=>{
if(client.readyState === ws.OPEN){
client.send('连接正常')
}
})
},5000)

客户端

使用内置的WebSocket即可

创建ws对象并建立连接

1
const ws = new WebSocket("ws://localhost:8080")//传入url,与指定的服务器建立连接

监听连接建立

1
2
3
ws.addEventListener('open',(e)=>{
console.log("客户端连接成功")
})

监听服务端传来的消息

1
2
3
ws.addEventListener('message',(e)=>{
console.log(e.data)
})

向服务器发送消息

1
2
3
function emit(){
ws.send('我是tom')
}

主动断开连接

1
ws.close()

之后wss.clients数组的长度会-1

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<button onclick="emit()">send</button>
<button onclick="ws.close()">close</button>
<input type="text">
</body>
<script>
const ws = new WebSocket("ws://localhost:8080")
ws.addEventListener('open',(e)=>{
console.log("客户端连接服务器成功")
})
//监听服务端传来的消息
ws.addEventListener('message',(e)=>{
//e的数据类型是一个对象
console.log(e.data)
})
//向服务器发送消息
const input = document.querySelector('input')
function emit(){
ws.send(input.value)
}
</script>

小结

  • 客户端创建ws对象使用的是js内置WebSocket对象,服务端使用的是npm下载的node
  • 客户端监听使用addEventListener,服务端监听使用on
  • 双端发送消息使用的都是send,接受消息的事件名都是message,不过传入回调函数的数据格式不同。

应用场景

  • 弹幕
  • 媒体聊天
  • 协同编辑
  • 基于位置的应用
  • 体育实况更新
  • 股票基金报价实时更新

跨站和跨域有什么区别?什么是主域名,子域名?

添加这部分内容主要是为了后续理解cookie和localStorage

浏览器通过 eTLD+1 判断是否属于同站(Same-Site)请求,防止跨站攻击。其中eTLD( Effective Top-Level Domain),意为有效顶级域名

  • www.bilibili.comgame.bilibili.com同站(共享 eTLD+1,即 bilibili.com)。
  • a.github.iob.github.io跨站(因 github.io 是公共后缀,视为不同站点)。

我们可能会问,com是有效顶级域名,允许用户直接注册二级域名(如 example.com),但这里的io为什么就不是有效顶级域名,github.io要被视为公共后缀(也就是特殊的顶级域名)。因为这类特殊的顶级域名,不允许用户直接注册二级域名,通常由服务商统一管理子域,防止恶意网站通过子域共享 Cookie 或凭据(如 user1.github.iouser2.github.io 应视为不同站点)

主域名通常就是eTLD+1,域名的其他部分就是子域名,因此我们可以说,简单的来说,主域名相同就是同站,否则就是跨站。

完整域名eTLD(有效顶级域名)eTLD+1(主域名)
www.bilibili.com.combilibili.com
user.github.iogithub.io(公共后缀)user.github.io
shop.co.uk.co.ukshop.co.uk

跨域就是浏览器基于同源策略,推出的安全手段,请求的域名,协议,端口号中有一项和当前页面的不同,就是跨域请求,跨域不一定跨站(比如只是协议不同),但是跨站就一定跨域(因为跨站域名一定不同)。

其他

这部分我看到的比较有意思的面试题,或者自己想出的问题,不过面试问的问题都是基于项目出发的,懂的都懂。

说说二维码登录流程

  • 用户在网页上选择二维码登录选项。

  • 网页携带设备信息(如浏览器类型、操作系统等)向服务器请求生成用于登录的二维码;服务端生成一个二维码id,并将这个id与请求中携带的设备信息绑定,存储在服务器数据库中,把二维码id返回给网页。

  • 网页获取二维码id后,展示二维码,并轮询二维码状态(设置定时器,每个一段时间发送一个ajax请求询问二维码状态)。

  • 用户用移动设备扫描二维码,获取到二维码id,移动设备将账号信息连同二维码id发送给服务器

  • 服务端收到信息后会将账号信息二维码id绑定,并返回一个临时token;此时移动端会提示是否登录,pc端二维码状态变为已扫描

  • 移动端确认登录,会将临时token发送到服务器,服务器收到token后会根据二维码id绑定的设备信息用户信息生成一个用于pc端登录的token,并在pc端轮询二维码状态的时候返回给pc端

说说用户登录流程

  • 用户输入账号密码,前端先校验账号密码的格式是否正确,然后把账号密码连同设备信息(如浏览器类型、操作系统、设备标识符等)发送给后端对应接口
  • 后端校验账号密码是否正确,如果校验通过(正确),把设备信息账号绑定,并返回一个根据用户id设备信息加密后生成的token字符串
  • 前端接收到返回的token后把它存储在localStorage
  • 后续前端的每次请求都会携带token设备信息
  • 后端会校验请求中的token是否有效,是否与携带的设备信息匹配,不匹配或者token无效则请求失败;服务器应该返回相应的错误信息,并要求用户重新登录。

说说withCredentials

withCredentialsXMLHttpRequest中的一个属性,用于控制是否在跨域请求中发送凭据(如 cookies、HTTP 认证信息等)。

1
2
3
4
// 原生XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 启用凭据
1
2
3
4
5
// Fetch API
fetch(url, { credentials: 'include' });
// Axios
//这个配置项说明,withCredentials不是请求头,而是和headers同一级别的属性
axios.get(url, { withCredentials: true });

当浏览器检测到跨域请求需要携带凭证(如 withCredentials: true)时,会触发以下机制

  • 此次跨域请求会携带凭证

  • 但响应头中必须同时满足

    1
    2
    Access-Control-Allow-Origin: https://your-domain.com  // 明确指定域名,禁止使用 *
    Access-Control-Allow-Credentials: true
  • 如果响应中未正确返回上述字段,或 Access-Control-Allow-Origin 使用了通配符 *,那么浏览器会拦截此次携带凭据的跨域请求的响应。

简单的来说,就是需要前后端协调配合,才能实现跨域携带cookie。

默认情况下,cookies 只会在同源请求中自动发送。这意味着如果请求是从同一协议、主机名和端口的页面发出的,则会包含相关 cookies。

举个例子:

如果当前页面是 www.bilibili.com,并且你发送一个请求,请求的域名是 www.bilibili.com,因为这个请求没有跨域,那么无论是设置为 Domain=www.bilibili.com 的 Cookie 还是设置为 Domain=.bilibili.com 的 Cookie 都会被自动携带。

但当你在 https://www.bilibili.com 页面下,发送请求 https://game.bilibili.com 时,这是一个跨域请求,但是没跨站Domain=www.bilibili.com 的 Cookie不会被自动携带,因为它们仅限于 www.bilibili.comDomain=.bilibili.com 的 Cookie可能会被携带,但是需要满足以下条件:

  • 客户端在请求中明确设置了 withCredentials: true
  • 服务器在响应中,正确配置了 CORS 头,允许凭据传输。

如果withCredentials的值为false(就是没有配置或者配置为false),那即便Domain=.bilibili.com的cookie能在game.bilibili.com下生效,也不会被携带。

如果发送请求,请求的域名是www.sanye.blog,很明显这是一个跨站请求(关于跨站和跨域的区别,参考前文),同时也是一个跨域请求,此时如果我们想要这个请求能携带cookie,就必须在请求的时候设置withCredentials = true,然后服务器还需要响应正确的cors头,否则此次请求的响应会被浏览器拦截。要注意的是,并不是所有Domain = www.sanye.blog的cookie都会被携带,只有SameSite的值为None 的cookie才会被携带,因为此次请求还是跨站的。

如何实现控制文件点击下载或者预览

前端

不添加download属性,默认点击链接预览图片

通过a标签的download属性强制下载,但是必须要求网页以http/https协议(网络协议)加载,不能是直接打开的网页。

1
<a href="./violet.png" download="薇尔莉特">点击下载</a>

给download赋值可以指定下载时,使用的文件名。如果省略此属性,则浏览器会尝试自动解析URL中的文件名(寻找默认文件名)

后端

可以在响应头中通过设置Content-disposition来决定是下载还是预览,因为点击a链接会送一个获取图片(文件)的请求。

  • inline

    指示浏览器直接显示内容(如在浏览器窗口或新标签页中打开),这是默认行为。

  • attachment:指示浏览器将内容作为附件下载,同时还可以通过filename指定下载名称

1
2
Content-Disposition: inline;
Content-Disposition: attachment; filename="example.pdf";

如何让图片复制后的链接失效

  • 在链接中添加token,让这个链接变成临时链接
  • 使用URL.createObjectURL创建的临时url来展示图片

host referer origin 这三个请求字段有什么区别

Host

  • 作用Host 头部是HTTP/1.1协议中必须包含的一个请求头,用于指定请求的目标主机名和端口号

  • 格式Host: example.com[:port]

  • 用途

    • 它帮助服务器识别客户端想要访问的具体主机,这对于虚拟主机(即在同一IP地址上托管多个域名)非常重要,典型的例子就是github.iogithub pages都被托管到4个固定的ip,但是却对应多个域名,比如域名syhy.github.iosanye.github.io对应的其实可能是同一个ip地址,但是服务器可以根据请求中的host,即syhy.github.iosanye.github.io,判断出一个是要访问用户syhy的pages,一个是想要访问sanye的pages。
  • 如果URL使用了默认端口(HTTP为80,HTTPS为443),则可以省略端口号。

示例:

1
Host: www.example.com

值得注意的是,一个页面的location对象中也有host属性,不过这个指的是当前页面的域名+端口号

Referer

  • 作用Referer 头部包含了发起当前请求的页面的URL。它主要用于追踪用户是从哪个页面跳转过来的,有助于分析流量来源以及实现防盗链等功能。

  • 注意拼写错误:虽然正确的英文单词应该是“Referrer”,但在HTTP协议中该头部被误写作“Referer”。

  • 格式Referer: http://example.com/path

  • 用途:

    • 网站可以通过这个信息来了解用户是从哪里链接过来的。
  • 常用于统计分析、反盗链措施等场景。

示例

1
Referer: https://www.example.com/previous-page

Origin

  • 作用Origin 头部用于标识发起请求的源站点(scheme, host, port)。它主要应用于跨域资源共享(CORS)请求中,用来告知服务器请求的来源,以便决定是否允许该请求。

  • 格式Origin: scheme://host[:port]

  • 用途:

    • 跨域请求时,浏览器会自动添加此头部以帮助服务器判断是否接受来自不同源的请求。
  • Referer不同的是,Origin仅包含协议、主机名和端口号,而不包括具体的资源路径

示例

1
Origin: https://www.example.com

总结

  • **Host**:指定请求的目标主机和端口,是HTTP请求的基本组成部分(域名+端口号)
  • **Referer**:提供发起请求的当前页面的完整URL,主要用于跟踪用户行为或实施安全策略(协议+域名+端口号+资源路径)。
  • **Origin**:标识请求发起者的源(协议+主机+端口),主要用于跨域请求的安全控制(协议+域名+端口号)

比如我在https://www.example.com/post/index.html发送一个请求https://www.sanye.blog,这个请求的host就是www.sanye.blog,而这个请求的referer就是https://www.example.com/post/index.html,这个请求的origin就是https://www.example.com