深入了解HTTP和Socket在实时性Web上的实践

注:本文很多内容直接来自参考资料,如果需要更详细的了解,建议阅读本文后继续阅读参考资料

实时的Web

在我们的大部分网站中,我们都采用了传统的HTTP请求来和服务端进行通信,包括资源文件的下载,异步数据的请求。这样的策略已经能满足大部分网站的需求,因为它并不需要保证站点上的数据有多么实时。但是,也总有很多情况下,我们需要去保证数据的实时性,比如股票价格,微博刷新,以及交通状况等。仅仅寄希望于用户的钛合金F5肯定是不负责任的,我们需要去考虑如何在我们的站点中也能提供实时的数据。

8IIli 深入了解HTTP和Socket在实时性Web上的实践

基于HTTP的实现

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

现在网站中大部分实时数据的实现,还是依赖于轮询和其他一些服务端推送的数据。最主要的原因是早期的浏览器只提供的传统HTTP请求的支持,所幸 HTML5规范中涉及到了新的请求机制, webSockets API 。各大浏览器也在最新的版本中对其进行了一些实践,兼容性如下:

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Version -76 support Obsolete} 6 4.0 (2.0) Not supported 11.00 (disabled) 5.0.1
Protocol version 7 support Not supported 6.0 (6.0) 
Moz
Not supported Not supported Not supported
Protocol version 10 support 14 7.0 (7.0) 
Moz
HTML5 Labs ? ?
RFC 6455 Support (IETF Draft 17) 16 11.0 (11.0) 10 12.10 ?
Feature Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
Version -76 support Obsolete} ? ? ? ? ?
Protocol version 7 support ? ? ? ? ?
Protocol version 8 support (IETF draft 10) ? 7.0 (7.0) ? ? ?
RFC 6455 Support (IETF Draft 17) 16 11.0 (11.0) ? 12.10 ?

其实我们还有一个选择就是通过使用Flash的XMLSocket来实现实时的数据交互。但是问题很明显:客户端必须安装flash;XMLSocket没有HTTP隧道功能,无法自动穿过防火墙;因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制。

Java Applet也是一个实现方式,但是有诸多的限制,有兴趣的同学可以自行了解一下。

我们先从传统的实现方式说起。

Comet推送

“服务器推送”是一种很早就存在的技术,在非浏览器平台上,通常都是通过客户端的套接字接口或者是服务端的远程调用实现。由于浏览器本身带来的限制,并没有一个非常完善的方案去实现并应用到产品中。以前,可以通过AJAX,或者Iframe嵌入文档的ActiveX组件中解决IE上的加载问题。后来Alex Russell(Dojo Toolkit 的项目 Lead)将基于HTTP长连接,无需在安装插件的服务端推送技术称为“Comet”。

Comet实现了一种从服务端异步推送数据到客户端的机制,虽然它是基于HTTP长连接的一种实现,但是HTTP规范本身并不提倡这种工作模式。下面介绍两种Comet的具体实践。

长轮询(long-polling)

长轮询,从字面的意思,我们就可以揣测到具体的实现方式:

客户端按照一个固定的时间定期向服务器发送请求,通常这个时间间隔的长度受到服务端的更新频率和客户端处理更新数据时间的影响。

服务端的处理方式就是:如果有更新数据,则返回更新数据,如果没有更新数据,则阻塞请求。当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。

2x0kZ 深入了解HTTP和Socket在实时性Web上的实践

基于Iframe的方式

iframe可以在页面中嵌套一个子页面,通过将iframe的src指向一个长连接的请求地址,服务端就能不断往客户端传输数据。具体的实现方式就是iframe服务端不直接输出数据到页面,而是通过脚本执行的方式,比如输出

“<script type=”text/javascript”>js_func(“data from server ”)</script>”

这样形式的代码可以直接被浏览器执行,从而把返回的数据传递给客户端。

Iframe解决方案的问题在于,IE和FF下的进度栏都会显示加载没有完成,并且IE上方的图标会不停转动。这个问题可以通过使用 new ActiveXObject(“htmlfile”) 来解决,有兴趣的同学可以自行了解下。

S6Dlq 深入了解HTTP和Socket在实时性Web上的实践

长连接只会在通信出现错误的时候关闭连接(一些防火墙经常会丢弃过长的连接),所以我们还需要有一个超时和出错重连的机制。

流(streaming)AJAX解决方案

streaming ajax是一种通过ajax实现的长连接维持机制。可以理解为是长轮询的一个分支,缘由是因为Firefox提供了对Streaming AJAX的支持,主要目的就是在数据传输过程中对返回的数据进行读取,并且不会关闭连接。

具体处理方式可能是

<span class="keyword" style="font-weight:bold">var</span> dv = document.getElementById(<span class="string" style="color:#dd1144;">'dvRst'</span>), 
  xhr = <span class="keyword" style="font-weight:bold">new</span> XMLHttpRequest();
xhr.onreadystatechange = <span class="function"><span class="keyword" style="font-weight:bold">function</span> <span class="params">()</span> {</span>
  dvRst.innerHTML += <span class="string" style="color:#dd1144;">"AJAX.readyState:"</span> + xhr.readyState + <span class="string" style="color:#dd1144;">"<br/>"</span>;
  <span class="keyword" style="font-weight:bold">if</span> (xhr.readyState == <span class="number" style="color:#09999;">3</span>) {
    dvRst.innerHTML += xhr.responseText + <span class="string" style="color:#dd1144;">'<br/>'</span>; 
  }
  <span class="keyword" style="font-weight:bold">if</span> (xhr.readyState == <span class="number" style="color:#09999;">4</span>) {
    clearInterval(timer);
  }
}
xhr.open(<span class="string" style="color:#dd1144;">"get"</span>, <span class="string" style="color:#dd1144;">"StreamingAJAX.ashx"</span>, <span class="literal">true</span>);
xhr.send(<span class="literal">null</span>);

Comet的一些注意问题

  • 由于在HTTP 1.1规范中规定,客户端不应该与服务器建立超过两个的HTTP连接,这也是为什么IE6/7在最多只能并行连接两个HTTP 1.1的同域请求。而Iframe的长连接会一直保持,导致占用了两个连接中的一个,那么如果有两个及以上的iframe保持长连接,就会导致其他同域请求的阻塞,可以考虑让多个iframe公用一个长连接。
  • Web服务器会为每一个连接创建一个线程,这对服务端来说需要维护大量并发的长连接。HTTP 1.1与1.0规范也有一个很大的不同:1.0规范在服务器处理完每个GET/POST请求后会关闭套接字接口连接;而1.1规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接会处于空闲状态。一些服务端在处理空闲连接的时候会把连接分配到的线程资源返回给线程池,如果频繁地进行请求,而且Comet机制下连接的事件会比较长,会导致服务端的效率降低,甚至阻塞新的连接处理。
  • 使用长连接的时候,存在一个问题,客户端的网页已经准备关闭了,但是服务端还处于阻塞状态,客户端需要通知服务端关闭数据连接。所以通常我们在使用iframe进行长连接时,还需要有一个额外的请求来告诉服务端客户端的状态,及时释放客户端的资源。
  • 服务端需要做一些异常状况湖综合超时处理,及时释放被客户端占用的资源。
  • 客户端需要有一个合理的机制来处理服务端返回的数据,包括轮询时间等等。频繁地刷新页面在用户体验也是一个巨大的考验,在保证数据实时性的同时,我们还要能保证页面的性能。

基于Socket的实现

好吧,上面赘述了很多关于HTTP上的web实时机制的实现。接下来应该说说重点了,就是Socket的实现方式,以及优势。

在网络上查找socket相关资料的时候,经常会把socket和电话插座拿来对比,不仅仅是因为socket的英文原义是“插座”,更重要的是,电话机发送信号和对方从电话机接收信号的过程,相当于socket发送和接收数据的过程。对于通话双方来说,通信设施的细节不重要,重要的是两端都有电话机,也就是都支持socket这样的连接方式。

webSocket机制

websocket是一个基于TCP连接的全双工通信方式,服务端和客户端可以相互推送数据。

我们可以看看websocket是如何保持客户端和服务端通信的。

这里需要注意一点,websocket在连接的时候有一个握手阶段,但是这和TCP的三次握手又是不一样的。TCP的三次握手是为了保证连接可靠,当TCP三次握手成功的时候,websocket的握手阶段才真正开始。TCP三次握手传送的是TCP报文,而websocket的握手传送的是HTTP报文,这个是不太一样的地方。

握手开始的时候,我们需要现发送一个HTTP 1.1的请求头部:(下面的例子来自websocket的 TFC6455 文档)

<span class="request" style="font-weight:bold">GET <span class="string" style="color:#dd1144;">/chat</span> HTTP/1.1</span>
<span class="attribute" style="color:#08080;">Host</span>: <span class="string" style="color:#dd1144;">server.example.com</span>
<span class="attribute" style="color:#08080;">Upgrade</span>: <span class="string" style="color:#dd1144;">websocket</span>
<span class="attribute" style="color:#08080;">Connection</span>: <span class="string" style="color:#dd1144;">Upgrade</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Key</span>: <span class="string" style="color:#dd1144;">dGhlIHNhbXBsZSBub25jZQ==</span>
<span class="attribute" style="color:#08080;">Origin</span>: <span class="string" style="color:#dd1144;">http://example.com</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Protocol</span>: <span class="string" style="color:#dd1144;">chat, superchat</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Version</span>: <span class="string" style="color:#dd1144;">13</span>

服务端返回的成功握手请求头部如下:

<span class="status" style="font-weight:bold">HTTP/1.1 <span class="number" style="color:#09999;">101</span> Switching Protocols</span>
<span class="attribute" style="color:#08080;">Upgrade</span>: <span class="string" style="color:#dd1144;">websocket</span>
<span class="attribute" style="color:#08080;">Connection</span>: <span class="string" style="color:#dd1144;">Upgrade</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Accept</span>: <span class="string" style="color:#dd1144;">s3pPLMBiTxaQ9kYGzzhZRbK+xOo=</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Protocol</span>: <span class="string" style="color:#dd1144;">chat</span>

Upgrade:WebSocket表示这是一个特殊的 HTTP 请求,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议。

一旦连接成功后,就可以在全双工的模式下在客户端和服务端之间来回传送WebSocket消息。这就意味着,在同一时间、任何方向,都可以双向发送基于文本的消息。每个消息已0×00字节开头,以0xff结尾(这样就可以解决TCP协议中的黏包问题,在TCP协议中,会存在两个缓冲区来存放发送的数据或者接收的数据,如果没有明显的分隔符,服务端无法正确识别命令),中间数据的编码是UTF-8。

P5Ff9 深入了解HTTP和Socket在实时性Web上的实践

关于如何使用WebSocket也不赘述,主要还是说说WebSocket带来了什么。

WebSocket的优势

传输速度收到的影响很多,我们可以从多个角度对HTTP和WebSocket进行比较。

从纯粹的字节数角度考虑

HTTP:每一次数据传输都需要有一个HTTP头部,头部的大小不一,可能只有几百B,也可能有几千B。

Xin3J 深入了解HTTP和Socket在实时性Web上的实践

WebSocket只有在进行连接的时候需要发送一个HTTP请求,之后就再也不需要发送纷繁的HTTP头部信息,光从字节数上就减少了很多。而在关闭WebSocket的过程中,也不需要像建立握手的时候那么繁杂,只需要传送一个特定的字节码0×8的关闭帧就行,服务端收到之后,需要响应一个关闭帧到客户端。

从请求数的角度考虑

正常情况下,如果我们要请求多个数据,就多发多次HTTP请求,整个过程包括建立连接,关闭连接,特别是建立连接的时间在整个传输时间中还占据了比较大的比重。HTTP长连接的劣势也在上面有描述过。

s1jMq 深入了解HTTP和Socket在实时性Web上的实践

WebSocket可以一直保持连接,通过Socket通道传输数据,节省掉了建立连接需要耗费的时间。

从服务器并发数的角度考虑

服务端要同时维持大量连接处于打开状态,就需要能以低性能开销接收高并发数据的架构。此类架构通常是围绕线程或所谓的非阻塞 IO 而设计的。这就与传统服务器围绕 HTTP 请求/响应循环的设计不同。这个时候,我们就会想到nodejs,使用事件机制和异步IO对请求进行处理,提高了服务器的并发能力,并且减少了线程切换带来的开销。

Java曾引入一个新的I/O API,其被称为非阻塞式的I/O。这一API使用一个选择器来避免每次有新的HTTP连接在服务器端建立时都要绑定一个线程的做法,当有数据到来时,就会有一个事件被接收,接着某个线程就被分配来处理该请求。因此,这种做法被称为每个请求一个线程(thread-per-request)模式。其允许web服务器,比如说WebSphere和Jetty等,使用固定数量的线程来容纳并处理越来越多的用户连接。在相同硬件配置的情况下,在这一模式下运行的web服务器的伸缩性要比运行在每个连接一个线程(thread-per-connection)模型下的好得多。

每个连接一个线程模式通常会有一个更好的响应时间,因为所有的线程都已启动、准备好且是等待中,但在连接的数目过高时,其会停止提供服务。在每个请求一个线程模式中,线程被用来为到达的请求提供服务,连接则是通过一个NIO选择器来处理。响应时间可能会较慢一些,但线程会回收再用,因此该方案在大容量连接方面有着更好的伸缩性。

而WebSocket对于服务端的优势就在于Socket减少了数据传输和处理的成本,使得这些异步的IO机制能够充分地扬长避短。

换句话说,WebSocket带来的并发能力提升,不仅仅因为传输机制本身,服务端一样需要做调整来适应新的机制,这样才能充分发挥WebSocket的优势。

Socket.io

Socket.IO是一个JavaScript端的框架, 提供了一个简单类似WebSocket的API,实现异步接收和发送服务端数据。Socket.io支持WebSocket,Flash Sockets,长轮询,流,持久帧(iframe)和JSONP轮询。具体使用哪个方案取决与浏览器的兼容性,尽可能使用最优方案解决。

具体可以上 Socket.io官网 看详细介绍。

发布了11 篇原创文章 · 获赞 30 · 访问量 10万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章