UDN-企业互联网技术人气社区

板块导航

浏览  : 4369
回复  : 11

[求助] WebSocket 的原理是什么?为什么可以实现持久连接?

[复制链接]
逗比坑总的头像 楼主
  WebSocket是HTML5出的东西 也就是说HTTP协议没有变化 但HTTP是不支持持久连接的(长连接,循环连接的不算)
  或者说WebSocket干脆就不是基于HTTP来执行的。但是。。。说不通啊。。。

  他是怎么实现的呢??为什么可以实现持久连接????


  【【【【【【【【【【【【【【补充】】】】】】】】】】】】】】】

  既然WebSocket和HTTP是两个协议 为什么要在HTML5才支持

  又如果说HTML5 出来以后可以用WebSocket了 就说明WebSocket是本来就有点东西只是HTML4不支持而已 http4时代 如何使用WebSocket呢?? 谢谢

相关帖子

发表于 2015-6-9 15:29:59 | 显示全部楼层

你可以把 WebSocket 看成是 HTTP 协议为了支持长连接所打的一个大补丁,它和 HTTP 有一些共性,是为了解决 HTTP 本身无法解决的某些问题而做出的一个改良设计。在以前 HTTP 协议中所谓的 keep-alive connection 是指在一次 TCP 连接中完成多个 HTTP 请求,但是对每个请求仍然要单独发 header;所谓的 polling 是指从客户端(一般就是浏览器)不断主动的向服务器发 HTTP 请求查询是否有新数据。这两种模式有一个共同的缺点,就是除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。它们建立的“长连接”都是伪.长连接,只不过好处是不需要对现有的 HTTP server 和浏览器架构做修改就能实现。

WebSocket 解决的第一个问题是,通过第一个 HTTP request 建立了 TCP 连接之后,之后的交换数据都不需要再发 HTTP request了,使得这个长连接变成了一个真.长连接。但是不需要发送 HTTP header就能交换数据显然和原有的 HTTP 协议是有区别的,所以它需要对服务器和客户端都进行升级才能实现。在此基础上 WebSocket 还是一个双通道的连接,在同一个 TCP 连接上既可以发也可以收信息。此外还有 multiplexing 功能,几个不同的 URI 可以复用同一个 WebSocket 连接。这些都是原来的 HTTP 不能做到的。

另外说一点技术细节,因为看到有人提问 WebSocket 可能进入某种半死不活的状态。这实际上也是原有网络世界的一些缺陷性设计。上面所说的 WebSocket 真.长连接虽然解决了服务器和客户端两边的问题,但坑爹的是网络应用除了服务器和客户端之外,另一个巨大的存在是中间的网络链路。一个 HTTP/WebSocket 连接往往要经过无数的路由,防火墙。你以为你的数据是在一个“连接”中发送的,实际上它要跨越千山万水,经过无数次转发,过滤,才能最终抵达终点。在这过程中,中间节点的处理方法很可能会让你意想不到。

比如说,这些坑爹的中间节点可能会认为一份连接在一段时间内没有数据发送就等于失效,它们会自作主张的切断这些连接。在这种情况下,不论服务器还是客户端都不会收到任何提示,它们只会一厢情愿的以为彼此间的红线还在,徒劳地一边又一边地发送抵达不了彼岸的信息。而计算机网络协议栈的实现中又会有一层套一层的缓存,除非填满这些缓存,你的程序根本不会发现任何错误。这样,本来一个美好的 WebSocket 长连接,就可能在毫不知情的情况下进入了半死不活状态。

而解决方案,WebSocket 的设计者们也早已想过。就是让服务器和客户端能够发送 Ping/Pong Frame(RFC 6455 - The WebSocket Protocol)。这种 Frame 是一种特殊的数据包,它只包含一些元数据而不需要真正的 Data Payload,可以在不影响 Application 的情况下维持住中间网络的连接状态。
使用道具 举报

回复

发表于 2015-6-9 15:40:40 | 显示全部楼层
额。。最高票答案没答到点子上,最后怎么跑到Nodejs上去了。。Websocket只是协议而已。。
我一个个来回答吧

一、WebSocket是HTML5出的东西(协议)
也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充可以通过这样一张图理解

6651f2f811ec133b0e6d7e6d0e194b4c_b.jpg


有交集,但是并不是全部,另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。。通俗来说,你可以用HTTP协议传输非Html数据,就是这样=。= 再简单来说,层级不一样。

二、Websocket是什么样的协议,具体有什么优点


首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。
1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

教练,你BB了这么多,跟Websocket有什么关系呢?
_(:з」∠)_好吧,我正准备说Websocket呢。。


首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。


在握手阶段是一样的


-------以下涉及专业技术内容,不想看的可以跳过lol:,或者只看加黑内容--------


首先我们来看个典型的Websocket握手(借用Wikipedia的。。)
GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==Sec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13Origin: http://example.com


熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。
我会顺便讲解下作用。


Upgrade: websocketConnection: Upgrade


这个就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。


Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==Sec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13


首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。


然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~


最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,

什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西~ 脱水:服务员,我要的是13岁的噢→_→

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!


HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=Sec-WebSocket-Protocol: chat


这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~


Upgrade: websocketConnection: Upgrade


依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。


然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。


后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。

至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。


具体的协议就不在这阐述了。


------------------技术解析部分完毕------------------

afe119b52e096016139edabc2dfa9661_b.jpg

你TMD又BBB了这么久,那到底Websocket有什么鬼用,http long poll,或者ajax轮询不都可以实现实时信息传递么。


20110e661edb1e93755a99c1d826e264_b.jpg

好好好,年轻人,那我们来讲一讲Websocket有什么用。


来给你吃点胡(苏)萝(丹)卜(红)


31ddf0cfbeecef21568d85ca60b5f1ff_b.jpg

三、Websocket的作用


在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。


首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。


场景再现:
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:没有(Response)
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:没有。。(Response)
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:你好烦啊,没有啊。。(Response)
  • 客户端:啦啦啦,有没有新消息(Request)
  • 服务端:好啦好啦,有啦给你。(Response)
  • 客户端:啦啦啦,有没有新消息(Request)
  • 服务端:。。。。。没。。。。没。。。没有(Response) ---- loop

long poll
long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。


场景再现
  • 客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
  • 服务端:额。。 等待到有消息的时候。。来 给你(Response)
  • 客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。


何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。


简单地说就是,服务器是一个很懒的冰箱(这是个梗)(不会、不能主动发起连接),但是上司有命令,如果有客户来,不管多么累都要好好接待。

说完这个,我们再来说一说上面的缺陷(原谅我废话这么多吧OAQ)


从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。


ajax轮询 需要服务器有很快的处理速度和资源。(速度)


long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)


所以ajax轮询 和long poll 都有可能发生这种情况。

  • 客户端:啦啦啦啦,有新信息么?
  • 服务端:月线正忙,请稍后再试(503 Server Unavailable)
  • 客户端:。。。。好吧,啦啦啦,有新信息么?
  • 服务端:月线正忙,请稍后再试(503 Server Unavailable)

客户端:

7c0cf075c7ee4cc6cf52f4572a4c1c10_b.jpg


然后服务端在一旁忙的要死:冰箱,我要更多的冰箱!更多。。更多。。(我错了。。这又是梗。。)

--------------------------


言归正传,我们来说Websocket吧


通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。


一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。


哦对了,忘记说了HTTP还是一个状态协议。


通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。

所以在这种情况下出现了,Websocket出现了。


他解决了HTTP的这几个难题。


首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。


所以上面的情景可以做如下修改。
  • 客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
  • 服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
  • 客户端:麻烦你有信息的时候推送给我噢。。
  • 服务端:ok,有的时候会告诉你的。
  • 服务端:balabalabalabala
  • 服务端:balabalabalabala
  • 服务端:哈哈哈哈哈啊哈哈哈哈
  • 服务端:笑死我了哈哈哈哈哈哈哈

就变成了这样,只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)


这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。


那么为什么他会解决服务器上消耗资源的问题呢?


其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。


简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。


本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。


Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。


这样就可以解决客服处理速度过慢的问题了。

同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。


虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。


但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析

HTTP协议,还要查看identity info的信息。


同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了
--------------------


至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能


但是可以通过上面说的 long poll 和 ajax 轮询来 模拟出类似的效果
-----


_(:з」∠)_两天写了两篇科普类文章。。好累OAQ,求赞。。


对啦,如果有错误,欢迎大家在底下点评指出噢~


使用道具 举报

回复

发表于 2015-6-9 15:41:06 | 显示全部楼层
WebSocket 是一种协议,基于 TCP 协议;HTTP 也是一种协议,基于 TCP 协议。
连接要保持还是关闭是由你服务器应用来控制的。

WebSocket 协议和 HTTP 协议是两种不同的东西,它们扯上关系是只是因为:
客户端开始建立 WebSocket 连接时要发送一个 header 标记了 Upgrade 的 HTTP 请求,表示请求协议升级。
使用道具 举报

回复

发表于 2015-6-9 15:41:29 | 显示全部楼层
websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。websocket是一种全新的协议,不属于http无状态协议,协议名为"ws",这意味着一个websocket连接地址会是这样的写法:ws://**。websocket协议本质上是一个基于tcp的协议。
使用道具 举报

回复

发表于 2015-6-9 15:41:45 | 显示全部楼层
HTML5 是一个很宽广的概念,是对大量新 API 的总称。不存在 HTTP5 的概念,HTTP 最高的版本号是 1.1。简单来说,你可以完全抛开 HTML5 和 HTML4 的概念,只考虑浏览器要么支持 WebSocket,要么不支持。

WebSocket 跟其他 API 比较不一样的是,它不仅仅依赖于浏览器支持,同时要求服务器和代理(假若需要经过代理的话)支持。WebSocket 本质上跟 HTTP 完全不一样,只不过为了兼容性,WebSocket
使用道具 举报

回复

发表于 2015-6-9 15:42:08 | 显示全部楼层
"或者说WebSocket干脆就不是基于HTTP来执行的",这句是对的,WebSocket 和 HTTP 都是基于 TCP 的,TCP是传输层协议, WebSocket 和 HTTP是应用层协议,就是因为HTTP不能满足特定的需求才被设计出来的,所以会支持你说的那些特性。
使用道具 举报

回复

发表于 2015-6-9 15:42:24 | 显示全部楼层
是服务器实现的。

客户端通过html5与服务器交互。http是不持续连接的,而websocket是。必须服务器支持websocket协议,才有效。对于不支持websocket服务的服务器,你客户端怎么写代码都没用。

web服务器必须支持websocket协议。我想这可以简单的回答题主的问题。
使用道具 举报

回复

逗比坑总的头像 楼主
发表于 2015-6-9 15:45:30 | 显示全部楼层

WebSocket protocol 是HTML5一种新的协议。它是实现了浏览器与服务器全双工通信(full-duplex)。
现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽。


而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送,改变了原有的B/S模式。

这里,我主要对websocket 协议进行一个简单的介绍。
Websocket 业务模型
浏览器端的websocket 请求一般是
  1.   // javacsript
  2.   var ws = new WebSocket("ws://127.0.0.1:4000");
  3.   ws.onopen = function(){
  4.     console.log("succeed");
  5.   };
  6.   ws.onerror = function(){
  7.     console.log(“error”);
  8.   };
  9.   ws.onmessage = function(e){
  10.   console.log(e);
  11.   }
复制代码


当 new 一个 websocket 对象之后,就会向服务器发送一个 get 请求

这个请求是对摸个服务器的端口发送的,一般的话,会预先在服务器将一个socket 绑定到一个端口上,客户端和服务器端在这个预定的端口上通信(我这里绑定的就是 4000 端口,默认情况下,websocke 使用 80 端口)。

然后,在服务器端的socket监听到这个packet 之后就生成一个新的 socket,将发送过来的数据中的 Sec-WebSocket-Key 解析出来,然后按照把“Sec-WebSocket-Ke”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。

客户端收到这个之后,就会将 通信协议 upgrade 到 websocket 协议。

然后就会在这个持久的通道下进行通信,包括浏览器的询问,服务器的push,双方是在一个全双工的状态下相互通信。 切换后的websocket 协议中的 数据传输帧的格式(此时不再使用html协议) 官方文档给出的说明:
    0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+
直接看这个,谁都会有点头大: 我花了一幅图来简单的解释这个 frame 的结构。

各字段的解释:
  • FIN 1bit 表示信息的最后一帧,flag,也就是标记符
  • RSV 1-3 1bit each 以后备用的 默认都为 0
  • Opcode 4bit 帧类型,
  • Mask 1bit 掩码,是否加密数据,默认必须置为1
  • Payload len 7bit 数据的长度,当这个7 bit的数据 == 126 时,后面的2 个字节也是表示数 据长度,当它 == 127 时,后面的 8 个字节表示数据长度Masking-key 1 or 4 bit 掩码Payload data playload len bytes 数据

所以我们这里的代码,通过判断 Playload len的值,来用 substr 截取 Masking-key 和 PlayloadData。
根据掩码解析数据的方法是:
  1. for( i = 0; i < data.length ; i++){
  2. orginalData += data<i> ^ maskingKey[i mod 4];
  3. }</i>
复制代码


在PHP中,当我们收到数据之后,按照这里的格式截取数据,并将其按照这里的方法解析后就得到了浏览器发送过来的数据。 当我们想把数据发送给浏览器时,也要按照这个格式组装frame。 这里是我的方法:
  1.   function frame($s){
  2.                   $a = str_split($s, 125);
  3.                   if (count($a) == 1){
  4.                           return "\x81" . chr(strlen($a[0])) . $a[0];
  5.                   }
  6.                   $ns = "";
  7.                   foreach ($a as $o){
  8.                           $ns .= "\x81" . chr(strlen($o)) . $o;
  9.                   }
  10.                   return $ns;
  11.           }
复制代码


强行将要发送的数据分割成 125 Byte / frame,这样 playload len 只需要 7 bits。也就是直接将数据的长度的ascall码拼接上去,然后后面跟上要发送的数据。 每一个 frame 前面加的 ‘\x81’ 用二进制就是: 1000 0001 1000 :
1 是 FIN
000 是三个备用的bit
0001 : 指的是 opcode 官方的解释:
  |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
| 0      | Continuation Frame                  | RFC 6455  |
-+--------+-------------------------------------+-----------|
| 1      | Text Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
| 2      | Binary Frame                        | RFC 6455  |
-+--------+-------------------------------------+-----------|
| 8      | Connection Close Frame              | RFC 6455  |
-+--------+-------------------------------------+-----------|
| 9      | Ping Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
| 10     | Pong Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
可以设置 opcode的值,来告诉浏览器这个frame的数据属性。

使用道具 举报

回复

发表于 2015-6-9 17:59:02 | 显示全部楼层
学习了好多  我想到了我的宝宝
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部