点击蓝字 「前端小苑」关注我
作者:三妹不是佩奇啊
https://juejin.im/post/5da16e9ef265da5b76373d0e
面试官:http/1.0和http/2.0区别是什么?
你:
1. 解决 HTTP 中的队头阻塞问题;
2. 二进制协议
3. http/2.0 的首部还会被深度压缩。这将显著减少传输中的冗余字节
4. 多路复用
你以为你备几条答案就完事了吗????,咱能不能有点出息,稍微详细一点可以不?又没让你现场调试http/2.0慌个毛线啊~????
老规矩啊,一下子没看懂的,或者脑袋放空不能吸收知识的时候,别来打我啊,请你收藏下来慢慢看,耐心!!!耐心!!!耐心!!!
三言两语http1.0和http/2.0
????http/0.9 :只支持get方法,不支持多媒体内容的 MIME 类型、各种 HTTP 首部,或者版本号,只是为了获取html对象。
????http/1.0 :添加了版本号、各种 HTTP 首部、一些额外的方法,以及对多媒体对象的处理。
????http/1.0+ :keep-alive 连接、虚拟主机支持,以及代理连接支持都被加入到 HTTP 之中等等。
????http/1.1: 重点关注的是校正 HTTP 设计中的结构性缺陷,明确语义,引入重要 的性能优化措施,并删除一些不好的特性:如Entity tag,If-Unmodified-Since, If-Match, If-None-Match;请求头引入了range头域,它允许只请求资源的某个部分(206);新增了更多的状态码;Host头处理(400)
????HTTP/2.0 被寄予了如下期望:
相比于使用 TCP 的 HTTP/1.1,最终用户可感知的多数延迟都有能够量化的显 著改善;
解决 HTTP 中的队头阻塞问题;
并行的实现机制不依赖与服务器建立多个连接,从而提升 TCP 连接的利用率,
特别是在拥塞控制方面;
保留 HTTP/1.1 的语义,可以利用已有的文档资源(如上所述),包括(但不限于)
HTTP 方法、状态码、URI 和首部字段;
明确定义 HTTP/2.0 和 HTTP/1.x 交互的方法,特别是通过中介时的方法(双向);
明确指出它们可以被合理使用的新的扩展点和策略。
http/2.0到底有什么?
http/2分层
http/2 大致可以分为两部分:分帧层,即 h2 多路复用能力的核心部分;数据或 http 层,其中包含传统上被认为是 HTTP 及其关联数据的部分。
二进制协议:
h2 的分帧层是基于帧的二进制协议。这方便了机器解析,但是肉眼识别起来比较困难。
首部压缩:
仅仅使用二进制协议似乎还不够,h2 的首部还会被深度压缩。这将显著减少传输中的冗余字节
多路复用:
在你喜爱的调试工具里查看基于 h2 传输的连接的时候,你会发现请求和响应交织在一起。
加密传输:
线上传输的绝大部分数据是加密过的,所以在中途读取会更加困难。
连接
连接是所有 HTTP/2 会话的基础元素,其定义是客户端初始化的一个 TCP/IP socket,客户端 是指发送 HTTP 请求的实体。这和 h1 是一样的,不过与完全无状态的 h1 不同的是,h2 把它所承载的帧(frame)和流(stream)共同依赖的连接层元素捆绑在一起,其中既包含连接层设置也包含首部表。
判断是否支持http/2.0
????协议发现——识别终端是否支持你想使用的协议——会比较棘手。HTTP/2 提供两种协
议发现的机制。
????在连接不加密的情况下,客户端会利用 Upgrade 首部来表明期望使用 h2。如果服务器 也可以支持 h2,它会返回一个“101 Switching Protocols”(协议转换)响应。这增加了 一轮完整的请求-响应通信。
????如果连接基于 TLS,情况就不同了。客户端在 ClientHello 消息中设置 ALPN (Application-Layer Protocol Negotiation,应用层协议协商)扩展来表明期望使用 h2 协 议,服务器用同样的方式回复。
为了向服务器双重确认客户端支持 h2,客户端会发送一个叫作 connection preface(连接 前奏)的魔法字节流,作为连接的第一份数据。这主要是为了应对客户端通过纯文本的 HTTP/1.1 升级上来的情况。该字节流用十六进制表示如下:
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
解码为 ASCII 是:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这个字符串的用处是,如果服务器(或者中间网络设备)不支持 h2,就会产生一个显式错误。这个消息特意设计成 h1 消息的样式。如果运行良好的 h1 服务器收到这个字符串,它会阻塞这个方法(PRI)或者版本(HTTP/2.0),并返回错误,可以让 h2 客户端明确地知道发生了什么错误。
这个魔法字符串会有一个 SETTINGS 帧紧随其后。服务器为了确认它可以支持 h2,会声明收到客户端的 SETTINGS 帧,并返回一个它自己的 SETTINGS 帧(反过来也需要确认), 然后确认环境正常,可以开始使用 h2。(注意⚠️:SETTINGS 帧是个非常重要的帧,http/2.0有十几个帧,不同的帧代表不一样的状态功能)
帧
HTTP/2 是基于帧(frame)的协议。采用分帧是为了将重要信息都封装起来,让协议的解析方可以轻松阅读、解析并还原信息。相比之下,h1 不是基于帧的,而是以 文本分隔。看看下面的简单例子:
GET / HTTP/1.1 <crlf>Host: www.example.com <crlf>Connection: keep-alive <crlf>Accept: text/html,application/xhtml+xml,application/xml;q=0.9... <crlf>User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4)... <crlf>Accept-Encoding: gzip, deflate, sdch <crlf>Accept-Language: en-US,en;q=0.8 <crlf>Cookie: pfy_cbc_lb=p-browse-w; customerZipCode=99912|N; ltc=%20;...<crlf>
解析这种数据用不着什么高科技,但往往速度慢且容易出错。你需要不断读入字节,直到 遇到分隔符为止(这里是指 <crlf>),同时还要考虑一些不太守规矩的客户端,它们会只 发送 <lf>。
解析 h1 的请求或响应可能出现下列问题:
一次只能处理一个请求或响应,完成之前不能停止解析。
无法预判解析需要多少内存。这会带来一系列问题:你要把一行读到多大的缓冲区里。
如果行太长会发生什么;应该增加并重新分配内存,还是返回 400 错误。为了解决这些问题,保持内存处理的效率和速度可不简单。
从另一方面来说,有了帧,处理协议的程序就能预先知道会收到什么。基于帧的协议,特别是 h2,开始有固定长度的字节,其中包含表示整帧长度的字段。
来,我们看一下帧结构????
Length: 3 字节 表示帧负载的长度(取值范围为 2^ 14~2^24-1 字节)。请注意,2^14 字节是默认的最大帧大小,如果需要更大的帧,必须在 SETTINGS 帧中设置 。
Type: 1 字节 当前帧类型
Flags:1 字节 具体帧类型的标识
R: 1 位 保留位,不要设置,否则可能带来严重后果
Stream Identifier:31 位 每个流的唯一 ID
Frame Payload:长度可变 真实的帧内容,长度是在 Length 字段中设置的
因为规范严格明确,所以解析逻辑大概是这样:
loop Read 9 bytes off the wire //读前9个字节 Length = the first three bytes //长度值为前3字节 Read the payload based on the length. //基于长度读负载 Take the appropriate action based on the frame type. // 根据帧类型采取对应操作end loop
http/1有个特性叫管道化(pipelining),允许一次发送一组请求,但是只能按照发送顺序依次接 收响应。而且,管道化备受互操作性和部署的各种问题的困扰,基本没有实用价值。在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之 后。这就是“队头阻塞”.它会阻碍网络传输和 Web 页面渲染,直至失去响应。为了防止 这种问题,现代浏览器会针对单个域名开启 6 个连接,通过各个连接分别发送请求。
它实 现了某种程度上的并行,但是每个连接仍会受到“队头阻塞”的影响。由于 h2 是分帧的, 请求和响应可以交错甚至多路复用。多路复用有助于解决类似队头阻塞的问题。
说了半天队头阻塞的过程是怎样的呢?
我怎么会让你们一脸懵逼的去百度呢,来来来,往下看
借老哥一张图:https://www.jianshu.com/p/450cc7320e30
图中第一种请求方式,就是单次发送request请求,收到response后再进行下一次请求,显示是很低效的。
于是http1.1提出了管线化(pipelining)技术,就是如图中第二中请求方式,一次性发送多个request请求。
然而pipelining在接收response返回时,也必须依顺序接收,如果前一个请求遇到了阻塞,后面的请求即使已经处理完毕了,仍然需要等待阻塞的请求处理完毕。这种情况就如图中第三种,第一个请求阻塞后,后面的请求都需要等待,这也就是队头阻塞(Head of line blocking)。
为了解决上述阻塞问题,http2中提出了多路复用(Multiplexing)技术,Multiplexing是通信和计算机网络领域的专业名词。http2中将多个请求复用同一个tcp链接中,将一个TCP连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。也就是将每个request-response拆分为了细小的二进制帧Frame,这样即使一个请求被阻塞了,也不会影响其他请求,如图中第四种情况所示。
流
HTTP/2 规范对流(stream)的定义是:“HTTP/2 连接上独立的、双向的帧序列交换。”你可以将流看作在连接上的一系列帧,它们构成了单独的 HTTP 请求和响应。如果客户端想 要发出请求,它会开启一个新的流。然后,服务器将在这个流上回复。这与 h1 的请求 / 响应流程类似,重要的区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相阻塞。
看到这里,是不是开始对之前看到的帧,多路复用模糊了,怎么又来了个流?阿西吧,别慌,让我给你解释解释????
流(stream),一个完整的请求-响应数据交互过程,具有如下几个特点:
1. 双向性:同一个流内,可同时发送和接受数据;
2. 有序性:帧(frames)在流上的发送顺序很重要. 接收方将按照他们的接收顺序处理这些frame. 特别是HEADERS和DATA frame的顺序, 在协议的语义上显得尤为重要.
流的创建:流可以被客户端或服务器单方面建立, 使用或共享;
流的关闭:流也可以被任意一方关闭;
敲黑板,画重点!!!别再混淆了!
多路复用:一个连接同一时刻可以被多个流使用。
流的并发性:某一时刻,连接上流的并发数。
咱再强调一下多路复用的好处:
1. 减少服务端连接压力,减少占用内存,提升连接吞吐量;
2. 连接数的减少改善了网络拥塞状况,慢启动时间减少,拥塞和丢包恢复速度更快;
3. 避免连接频繁创建和关闭(三次连接、四次挥手);
连接、流和帧的关系
1. 一个连接同时被多个流复用;
2. 一个流代表一次完整的请求/响应过程,包含多个帧;
3. 一个消息被拆分封装成多个帧进行传输;
消息
HTTP 消息泛指 HTTP 请求或响应。 流是用来传输一对请求 / 响 应消息的。一个消息至少由 HEADERS 帧(它初始化流)组成,并且可以另外包含 CONTINUATION 和 DATA 帧,以及其他的 HEADERS 帧。
h1 的请求和响应都分成消息首部和消息体两部分;与之类似,h2 的请求和响应分成HEADERS 帧和 DATA 帧。
h1 把消息分成两部分:请求 / 状态行;首部。h2 取消了这种区分,并把这些行变成了 魔法伪首部。举个例子,HTTP/1.1 的请求和响应可能是这样的:
GET / HTTP/1.1Host: www.example.comUser-agent: Next-Great-h2-browser-1.0.0Accept-Encoding: compress, gzipHTTP/1.1 200 OKContent-type: text/plainContent-length: 2 ...
在 HTTP/2 中,它等价于:
:scheme: https:method: GET:path: /:authority: www.example.comUser-agent: Next-Great-h2-browser-1.0.0Accept-Encoding: compress, gzip:status: 200content-type: text/plain
请注意,请求和状态行在这里拆分成了多个首部,即 :scheme、:method、:path 和 :status。同时要注意的是,http/2.0 的这种表示方式跟数据传输时不同。
没有分块编码(chunked encoding)
在基于帧的世界里,谁还需要分块?只有在无法预先知道数据长度的情况下向对方发送 数据时,才会用到分块。在使用帧作为核心协议的 h2 里,就不再需要它了。
不再有101的响应
Switching Protocol 响应是 h1 的边缘应用。它如今最常见的应用可能就是用以升级到 WebSocket 连接。ALPN 提供了更明确的协议协商路径,往返的开销也更小。
其实现在大部分还是都是http/1.1,http/2.0网站不多。还好平常刷leetcode,发现leetcode不仅用http/2.0,还用graphql,走在科技的前沿????
再来观摩一下人家的graphql,想起来前段时间组里叫我实现前端graphql,现在想想就是一把辛酸泪????
流量控制
h2 的新特性之一是基于流的流量控制。不同于 h1 的世界,只要客户端可以处理,服务端就会尽可能快地发送数据,h2 提供了客户端调整传输速度的能力。(并且,由于在 h2 中, 一切几乎都是对称的,服务端也可以调整传输的速度。)WINDOW_UPDATE 帧用来指示流量控制信息。每个帧告诉对方,发送方想要接收多少字节。
客户端有很多理由使用流量控制。一个很现实的原因可能是,确保某个流不会阻塞其他流。也可能客户端可用的带宽和内存比较有限,强制数据以可处理的分块来加载反而可以提升效率。尽管流量控制不能关闭,把窗口最大值设定为设置 2^31-1 就等效于禁用它,至少对小于 2GB 的文件来说是如此。
另一个需要注意的是中间代理。通常情况下,网络内容通过代理或者 CDN 来传输,也许它们就是传输的起点或终点。由于代理两端的吞吐能力可能不同,有了流量控制,代理的两端就可以密切同步,把代理的压力降到最低。
优先级
流的最后一个重要特性是依赖关系。现代浏览器都经过了精心设计,首先请求网页上最重要的元素,以最优的顺序获取资源,由此来优化页面性能。拿到了 HTML 之后,在渲染页面之前,浏览器通常还需要 CSS 和关键 JavaScript 这样的东西。在没有多路复用的时候,在它可以发出对新对象的请求之前,需要等待前一个响应完成。有了 h2,客户端就可以 一次发出所有资源的请求,服务端也可以立即着手处理这些请求。由此带来的问题是,浏览器失去了在 h1 时代默认的资源请求优先级策略。假设服务器同时接收到了 100 个请求, 也没有标识哪个更重要,那么它将几乎同时发送每个资源,次要元素就会影响到关键元素 的传输。
h2 通过流的依赖关系来解决这个问题。通过 HEADERS 帧和 PRIORITY 帧,客户端可以明确地和服务端沟通它需要什么,以及它需要这些资源的顺序。这是通过声明依赖关系树 和树里的相对权重实现的。
• 依赖关系为客户端提供了一种能力,通过指明某些对象对另一些对象有依赖,告知服务器这些对象应该优先传输。
• 权重让客户端告诉服务器如何确定具有共同依赖关系的对象的优先级。
我们来看看这个简单的网站:
index.html– header.jpg– critical.js– less_critical.js– style.css– ad.js– photo.jpg
在收到主体 HTML 文件之后,客户端会解析它,并生成依赖树,然后给树里的元素分配权重。这时这棵树可能是这样的:
index.html – style.css – critical.js – less_critical.js (weight 20) – photo.jpg (weight 8) – header.jpg (weight 8) – ad.js (weight 4)
在这个依赖树里,客户端表明它最需要的是 style.css,其次是 critical.js。没有这两个文件, 它就不能接着渲染页面。等它收到了 critical.js,就可以给出其余对象的相对权重。权重表示服务一个对象时所需要花费的对应“努力”程度。
服务器推送
提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 HTTP/2 的服务端推送的目的。推送使服务器能够主动将对象发给客户端,这可能是因为 它知道客户端不久将用到该对象。
如果服务器决定要推送一个对象(RFC 中称为“推送响应”),会构造一个 PUSH_PROMISE 帧。这个帧有很多重要属性,列举如下 :
????PUSH_PROMISE 帧首部中的流 ID 用来响应相关联的请求。推送的响应一定会对应到 客户端已发送的某个请求。如果浏览器请求一个主体 HTML 页面,如果要推送此页面 使用的某个 JavaScript 对象,服务器将使用请求对应的流 ID 构造 PUSH_PROMISE 帧。
????PUSH_PROMISE 帧的首部块与客户端请求推送对象时发送的首部块是相似的。所以客户端有办法放心检查将要发送的请求。
????被发送的对象必须确保是可缓存的。
????:method 首部的值必须确保安全。安全的方法就是幂等的那些方法,这是一种不改变
任何状态的好办法。例如,GET 请求被认为是幂等的,因为它通常只是获取对象,而POST 请求被认为是非幂等的,因为它可能会改变服务器端的状态。
????理想情况下,PUSH_PROMISE 帧应该更早发送,应当早于客户端接收到可能承载着推
送对象的 DATA 帧。假设服务器要在发送 PUSH_PROMISE 之前发送完整的 HTML, 那客户端可能在接收到 PUSH_PROMISE 之前已经发出了对这个资源的请求。h2 足够健壮,可以优雅地解决这类问题,但还是会有些浪费。
????PUSH_PROMISE 帧会指示将要发送的响应所使用的流 ID。
客户端会从 1 开始设置流 ID,之后每新开启一个流,就会增加 2,之后一直 使用奇数。服务器开启在 PUSH_PROMISE 中标明的流时,设置的流 ID 从 2 开始,之后一直使用偶数。这种设计避免了客户端和服务器之间的流 ID 冲 突,也可以轻松地判断哪些对象是由服务端推送的。0 是保留数字,用于连 接级控制消息,不能用于创建新的流。
如果客户端对 PUSH_PROMISE 的任何元素不满意,就可以按照拒收原因选择重置这个流 (使用 RST_STREAM),或者发送 PROTOCOL_ERROR(在 GOAWAY 帧中)。常见的情况是缓存中已经有了这个对象。而 PROTOCOL_ERROR 是专门留给 PUSH_PROMISE 涉及的协议层面问题的,比如方法不安全,或者当客户端已经在 SETTINGS 帧中表明自己不接受推送时,仍然进行了推送。值得注意的是,服务器可以在 PUSH_PROMISE 发送后立即启动推送流,因此拒收正在进行的推送可能仍然无法避免推送大量资源。推送正确的资源是不够的,还需要保证只推送正确的资源,这是重要的性能优化手段。
所以到底如何选择要推送的资源?
决策的过程需要考虑到如下方面:
• 资源已经在浏览器缓存中的概率
• 从客户端看来,这些资源的优先级
• 可用的带宽,以及其他类似的会影响客户端接收推送的资源
如果用户第一次访问页面时,就能向客户端推送页面渲染所需的关键 CSS 和 JS 资源,那 么服务端推送的真正价值就实现了。不过,这要求服务器端实现足够智能,以避免“推送 承诺”(push promise)与主体 HTML 页面传输竞争带宽。
首部压缩
“臃肿的消息首部”提到过,现代网页平均包含 140 个请求,每个 HTTP 请求 平均有 460 字节,总数据量达到 63KB。即使在最好的环境下,这也会造成相当长的延时, 如果考虑到拥挤的 WiFi 或连接不畅的蜂窝网络,那可是非常痛苦的。这些请求之间通常几乎没有新的或不同的内容,这才是真正的浪费。所以,大家迫切渴望某种类型的压缩。 经过多次创新性的思考和讨论,人们提出了 HPACK。HPACK 是种表查找压缩方案,它利用霍夫曼编码获得接近 GZIP 的压缩率。
CRIME 攻击告诉我们,GZIP 也有泄漏加密信息的风 险。CRIME 的原理是这样的,攻击者在请求中添加数据,观察压缩加密后的数据量是否会小于预期。如果变小了,攻击者就知道注入的文本和请求中的其他内容(比如私有的会话 cookie)有重复。在很短的时间内,经过加密 的数据内容就可以全部搞清楚。因此,大家放弃了已有的压缩方案,研发出 HPACK。
第一个请求:
:authority: www.akamai.com:method: GET:path: /:scheme: httpsaccept: text/html,application/xhtml+xmlaccept-language: en-US,en;q=0.8cookie: last_page=286A7F3DEupgrade-insecure-requests: 1user-agent: Awesome H2/1.0
第二个请求:
:authority: www.akamai.com:method: GET :path: /style.css :scheme: https accept: text/html,application/xhtml+xml accept-language: en-US,en;q=0.8 cookie: last_page=*398AB8E8F upgrade-insecure-requests: 1 user-agent: Awesome H2/1.0
可以看到,后者的很多数据与前者重复了。第一个请求约有 220 字节,第二个约有 230 字 节,但二者只有 36 字节是不同的。如果仅仅发送这 36 字节,就可以节省约 85%的字节 数。简而言之,HPACK 的原理就是这样。
http/2.0反模式
h1 下的一些性能调优办法在 h2 下会起到反作用。
域名拆分
域名拆分是为了利用浏览器对每个域名开启多个连接的能力,以便实现资源的并行下载, 绕过 h1 的串行化下载的限制。对于包含大量小型资源的网站,普遍的做法是拆分域名, 以利用现代浏览器针能对每个域名开启 6 个连接的特性。这样实际上做到了让浏览器并行 发送多个请求,以及充分利用可用带宽的效果。因为 HTTP/2 采取多路复用,所以域名拆 分就不是必要的了,并且反而会让协议力图实现的目标落空。
资源内联
资源内联包括把 JavaScript、样式,甚至图片插入到 HTML 页面中,目的是省掉加载外部资源所需的新连接以及请求响应的时间。然而,有些 Web 性能的最佳实践不推荐使用内联,因为这样会损失更有价值的特性,比如缓存。如果有同一个页面上的重复访问,缓存通常可以减少请求数(而且能够加速页面渲染)。尽管如此,总体来说,对那些渲染滚动 条以上区域所需的微小资源进行内联处理仍是值得的。
事实上有证据表明,在性能较弱的 设备上,缓存对象的好处不够多,把内联资源拆分出来并不划算。使用 h2 时的一般原则是避免内联,但是内联也并不一定毫无价值。
资源合并
资源合并意味着把几个小文件合并成一个大文件。它与内联很相似,旨在省掉那些加载外部资源的请求响应时间,以及解码 / 执行那些资源所消耗的 CPU 资源。之前针对资源内联 的规则同样适用于资源合并,我们可以使用它来合并非常小的文件(1KB 或更小),以及 对初始渲染很关键的最简化 JavaScript/CSS 资源。
禁用cookie的域名
通过禁用 cookie 的域名来提供静态资源是一项标准的性能优化最佳实践。尤其是使用 h1 时,你无法压缩首部,而且有些网站使用的 cookie 大小常常超过单个 TCP 数据包的限度。不过,在 h2 下请求首部使用 HPACK 算法被压缩,会显著减少巨型 cookie(尤其是当它 们在先后请求之间保持不变)的字节数。与此同时,禁用 cookie 的域名需要额外的主机名 称,这意味着将开启更多的连接。
如果你正在使用禁用 cookie 的域名,以后有机会你可能得考虑消灭它。如果你确实不需要那些域名,最好删掉它们。省一个字节就是一个字节。
生成精灵图
目前,生成精灵图仍是一种避免小资源请求过多的技术(你能看到人们乐意做什么来优化 h1)。为了生成精灵图,开发者把较小的图片拼合成较大的图片,然后用 CSS 选择图片中 某个部分展示出来。依据设备及其硬件图形处理能力的不同,精灵图要么非常高效,要么 非常低效。如果用 h2,最佳实践就是避免生成精灵图;主要原因在于,多路复用和首部压缩去掉了大量的请求开销。即便如此,还是有些场景适合使用精灵图。
性能优化因人而异
为了最大化 Web 性能,需要在许多变量之间取舍,包括网络条件、设备 处理能力、浏览器能力,还有协议限制。这些组成了我们所说的场景,大多数开发者 的时间远远不够考虑那么多场景。
怎么办?最佳实践的第一原则就是:测试。性能测试与监控是获得最大成果的关键, HTTP/2 也不例外。观察真实用户数据、详尽分析各种条件、查找问题,然后解决它们。要遵循业界推荐的方式,但也不要陷入过早优化的陷阱。
关于本文
作者:三妹不是佩奇啊
来源:掘金
原文链接:https://juejin.im/post/5da16e9ef265da5b76373d0e
版权声明:版权归作者所有
更多文章请点击“阅读原文”
喜欢本文点个“在看”哟!