https://www.yduba.com/biancheng-7020924864.html
构造网络应用的过程中,我们经常需要与服务器进行持续的通讯以保持双方信息的同步。通常这种持久通讯在不刷新页面的情况下进行,消耗一定的内在资源常驻后台,并且对于用户不可见。比如:一个问答系统,当用户发表问题后,如果有其它用户回答,则提问者应该在不刷新页面的情况下,及时收到这个回答的内容。对于这样的需求,我们一般常用的解决方案是短轮询、长轮询、服务器推送、websocket。

短轮询

短轮询是一种常见的使用方法,这种方法实现起来比较简单,就是客户端每隔一段时间去服务器请求一次。如果有新的数据返回,就刷新内容。而在服务端收到请求后,不管是否有新数据,都直接响应 http 请求。这种方法,对于服务器端,不需要有什么特别的要求。对于客户端,一般采用 setInterval 或 setTimeout 实现。
服务端代码

  1. <?php
  2. header('Content-type: application/json; charset=UTF-8');
  3. // 从数据库里查看,是不是有新回答(为了提高效率,可以采用 nosql 数据库 ),这里不做具体的实现
  4. $data = array();
  5. // 如果没有新回答,则返回空
  6. if( !is_array($data) || empty($data) ) exit(json_encode( array('errcode'=>1, 'errmsg'=>'error') ));
  7. // 如果有新的回答,则返回数据
  8. exit(json_encode( array('errcode'=>0, 'errmsg'=>'success', 'data'=>$data) ));
  9. ?>

客户端代码

  1. //循环执行
  2. setInterval(function() {
  3. $.post("http://www.study.com/index.php", function(data, status)
  4. {
  5. console.log(data);
  6. if( data.errcode != '1' )
  7. {
  8. // 刷新页面内容
  9. }
  10. }, 'json');
  11. }, 10000);

这个程序会每隔10秒向服务器请求一次数据,如果请求后有新的内容,则去刷新页面。这个实现方法通常可以满足简单的需求,然而同时也存在着很大的缺陷:网络情况不稳定的情况下,服务器从接收请求、发送数据到客户端的总时间有可能会超过10秒,而客户端是每隔10秒请求一次的,所以,这样会导致接收数据的顺序和发送顺序不一致,第二次请求的结果可能会比每一次请求的结果还要先拿到。针对这种情况,我们可以采用 setTimeout 的轮询方式:

  1. function poll()
  2. {
  3. //setTimeout只会执行一次
  4. setTimeout(function()
  5. {
  6. $.get("http://www.study.com/index.php", function(data, status)
  7. {
  8. console.log(data);
  9. // 有返回之后,发起下一次请求
  10. poll();
  11. });
  12. }, 10000);
  13. }
  14. poll();

程序首先会设置 10 秒后发起请求,当数据返回后,再隔10秒发起第二次请求,以此类推。这样的话虽然无法保证两次请求之间的时间间隔为固定值,但是可以保证到达数据的顺序。

长轮询

短轮询方式存在一个严重缺陷:程序在每次请求时都会新建一个HTTP请求,然而并不是每次都能返回所需的新数据。当同时发起的请求达到一定数目时,会对服务器造成较大负担。这时我们可以采用长轮询方式解决这个问题。
长轮询和短轮询原理是一样的,只不过在服务器端收到请求后,如果没有数据,不是马上响应;而是停留一段时间,执行循环操作,直到有新的数据或请求超时;才会返回,结束这一次请求。

服务端代码

  1. <?php
  2. // 关闭脚本最大执行时间 ,这样可以保证代码一直运行
  3. set_time_limit(0);
  4. header('Content-type: application/json; charset=UTF-8');
  5. $data = array();
  6. while( ! $data )
  7. {
  8. // 这里从数据库查询,是不是有新的回答( 注意:千万不能直接不停的去操作 mysql
  9. // ,如果不停的操作查询,数据库会疯了。可以采用 nosql 或 缓存,或 redis发布订阅等等 )
  10. $data = array();
  11. // 每次查询数据库的时间间隔是10秒
  12. if( ! $data ) sleep(10);
  13. }
  14. // 如果有新的回答,则返回数据
  15. exit(json_encode( array('errcode'=>0, 'errmsg'=>'success', 'data'=>$data) ));
  16. ?>

客户端代码

  1. function longPoll ()
  2. {
  3. var _timestamp;
  4. $.get("http://www.study.com/index.php")
  5. .done(function(res) {
  6. try {
  7. console.log(res);
  8. } catch (e) {}
  9. })
  10. .always(function() {
  11. setTimeout(function()
  12. {
  13. longPoll();
  14. }, 10000);
  15. });
  16. }
  17. longPoll();

由以上两个程序可以看出,长轮询是在服务器端的停留,而短轮询是在浏览器端的停留。长轮询可以减少请求次数,有效地解决短轮询带来的带宽浪费,但是每次连接的保持是以消耗服务器资源为代价的。所以,不管是长轮询还是短轮询,都不太适用于客户端数量太多的情况,因为每个服务器所能承载的TCP连接数是有上限的,这种轮询很容易把连接数顶满;

其它例子:

这里使用AJAX请求data.php页面获得‘success’的值,请求的时间达到80秒。在这80秒中若没有从服务端返回‘success’则一直保持连接状态,直到有数据返回或‘success’的值为0才关闭连接。在关闭连接后在继续下一次的请求。
index.html

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  5. <script type="text/javascript" src="http://s1.hqbcdn.com/??lib/jquery/jquery-1.7.2.min.js"></script>
  6. </head>
  7. <body>
  8. <div id="msg"></div>
  9. <input id="btn" type="button" value="测试" />
  10. <script type="text/javascript" >
  11. $(function(){
  12. $("#btn").bind("click",{btn:$("#btn")},function(evdata){
  13. $.ajax({
  14. type:"POST",
  15. dataType:"json",
  16. url:"data.php",
  17. timeout:80000, //ajax请求超时时间80秒
  18. data:{time:"40"}, //40秒后无论结果服务器都返回数据
  19. success:function(data,textStatus){
  20. //从服务器得到数据,显示数据并继续查询
  21. if(data.success=="1"){
  22. $("#msg").append("<br>[有数据]"+data.text);
  23. evdata.data.btn.click();
  24. }
  25. //未从服务器得到数据,继续查询
  26. if(data.success=="0"){
  27. $("#msg").append("<br>[无数据]");
  28. evdata.data.btn.click();
  29. }
  30. },
  31. //Ajax请求超时,继续查询
  32. error:function(XMLHttpRequest,textStatus,errorThrown){
  33. if(textStatus=="timeout"){
  34. $("#msg").append("<br>[超时]");
  35. evdata.data.btn.click();
  36. }
  37. }
  38. });
  39. });
  40. });
  41. </script>
  42. </body>
  43. </html>

在这里是无限的循环,循环的结束条件就是获取到了返回结果返回Json数据。
并且接受$_POST[‘time’]参数来限制循环的超时时间,避免资源的过度浪费。(浏览器关闭不会发消息给服务器,使用可能一直循环下去)

data.php

  1. <?php
  2. if(empty($_POST['time']))exit();
  3. set_time_limit(0);//无限请求超时时间
  4. $i=0;
  5. while (true){
  6. sleep(1); //延迟一秒
  7. $i++;
  8. //若得到数据则马上返回数据给客服端,并结束本次请求
  9. $rand=rand(1,999);
  10. if($rand<=15){
  11. $arr=array('status'=>"1",'name'=>'success','text'=>$rand);
  12. echo json_encode($arr);
  13. exit();
  14. }
  15. //到指定超时时间还未返回数据则断开连接
  16. if($i==$_POST['time']){
  17. $arr=array('status'=>"0",'name'=>'error','text'=>'无数据');
  18. echo json_encode($arr);
  19. exit();
  20. }
  21. }

服务器发送事件 SSE

服务器发送事件(以下简称SSE)是HTML 5规范的一个组成部分,可以实现服务器到客户端的单向数据通信。通过SSE,客户端可以自动获取数据更新,而不用重复发送HTTP请求。一旦连接建立,“事件”便会自动被推送到客户端。服务器端SSE通过“事件流(Event Stream)”的格式产生并推送事件。事件流对应的MIME类型为“text/event-stream”,包含四个字段:event、data、id和retry。event表示事件类型,data表示消息内容,id用于设置客户端EventSource对象的“last event ID string”内部属性,retry指定了重新连接的时间。

服务端代码

  1. <?php
  2. header("Content-Type: text/event-stream");
  3. header("Cache-Control: no-cache");
  4. $time = date("r");
  5. echo "event: ping\n";
  6. echo "retry: 3000\n"; // 表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间
  7. echo "data: The server time is: {$time}\n\n";
  8. 客户端代码
  9. var eventSource = new EventSource("http://www.study.com/index.php");
  10. eventSource.addEventListener("ping", function(e)
  11. {
  12. console.log(e)
  13. }, false);

SSE相较于轮询具有较好的实时性,使用方法也非常简便。然而SSE只支持服务器到客户端单向的事件推送,而且所有版本的IE(包括到目前为止的Microsoft Edge)都不支持SSE。如果需要强行支持IE和部分移动端浏览器,可以尝试EventSource Polyfill(本质上仍然是轮询)。SSE的浏览器支持情况如下图所示:
传统轮询、长轮询、服务器发送事件与WebSocket - 图1

WebSocket

WebSocket同样是HTML 5规范的组成部分之一,现标准版本为RFC 6455。WebSocket相较于上述几种连接方式,实现原理较为复杂。
用一句话概括就是:客户端向WebSocket服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。由于WebSocket本身是基于TCP协议的,所以在服务器端我们可以采用构建TCP Socket服务器的方式来构建WebSocket服务器。

服务端代码

  1. <?php
  2. $serv = new Swoole\Websocket\Server("127.0.0.1", 9501);
  3. $serv->on('Open', function($server, $req)
  4. {
  5. echo "connection open: ".$req->fd;
  6. });
  7. $serv->on('Message', function($server, $frame)
  8. {
  9. echo "message: ".$frame->data;
  10. $server->push($frame->fd, json_encode(["hello", "world"]));
  11. });
  12. $serv->on('Close', function($server, $fd)
  13. {
  14. echo "connection close: ".$fd;
  15. });
  16. $serv->start();

这段代码是采用了 swoole,请在运行前确保安装了 swoole,并在 cli 环境下运行

客户端代码**

  1. var url='ws://127.0.0.1:9501';
  2. socket=new WebSocket(url);
  3. socket.onopen=function()
  4. {
  5. socket.send('type=add&ming=hello');
  6. }
  7. socket.onmessage=function(msg)
  8. {
  9. console.log( msg )
  10. }
  11. socket.onclose= function()
  12. {
  13. console.log("退出了")
  14. }

WebSocket同样具有实时性,每次通讯无需重发请求头部,节省带宽,而且它的浏览器支持非常好(详见下图)。
传统轮询、长轮询、服务器发送事件与WebSocket - 图2

总结

四种通信方式的优缺点

短轮询 长轮询 服务器发送事件 WebSocket
浏览器支持 几乎所有现代浏览器 几乎所有现代浏览器 Firefox 6+
Chrome 6+
Safari 5+
Opera 10.1+
IE 10+ Edge Firefox 4+
Chrome 4+
Safari 5+
Opera 11.5+
服务器负载 较少的CPU资源,较多的内存资源和带宽资源 与短轮询相似,但是占用带宽较少 与长轮询相似,除非每次发送请求后服务器不需要断开连接 无需循环等待(长轮询),CPU和内存资源不以客户端数量衡量,而是以客户端事件数衡量。四种方式里性能最佳。
客户端负载 占用较多的内存资源与请求数 与传统轮询相似 浏览器中原生实现,占用资源很小 同服务器发送事件
延迟 非实时,延迟取决于请求间隔 同短轮询 非实时,默认3秒延迟,延迟可自定义 实时
实现复杂度 非常简单 需要服务器配合,客户端实现非常简单 需要服务器配合,而客户端实现甚至比前两种更简单 需要Socket程序实现和额外端口,客户端实现简单