websocket

一、低延迟的客户端-服务器 和 服务器-客户端的连接

很多时候所谓的http的请求、响应的模式,都是客户端加载一个网页,直到用户在进行下一次点击的时候,什么都不会发生。并且所有的http的通信都是客户端控制的,这时候就需要用户的互动或定期轮训的,以便从服务器端加载新的数据。

  通常采用的技术比如推送和comet(使用http长连接、无需安装浏览器安装插件的两种方式:基于ajax的长轮询;基于iframe及html的流方式);最普通的一种手段是对服务器发起创建的连接创建假象,也称之为长连接。利用长轮询,客户端可以打开指向的服务器的http连接;服务器就会一直保持连接打开,直到发送响应。服务器只要有新数据,就发送响应,其他技术比如:flash、xhr multipart请求以及htmlfile。长轮询的使用场景还是蛮多的;但是这些方案都有一个通病:带有http的开销,致使他们不适合低延迟应用。

关于长连接和长轮询

1、基于http的长连接是通过长轮询的方式实现服务器推的技术,用来弥补http的请求响应模式的不足,增强程序的及时和交互性。通俗的话说就是客户端不同的向服务器端不停的发送请求以获取最新的数据。不过所谓的不停其实只是个假象,不过这个我们肉眼并不能分辨(用快速停下来然后又不停连接而已)。

一般长连接和长轮询用于一些需要及时交互的网站应用中。 DWR 反向Ajax 服务器端推的方式:http://www.cnblogs.com/hoojo/category/276235.html

 2、轮询:客户端定时向服务器发送ajax请求,一旦服务器接到请求并响应将立马关闭连接

      这样的话后台的代码编写就比较简单,不过多半请求都是无用的,浪费带宽和服务器资源;这样的方式只适合小型的应用

    长轮询:客户端向服务器发送ajax请求,服务器端接到请求之后要一直保持连接。直到有新数据才返回响应内容,并关闭连接,客户端处理完响应之后再向服务器发送请求,重复前面的操作;这样可以在无新的信息不会频繁请求,损耗资源较小;不过让服务端一直保持连接,返回数据的顺序是没有保证的,并难以管理维护;

     长连接:在页面中嵌入一个隐藏的iframe并src的属性设为一个长连接的请求或者xhr请求;服务器端就会不断的往客户端输入信息;这样保证内容及时到达,不发无用的请求,也方便管理。与此同时增加了服务器维护一个长连接的开销。

     还有就是flash socket 因为需要在客户端安装flash插件,也不是后http并不能自动穿越防火墙在这里就不多叙述。感兴趣:http://www.ibm.com/developerworks/cn/web/wa-lo-comet/

   一般长连接需要客户端和服务器同时配合方可实现;在服务器端加入一个死循环;再循环中监测数据的变动。发现新数据,立即输出到客户端并断开连接;接受完数据之后,将会再次发起第二次请求,已进入下一个周期,这种方式也称之为长轮询。一般经过如下几步

  1、轮询建立 客户端发送请求之后进去循环等待;此时服务器未应答的话,将会一直处于连接状态

  2、数据推送:服务器端通过在死循环中监控数据的变更,如有变更,则立即响应给客户端,并随即断开连接,已完成应答,实现所谓的服务器推

  3、轮询终止:1、有新数据推送(要主动断开连接,是客户端及时收到信息)  2、没有数据推送(应该设置最长时限,避免服务器的超时。倘若一直没有新数据,服务端要主动给客户端响应,并断开连接,这个称之为“心跳”) 3、网络自身的问题(故障或异常)这是客户端会受到错误信息 4、轮询重建  接到响应之后进行处理,在重新发起新的请求,开始新的轮询。

  ajax轮询方式

  

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="pragma" content="no-cache">
        <meta http-equiv="cache-control" content="no-cache">
        <meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <%@ include file="/tags/jquery-lib.jsp"%>
        
        <script type="text/javascript">
            $(function () {
            
                window.setInterval(function () {
                
                    $.get("${pageContext.request.contextPath}/communication/user/ajax.mvc", 
                        {"timed": new Date().getTime()}, 
                        function (data) {
                            $("#logs").append("[data: " + data + " ]<br/>");
                    });
                }, 3000);
                
            });
        </script>
    </head>
    
    <body>
        <div id="logs"></div>
    </body>
</html>
客户端实现的就是用一种普通轮询的结果,比较简单。利用setInterval不间断的刷新来获取服务器的资源,这种方式的优点就是简单、及时。缺点是链接多数是无效重复的;响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了);请求多,难于维护、浪费服务器和网络资源。
@RequestMapping("/ajax")
public void ajax(long timed, HttpServletResponse response) throws Exception {
     PrintWriter writer = response.getWriter();
     
     Random rand = new Random();
     // 死循环 查询有无数据变化
     while (true) {
         Thread.sleep(300); // 休眠300毫秒,模拟处理业务等
         int i = rand.nextInt(100); // 产生一个0-100之间的随机数
         if (i > 20 && i < 56) { // 如果随机数在20-56之间就视为有效数据,模拟数据发生变化
             long responseTime = System.currentTimeMillis();
             // 返回数据信息,请求时间、返回数据时间、耗时
             writer.print("result: " + i + ", response time: " + responseTime + ", request time: " + timed + ", use time: " + (responseTime - timed));
             break; // 跳出循环,返回数据
         } else { // 模拟没有数据变化,将休眠 hold住连接
             Thread.sleep(1300);
         }
     }
     
}
服务器端实现,这里就模拟下程序监控数据的变化。上面代码属于SpringMVC 中controller中的一个方法,相当于Servlet中的一个doPost/doGet方法。如果没有程序环境适应servlet即可,将方法体中的代码copy到servlet的doGet/doPost中即可。

服务器端在进行长连接的程序设计时,要注意以下几点: 
1. 服务器程序对轮询的可控性 
由于轮询是用死循环的方式实现的,所以在算法上要保证程序对何时退出循环有完全的控制能力,避免进入死循环而耗尽服务器资源。 
2. 合理选择“心跳”频率 
从图1可以看出,长连接必须由客户端不停地进行请求来维持,所以在客户端和服务器间保持正常的“心跳”至为关键,参数POLLING_LIFE应小于WEB服务器的超时时间,一般建议在10~20秒左右。 
3. 网络因素的影响 
在实际应用时,从服务器做出应答,到下一次循环的建立,是有时间延迟的,延迟时间的长短受网络传输等多种因素影响,在这段时间内,长连接处于暂时断开的空档,如果恰好有数据在这段时间内发生变动,服务器是无法立即进行推送的,所以,在算法设计上要注意解决由于延迟可能造成的数据丢失问题。 
4. 服务器的性能 
在长连接应用中,服务器与每个客户端实例都保持一个持久的连接,这将消耗大量服务器资源,特别是在一些大型应用系统中更是如此,大量并发的长连接有可能导致新的请求被阻塞甚至系统崩溃,所以,在进行程序设计时应特别注意算法的优化和改进,必要时还需要考虑服务器的负载均衡和集群技术。

iframe方式

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="pragma" content="no-cache">
        <meta http-equiv="cache-control" content="no-cache">
        <meta http-equiv="expires" content="0">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <%@ include file="/tags/jquery-lib.jsp"%>
        
        <script type="text/javascript">
            $(function () {
            
                window.setInterval(function () {
                    $("#logs").append("[data: " + $($("#frame").get(0).contentDocument).find("body").text() + " ]<br/>");
                    $("#frame").attr("src", "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime());
                    // 延迟1秒再重新请求
                    window.setTimeout(function () {
                        window.frames["polling"].location.reload();
                    }, 1000);
                }, 5000);
                
            });
        </script>
    </head>
    
    <body>
        <iframe id="frame" name="polling" style="display: none;"></iframe>
        <div id="logs"></div>
    </body>
</html>

 长连接iframe方式

   

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="pragma" content="no-cache">
        <meta http-equiv="cache-control" content="no-cache">
        <meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <%@ include file="/tags/jquery-lib.jsp"%>
        
        <script type="text/javascript">
            $(function () {
            
                window.setInterval(function () {
                    var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();
                    var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');
                    $("body").append($iframe);
                
                    $iframe.load(function () {
                        $("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");
                        $iframe.remove();
                    });
                }, 5000);
                
            });
        </script>
    </head>
    
    <body>
        
        <div id="logs"></div>
    </body>
</html>
客户端程序是利用隐藏的iframe向服务器端不停的拉取数据,将iframe获取后的数据填充到页面中即可。同ajax实现的基本原理一样,唯一不同的是当一个请求没有响应返回数据的情况下,下一个请求也将开始,这时候前面的请求将被停止。如果要使程序和上面的ajax请求一样也可以办到,那就是给每个请求分配一个独立的iframe即可。
如果要保证有序,可以不使用setInterval,将创建iframe的方法放在load事件中即可,即使用递归方式。调整后的代码片段如下:

<script type="text/javascript">
    $(function () {
        (function iframePolling() {
            var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();
            var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');
            $("body").append($iframe);
        
            $iframe.load(function () {
                $("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");
                $iframe.remove();
                
                // 递归
                iframePolling();
            });
        })();    
    });
</script>
这种方式虽然保证了请求的顺序,但是它不会处理请求延时的错误或是说很长时间没有返回结果的请求,它会一直等到返回请求后才能创建下一个iframe请求,总会和服务器保持一个连接。和以上轮询比较,缺点就是消息不及时,但保证了请求的顺序。
ajax实现长连接

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="pragma" content="no-cache">
        <meta http-equiv="cache-control" content="no-cache">
        <meta http-equiv="expires" content="0">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <%@ include file="/tags/jquery-lib.jsp"%>
        
        <script type="text/javascript">
            $(function () {
            
                (function longPolling() {
                
                    $.ajax({
                        url: "${pageContext.request.contextPath}/communication/user/ajax.mvc",
                        data: {"timed": new Date().getTime()},
                        dataType: "text",
                        timeout: 5000,
                        error: function (XMLHttpRequest, textStatus, errorThrown) {
                            $("#state").append("[state: " + textStatus + ", error: " + errorThrown + " ]<br/>");
                            if (textStatus == "timeout") { // 请求超时
                                    longPolling(); // 递归调用
                                
                                // 其他错误,如网络错误等
                                } else { 
                                    longPolling();
                                }
                            },
                        success: function (data, textStatus) {
                            $("#state").append("[state: " + textStatus + ", data: { " + data + "} ]<br/>");
                            
                            if (textStatus == "success") { // 请求成功
                                longPolling();
                            }
                        }
                    });
                })();
                
            });
        </script>
    </head>
    
    <body>

 html5 websocket

   websocket规范定义了可在网络浏览器和服务器之间建立套接字连接;通俗说就是客户端和服务器端保持持久连接,双方可以随时发送信息。

   一般使用

   1、创建websocket

    var conn = new Websocket('ws://websocket地址',['soap','xmpp']);说明ws使用websocket使用的一种协议;对应安全的websocket的采用 wss类似https;第二个参数可接受的子协议字符串或者字符串数组;但是服务器端只接受其中一个协议,有websocket对象中的protocol属性决定

   2、添加打开连接、接受信息、出现异常的操作

      // 打开连接

      conn.onopen = function (){conn.send('ping');}

      // 出错信息 

      conn.onerror = function (error){console.log('websocket error ' + error)}

     // 接受信息

      conn.onmessage = function (e){console.log('data == ' + e.data)};

 与服务器的通信

   客户端执行onopen事件之后,通过send方法向服务器发送数据(数据可以采用字符串和二进制两种方式,二进制使用 Blob 或者 ArrayBuffer对象)

// Sending String
connection.send('your message');

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
connection.send(binary.buffer);

// Sending file as Blob
var file = document.querySelector('input[type="file"]').files[0];
connection.send(file);

 一旦服务器端要返回数据,就会通过onmessage回调方法;接受对象为事件对象,实际的数据是在data属性中;注意在使用二进制时 binaryType属性设为blob和arraybuffer 默认为blob也意味着在发送时调整binaryType的参数

// Setting binaryType to accept received binary as either 'blob' or 'arraybuffer'
connection.binaryType = 'arraybuffer';
connection.onmessage = function(e) {
  console.log(e.data.byteLength); // ArrayBuffer object if binary
};

WebSocket 的另一个新增功能是扩展。利用扩展,可以发送压缩帧、多路复用帧等。您可以检查 open 事件后的 WebSocket 对象的 extensions 属性,查找服务器所接受的扩展
console.log(connection.extensions);

    

跨源通信

作为现代协议,跨源通信已内置在 WebSocket 中。虽然您仍然应该确保只与自己信任的客户端和服务器通信,但 WebSocket 可实现任何域上多方之间的通信。服务器将决定是向所有客户端,还是只向驻留在一组指定域上的客户端提供服务。

代理服务器

每一种新技术在出现时,都会伴随着一系列问题。WebSocket 也不例外,它与大多数公司网络中用于调解 HTTP 连接的代理服务器不兼容。WebSocket 协议使用 HTTP 升级系统(通常用于 HTTP/SSL)将 HTTP 连接“升级”为 WebSocket 连接。某些代理服务器不支持这种升级,并会断开连接。因此,即使指定的客户端使用了 WebSocket 协议,可能也无法建立连接。这就使得下一部分的内容更加重要了。