❝当我们面对失败的时候,最让我们难以接受的,不是挫败感,而是大众的否定和轻视 ❞
大家好,我是「柒八九」。
今天,我们继续「前端面试」的知识点。我们来谈谈关于「网络通信」的相关知识点和具体的算法。
该系列的文章,大部分都是前面文章的知识点汇总,如果想具体了解相关内容,请移步相关系列,进行探讨。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
好了,天不早了,干点正事哇。
大致经历了5个组件或者步骤:
通信模型
把链路层和物理层都表示为网络接口层
在五层协议之上加了表示层和会话层
❝「URL」:统一资源定位符Uniform Resource Locator,是互联网上标准资源的地址。 ❞
互联网上的「每个文件都有唯一的一个的URL」,
基本URL包含:模式(或称协议)、服务器名称(或IP地址/网址)、路径和文件名。
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
❝浏览器是一个具备「多种客户端功能」的综合性客户端软件 ❞
浏览器要做的第一步工作就是对 URL 进行解析,从而生成发送给 Web服务器的「请求消息」。
解析过程
HTTP 的基本思路
❝HTTP 协议定义了「客户端和服务器之间交互的消息内容和步骤」 ❞
❝请求消息中包含的内容是「对什么」和「进行怎样的操作」两个部分 ❞
其中相当于“对什么”的部分称为 「URI」。一般来说,URI 的内容是一个存放网页数据的「文件名」,例如/dir1/file1.html
。
❝URI:统一资源标识符Uniform Resource Identifier ❞
把“进行怎样的操作”的部分称为「方法」。
GET
和 POST
比较
对 URL 进行解析之后,浏览器确定了 Web 服务器和文件名,接下来就是根据这些信息来生成 HTTP
请求消息了。
请求消息格式
/< 目录名 >/…/< 文件名 >
<方法> <URI> <HTTP版本>
URI
部分的格式如下,一般是文件和程序的「路径名」。响应消息格式
在响应消息中,第一行的内容为「状态码」和「响应短语」,用来表示请求的执行结果是成功还是出错。
返回响应消息之后,浏览器会将「数据类型」 (Content-Type
)提取出来并显示在屏幕上。
客户端(浏览器)生成 HTTP 消息之后,接下来需要委托「操作系统」将消息发送给 Web 服务器。在委托操作系统发送消息时,必须要提供的不是通信对象的域名,而是它的 「IP 地址」。
❝IP 地址是一个「网卡」在网络世界的通讯地址,相当于我们现实世界的门牌号码 ❞
MAC 地址,是一个「网卡的物理地址」,用十六进制,6 个 byte
表示。
MAC 地址全局唯一,「不会有两个网卡有相同的 MAC 地址」,而且网卡自生产出来,就带着这个地址。
很多人看到这里就会想,只要知道了对方的 MAC 地址,就可以把信息传过去。这是行不通的。「一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能」。 而有门牌号码属性的 IP 地址,才是有远程定位功能的。
❝「MAC 地址更像是身份证,是一个唯一的标识」。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。「从硬件角度,保证不同的网卡有不同的标识」。 ❞
MAC 地址是有一定定位功能的,只不过范围非常有限。MAC 地址的通信范围比较小,「局限在一个子网里面」
❝只要是在网络上跑的包,都是完整的,可以有下层没上层,「绝对不可能有上层没下层」。 ❞
假设,我们想要在源 IP 地址为 16.158.23.6
的机器上访问目标 IP 地址 为 192.168.1.6
机器的数据。发现包发不出去,这是因为 MAC 层还没填。
ARP
请求,获取 MAC
地址。Linux
「默认的逻辑」是,如果这是一个跨网段的调用,它便不会直接将包发送到网络上,而是企图「将包发送到网关」。Linux
会获取网关的 MAC
地址,然后将包发出去。手动配置需要不停的对自己机器的IP进行配置。是一件很麻烦的事。
需要有一个「自动配置」的协议,也就是称动态主机配置协议Dynamic Host Configuration Protocol,简称DHCP。
只需要配置一段「共享的 IP 地址」。每一台新接入的机器都通过 DHCP 协议,来这个共享的 IP 地址里申请,然后自动配置好就可以了。等人走了,或者用完了,还回去,这样其他的机器也能用。
向 DNS 服务器发出查询,并接收服务器返回的响应消息。对于 DNS 服务器,我们的计算机上一定有相应的 「DNS 客户端」,而相当于 DNS 客户端的部分称为 DNS 解析器
,或者简称「解析器」。
❝通过 DNS 查询 IP 地址的操作称为「域名解析」 ❞
解析器实际上是一段程序,它包含在操作系统的 Socket
库中。
解析器的调用方法
调用解析器后,解析器会向 DNS 服务器发送查询消息,然后 DNS 服务器会返回响应消息。响应消息中包含查询到的 IP 地址,解析器会取出 IP 地址,并将其写入浏览器指定的「内存地址」中。
接下来,浏览器在向 Web 服务器发送消息时,只要从该内存地址取出 IP 地址,并将它与 HTTP 请求消息一起交给操作系统就可以了。
❝在 Socket 库中,采用
UDP
协议,进行信息的查询。 ❞
DNS 服务器的基本工作就是「接收来自客户端的查询消息」,然后「根据消息的内容返回响应」。
来自客户端的查询消息包含以下 「3 种信息」。
类型 | 描述 |
---|---|
「域名」 | 服务器、邮件服务器(邮件地址中 @ 后面的部分)的名称 |
「Class」 | Class 的值「永远」是代表互联网的 「IN」 |
「记录类型」 | 表示域名对应「何种类型」的记录 类型为 「A」 时:表示域名对应的是 「IP 地址」 类型为 「MX」 时:表示域名对应的是邮件服务器 |
「A 是 Address 的缩写」 /「MX:Mail eXchange,邮件交换的缩写」
DNS 服务器上「事先保存」有前面这 3 种信息对应的记录数据。
DNS 服务器的基本工作
例如,如果要查询 www.wl.com
这个域名对应的 IP 地址,客 户端会向 DNS 服务器发送包含以下信息的查询消息。
信息 |
---|
域名 = www.wl.com |
Class = IN |
记录类型 = A |
然后,DNS 服务器会从「已有的记录」中查找域名、Class 和记录类型「全部匹配」的记录。
❝DNS 服务器会从域名与 IP 地址的对照表中查找相应的记录,并返回 IP 地址 ❞
实际上还有很多其他的类型。
PTR
类型CNAME
类型NS
类型SOA
类型等首先,DNS 服务器中的所有信息都是「按照域名以分层次的结构」来保存的。DNS 中的域名都是用「句点来分隔」的,比如 www.wl.com
,这里的句点代表了不同层次之间的界限。
在域名中,「越靠右的位置表示其层级越高」。
这种「具有层次结构」的域名信息会注册到 DNS 服务器中,而「每个域」都是作为「一个整体」来处理的。
于是,「DNS 服务器也具有了像域名一样的层次结构」,每个域的信息都存放在相应层级的 DNS 服务器中。
这里的关键在于「如何找到我们要访问的 Web 服务器的信息归哪一台 DNS 服务器管」。
com
、cn
这些域(称为顶级域),它们「各自负责」保存下级 DNS 服务器的信息。在互联网中,com
和 cn
的上面还有一级域,称为「根域」。根域不像 com、cn 那样有自己的名字,因此在一般书写域名时经常被省略,如果要明确表示根域,应该像 www.wl.com.
这样在域名的「最后再加上一个句点」,而这个最后的句点就代表根域。
树状的层次结构
❝根域的 DNS 服务器中保管着
com
、cn
等的 DNS 服务器的信息 ❞
除此之外还需要完成另一项工作,那就是将根域的 DNS 服务器信息保存在互联网中「所有」的 DNS 服务器中。客户端只要能够找到任意一台DNS 服务器,就可以通过它找到根域 DNS 服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。
分配给根域 DNS 服务器的 IP 地址在全世界「仅有 13 个」 ,而且这些地址几乎不发生变化。
www.wl.com
的 IP 是啥啊,并发给本地域名服务器 (「本地 DNS」)。www.wl.com
,它直接就返回 IP 地址。.com
,说:“www.wl.com 啊,这个域名是由.com 区域管理,我给你它的「顶级域名服务器的地址」,你去问问它吧。”.com、.net、 .org
这些「一级域名」wl.com
,所以它能提供一条更清晰的方向如果要查询的域名和相关信息「已经在缓存」中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。
这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会「发生改变」,这时缓存中的信息就有可能是不正确的。因此,DNS 服务器中保存的信息都设置有一个「有效期」,当缓存中的信息超过有效期后,数据就会从缓存中删除。
例如,某个应用要访问另外一个应用,如果配置另外一个应用的 IP 地址,那么这个访问就是「一对一」的。但是当被访问的应用撑不住的时候,我们其实可以部署多个。但是,访问它的应用,如何在多个之间进行负载均衡?只要「配置成为域名」就可以了。在域名解析的时候,我们只要「配置策略」,这次返回第一个 IP,下次返回第二个 IP,就可以实现负载均衡了。
为了保证我们的应用「高可用」,往往会部署在多个机房,「每个地方都会有自己的 IP 地址」。当用户访问某个域名的时候,这个 IP 地址可以「轮询访问多个数据中心」。如果一个数据中心因为某种原因挂了,只要在 DNS 服务器里面,将这个数据中心对应的 IP 地址删除,就可以实现一定的高可用。
协议栈并「不关心」应用程序传来的数据是什么内容,在协议栈看来,要发送的数据就是「一定长度的二进制字节序列」而已。
协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的「发送缓冲区」中,并等待应用程序的下一段数据。
至于要积累多少数据才能发送,不能一概而论,但都是根据下面几个要素来判断的。
MTU
)的参数来进行判断。1500
字节。MSS
)。MTU 与 MSS
HTTP 请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能「超过一个网络包」所能容纳的数据量。
这种情况下,「发送缓冲区」中的数据就会「超过 MSS 的长度」,这时我们当然不需要继续等待后面的数据了。发送缓冲区中的数据会被「以 MSS 长度为单位进行拆分」,拆分出来的每块数据会被放进「单独」的网络包中。
根据发送缓冲区中的数据拆分的情况,当判断需要发送这些数据时,就在每一块数据前面「加上 TCP 头部」,并根据「套接字中记录」的控制信息标记发送方和接收方的「端口号」,然后交给 IP 模块来执行发送数据的操作。
序数据的拆分
自此,从客户端发送的消息,被封装成一个个网络包。然后,就会顺着通信通道进行传输。
TCP 头部信息
我们来简单介绍一下,重要字段的作用。
MSS
)就得要进行分段。Sequence Number
就是记录每个封包的序号,可以让 server 重新将 TCP 的数据组合起来。三次握手
x
,并发送一个 SYN
分组y
x + 1
x + 1
y + 1
三次握手完成后,客户端与服务器之间就可以通信了。客户端可以在发送 ACK 分组之后「立即发送数据」,而服务器必须等接收到 ACK 分组之后才能发送数据。
TCP 采用「滑动窗口」来管理数据发送和 ACK 号的操作。
❝所谓「滑动窗口」,就是在发送一个包之后,不等待 ACK 号返回,而是直接发送后续的一系列包 ❞
通过这种方式,就可以实现同一时间发送多个包,减少网络延迟。
其实,通过窗口,TCP 可以控制双向发送数据的「速度」。
❝流量控制是一种「预防发送端过多向接收端发送数据」的机制。 ❞
为实现流量控制,TCP 连接的每一方都要通告自己的「接收窗口」(rwnd
),其中包含能够保存数据的「缓冲区空间大小信息」。
「第一次」建立连接时,两端都会使用自身系统的「默认设置」来发送 rwnd
。在后面的数据交换过程中,「每个 ACK 分组」都会携带相应的「最新 rwnd
值」,以便两端「动态调整数据流速」,使之适应发送端和接收端的容量及处理能力。
流量控制确实可以「防止发送端向接收端过多发送数据」,但却没有机制预防「任何一端」向潜在网络「过多发送数据」。换句话说,发送端和接收端在「连接建立之初」,谁也不知道「可用带宽」是多少。因此需要一个「估算机制」,然后还要根据网络中不断变化的条件而「动态改变速度」。
「拥塞窗口大小」(cwnd
):发送端对从「客户端」接收确认(ACK)「之前」可以发送数据量的限制。发送端不会通告 cwnd
变量,即「发送端和接收端不会交换这个值」。
服务器和客户端怎么确定拥塞窗口大小的最优值呢:解决方案就是「慢启动」:即在分组被确认后增大窗口大小,「慢慢地启动」。
❝无论带宽多大,每个 TCP 连接都「必须经过慢启动阶段」 ❞
换句话说,应用「不可能一上来就完全利用连接的最大带宽」
❝把「初始拥塞窗口大小」增加到一个合理值,可以减少客户端与服务器之间的往返时间 ❞
❝TCP 在「不可靠的信道」上实现了「可靠的网络传输」 ❞
每个 TCP 分组都会带着一个「唯一的序列号」被发出,而所有分组「必须按顺序」传送到接收端。如果中途有一个分组没能到达接收端,那么后续分组必须「保存在接收端的 TCP 缓冲区,等待丢失的分组重发并到达接收端」。这「一切」都发生在 TCP 层,「应用程序」对 TCP 重发和缓冲区中排队的分组「一无所知」,必须等待分组全部到达才能访问数据。在此之前,应用程序只能在通过套接字读数据时感觉到「延迟交付」。这种效应称为TCP 队首阻塞Head of Line Blocking (HOL)
TCP 队首阻塞
队首阻塞造成的延迟可以让我们的应用程序不用关心「分组重排和重组」,分组到达时间会存在「无法预知的延迟变化」。这个时间变化通常被称为「抖动」,也是影响应用程序性能的一个主要因素。
❝「TCP 队首阻塞」造成的延迟,也是影响应用程序性能的一个主要因素 ❞
x
,并发送一个 FIN
分组x + 1
y
x + 1
y + 1
注意点:
SYN
换成 FIN
。FIN
包时候,此时可能正处于某些大包数据的发送阶段,如果此时「直接回复」 发送端的断开操作。并且,如果 server FIN 包早于其他正常数据包到达 client。那这些本应该被 client 收录的数据,就会「平白无故的丢失」。❝TCP协议是一种「面向连接」的、「可靠」的、「基于字节流」的传输层通信协议。 ❞
TCP是「全双工模式」,这就意味着
❝「HTTP缓存」是作用于网站「导航阶段」的网络请求的「开始阶段」
SericeWorker
之后Socket
进行 DNS
查询之前❞
在缓存中,存在两种比较常见的淘汰机制
❝「最好最快」的请求就是「没有请求」 ❞
浏览器对「静态资源」的缓存本质上是 HTTP
协议的缓存策略,其中又可以分为
❝两种缓存策略都会「将资源缓存到本地」 ❞
具体采用哪种缓存策略,由 HTTP 协议的首部( Headers
)信息决定。
我们对HTTP缓存用到的字段进行一次简单的分类和汇总。
头字段 | 所属分组 |
---|---|
Expires | 实体头 |
Cache-control | 通用头 |
ETag | 实体头 |
❝ETag:
Etag
向客户端发送一个唯一标识,在下次请求中客户端可以通过 If-Match
、If-None-Match
、If-Range
字段将这个标识告知服务器,这样服务器就知道该请求和上次的响应是相关的。
这个字段的「功能和 Cookie 是相同」的,但 Cookie 是网景(Netscape)公司自行开发的规格,而 Etag 是将其进行标准化后的规格❞
❝
Expires
和Cache-control:max-age=x
是「强制缓存」策略的关键信息,两者均是「响应首部信息」(后端返给客户端)的。 ❞
Expires
是 HTTP 1.0
加入的特性,通过指定一个「明确的时间点」作为缓存资源的过期时间,在此时间点之前客户端将使用本地缓存的文件应答请求,而不会向服务器发出实体请求。
Expires
的优点:
对应的语法
Expires: Wed, 24 Oct 2022 14:00:00 GMT
上述信息指定对应资源的「缓存过期时间」为 2022年8月24日 14点
❝
Expires
一个「致命的缺陷」是:
❞
如果客户端的时间与服务器存在「误差」,那么通过 Expires
控制的缓存资源将会「失效」,客户端将会发送实体请求获取对应资源。
针对这个问题, HTTP 1.1
新增了 Cache-control
首部信息以便「更精准」地控制缓存。
常用的 Cache-control
信息有以下几种。
no-cache
:no-cache
将会和服务器进行一次通讯no-store
no-store
要求资源每次都被请求并且下载下来public & private
public
表示此响应可以被浏览器以及中间缓存器「无限期缓存」private
表示此响应可以被用户浏览器缓存,但是「不允许任何中间缓存器」对其进行缓存。max-age=<seconds>
max-age=360
表示浏览器在接下来的 1 小时内使用此响应的本地缓存,不会发送实体请求到服务器no-transform
no-transform
指令告诉中间代理不要改变资源的格式❝
max-age
指定的是缓存的「时间跨度」,而非缓存失效的时间点,不会受到客户端与服务器时间误差的影响。 ❞
与 Expires
相比, max-age
可以「更精确地控制缓存」,并且比 Expires
有「更高的优先级」
强制缓存策略下( Cache-control
未指定 no-cache
和no-store
)的缓存判断流程
Etag
和 If-None-Match
(协商缓存)❝
Etag
是「服务器」为资源分配的字符串形式「唯一性标识」,作为「响应首部」信息返回给浏览器 ❞
「浏览器」
Cache-control
指定 no-cache
max-age
和 Expires
均过期之后,将Etag
值通过 If-None-Match
作为「请求首部」信息发送给服务器。
「服务器」接收到请求之后,对比所请求资源的 Etag
值是否改变,
304 Not Modified
,并且根据既定的缓存策略分配新的 Cache-control
信息;Etag
值。如果强制浏览器使用协商缓存策略,需要将 Cache-control
首部信息设置为 no-cache
,这样便不会判断 max-age
和 Expires
过期时间,从而「每次资源请求都会经过服务器对比」。
HTTP/0.9 是于 1991
年提出的,主要用于学术交流,需求很简单——用来在网络之间传递 HTML
超文本的内容,所以被称为「超文本传输协议」。
HTTP/0.9 的一个完整的请求流程
HTTP
都是基于 TCP
协议的,所以客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。GET
请求行的信息,如GET /index.html
用来获取 index.html。总的来说,当时的需求很简单,就是用来传输体积很小的 HTML 文件,所以 HTTP/0.9 的实现有以下三个特点。
HTTP 1.0 是在 1996
年引入的,由于在浏览器中展示的不单是 HTML 文件了,还包括了 JavaScript
、CSS
、图片、音频、视频等不同类型的文件。因此「支持多种类型的文件下载是 HTTP/1.0 的一个核心诉求」。
Key-Value
形式保存的header
中的 If-Modified-Since
和 Expires
作为缓存失效的标准。HTTP 1.1 是 HTTP 1.0 开发三年后出现的,也就是 1999
年,它做出了以下方面的变化
keep-alive
来设置E-tag
, If-Match
, If-None-Match
等缓存控制标头来控制缓存失效。Range
来实现。 xHTTP 2.0 是 2015
年开发出来的标准,HTTP/2
的一个核心特性是使用了「多路复用技术」,因此它可以通过一个 TCP 连接来发送多个 URL 请求。多路复用技术能充分利用带宽,最大限度规避了 TCP 的慢启动所带来的问题。
User-Agent
、Cookie
、Accept
、Server
、Range
等字段可能会占用几百甚至几千字节,而 Body
却经常只有几十字节,所以导致「头部偏重」。HPACK
算法进行压缩。HTTPS
上。HTTP/3 选择了一个折衷的方法——UDP
协议,基于 UDP
实现了类似于 「TCP 的多路数据流」、「传输可靠性」等功能,我们把这套功能称为 「QUIC 协议」。
TLS1.3
,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是「减少了握手所花费的 RTT 个数」。HTTPS
并不是一个新的应用层协议,它其实就是 HTTP + TLS/SSL
协议组合而成,而安全性的保证正是 TLS/SSL 所做的工作。
❝HTTPS 就是身披了一层
SSL
的HTTP
。
❞
探讨 HTTPS
的握手过程,其实就是 SSL/TLS
的握手过程。
TLS
旨在为 Internet
提供通信安全的加密协议。「TLS 握手是启动和使用 TLS 加密的通信会话的过程」。在 TLS 握手期间,Internet 中的通信双方会彼此交换信息,验证密码套件,交换会话密钥。
每当用户通过 HTTPS 导航到具体的网站并发送请求时,就会进行 TLS 握手。除此之外,每当其他任何通信使用HTTPS(包括 API 调用和在 HTTPS 上查询 DNS)时,也会发生 TLS 握手。
TLS 具体的握手过程会「根据所使用的密钥交换算法的类型和双方支持的密码套件而不同」。
client-random
+支持的「加密算法集合」加密算法
+server-random
+ 证书
client-random
+server-random
生成pre-master
通过服务器「公钥加密」 发送给serverUDP 的全称是 用户数据报协议User Datagram Protocol。它不需要所谓的握手操作,从而加快了通信速度,允许网络上的其他主机在接收方同意通信之前进行数据传输。
UDP
的特点主要有
DNS
查找,DNS 是建立在 UDP 之上的应用层协议。TCP 的全称是传输控制协议Transmission Control Protocol 。它能够帮助你确定计算机连接到 Internet 以及它们之间的数据传输。通过三次握手来建立 TCP 连接,三次握手就是用来启动和确认 TCP 连接的过程。一旦连接建立后,就可以发送数据了,当数据传输完成后,会通过关闭虚拟电路来断开连接。
TCP 的主要特点有
WebSocket
是一种网络传输协议,可在单个 TCP 连接上进行「全双工通信」,位于 OSI 模型的应用层。
WebSocket
使得客户端和服务器之间的数据交换变得更加简单,允许「服务端主动向客户端推送数据」。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
常见的Web攻击方式有(Scirpting
+ Request
+ SQL
)
跨站脚本攻击Cross Site Scripting,允许攻击者将恶意代码植入到提供给其它用户使用的页面中。
XSS涉及到三方
XSS
的攻击目标是为了「盗取」存储在客户端的cookie
或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
攻击者往Web页面里「插入恶意Script代码」,当用户浏览该页之时,嵌入其中Web里面的Script
代码会被执行,从而达到恶意攻击用户的目的。
XSS攻击的两大要素:
.innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等Vue/React
技术栈,并且不使用 v-html/dangerouslySetInnerHTML
功能,就在前端 render
阶段避免 innerHTML
、outerHTML
的 XSS 隐患location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免跨站请求伪造Cross-site Request Forgery:攻击者诱导受害者进入「第三方网站」,在第三方网站中,向被攻击网站发送跨站请求。
利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目。
CSRF通常从「第三方网站」发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。
Samesite Cookie
CSRF Token
Sql 注入攻击,是通过将恶意的 Sql查询或添加语句插入到应用的输入参数中,再在后台 Sql服务器上解析执行进行的攻击。
「分享是一种态度」。
参考资料:
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」