Websocket消息推送

# Websocket消息推送

背景

HTTP协议特点

消息推送方案

Websocket简介

Websocket实例

# 一、背景

HTTP协议的无状态和被动性,使得B/S架构的服务器主动推送消息给浏览器比较困难,而通用的一些解决方案又有各种各样的问题,比如:ajax轮询会有很多无用的请求,浪费宽带;基于Flash的消息推送又有Flash支持不好,无法自动穿越防火墙等问题......

Websocket就是在这种情况下出现的一个协议。

# 二、HTTP协议特点

B/S架构的系统多使用HTTP协议,HTTP协议的特点:

1、简单快速

客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

2、灵活

HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

3、无状态

即无状态协议。这指的是,HTTP协议不对请求和响应之间的通信状态进行保存。所以使用HTTP协议,每当有新的请求发送,就会有对应的新响应产生。这样做的好处是更快地处理大量事务,确保协议的可伸缩性。

然而,随着时间的推移,人们发现静态的HTML着实无聊而乏味,增加动态生成的内容才会令Web应用程序变得更加有用。于是乎,HTML的语法在不断膨胀,其中最重要的是增加了表单(Form);客户端也增加了诸如脚本处理、DOM处理等功能;对于服务器,则相应的出现了CGI(Common Gateway Interface)以处理包含表单提交在内的动态请求。

在这种客户端与服务器进行动态交互的Web应用程序出现之后,HTTP无状态的特性严重阻碍了这些交互式应用程序的实现,毕竟交互是需要承前启后的,简单的购物车程序也要知道用户到底在之前选择了什么商品。于是,两种用于保持HTTP状态的技术就应运而生了,一个是Cookie,而另一个则是Session。

4、持久连接

HTTP协议初试版本中,每进行一次HTTP通信就要断开一次TCP连接。

早期这么做的原因是HTTP协议产生于互联网,因此服务器需要处理同时面向全世界数十万、上百万客户端的网页访问,但每个客户端(即浏览器)与服务器之间交换数据的间歇性较大(即传输具有突发性、瞬时性),并且网页浏览的联想性、发散性导致两次传送的数据关联性很低,如果按照上面的方式则需要在服务器端开的进程和句柄数目都是不可接受的,大部分通道实际上会很空闲、无端占用资源。因此HTTP的设计者有意利用这种特点将协议设计为请求时建连接、请求完释放连接,以尽快将资源释放出来服务其他客户端。

但是当浏览器请求一个包含多张图片的HTML页面时,会增加通信量的开销。为了解决这个问题,HTTP/1.1相处了持久连接(HTTP keep-alive)方法。其特点是,只要任意一端没有明确提出断开连接,则保持TCP连接状态,在请求首部字段中的Connection: keep-alive即为表明使用了持久连接。

这样一来,客户端和服务器之间的HTTP连接就会被保持,不会断开(超过Keep-Alive规定的时间,意外断电等情况除外),当客户端发送另外一个请求时,就使用这条已经建立的连接。

5、支持B/S及C/S模式

PS:这节内容多来自博文:http协议特点

# 三、消息推送方案

3.1、HTTP使得服务器无法主动推送消息 HTTP的生命周期通过Request来界定,也就是一个Request,一个Response,在HTTP1.0中,这次HTTP请求就结束了。

在HTTP1.1中进行了改进,添加了一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是在HTTP中永远是Request=Response,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

反映在日常生活中就是:

客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现。

这种机制对于信息变化不是特别频繁的应用可以良好支撑,但对于实时要求高、海量并发的应用来说显得捉襟见肘,在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是Web应用经常面临的问题,比如金融证券的实时信息、Web导航应用中的地理位置获取、社交网络的实时消息推送等。

3.2、实现服务器主动推送消息

有问题出现,就会有解决方案:

1.Ajax轮询

其原理简单易懂,就是客户端定时向服务器发送Ajax请求,询问服务器是否有新信息。

但它的问题也很明显:当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。

适于小型应用。

2.Flash Socket

AdobeFlash通过自己的Socket实现完成数据交换,再利用Flash暴露出相应的接口给JavaScript调用,从而达到实时传输目的。此方式比轮询要高效,且因为Flash安装率高,应用场景广泛。

但是移动互联网终端上Flash的支持并不好:IOS系统中无法支持Flash,Android虽然支持Flash但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012年Adobe官方宣布不再支持Android4.1+系统,宣告了Flash在移动终端上的死亡。

3.长轮询(long poll),长连接

keep-alive connection是指在一次TCP连接中完成多个HTTP请求,但是对每个请求仍然要单独发HTTP header;长轮询是指从客户端(一般就是浏览器)不断主动的向服务器发 HTTP 请求查询是否有新数据。这两种模式有一个共同的缺点,就是除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。它们建立的“长连接”都是伪.长连接,只不过好处是不需要对现有的HTTP server和浏览器架构做修改就能实现。

还有就是下面要说的Websocket。

PS:关于Ajax轮询,长轮询等解释来自知乎: https://www.zhihu.com/question/20215561

# 四、Websocket简介

4.1、Websocket是什么

WebSocket是HTML5下一种新的协议。

它是一个新的基于TCP的应用层协议,只需要一次连接,以后的数据不需要重新建立连接,可以直接发送,它是基于TCP的,属于和HTTP相同的地位。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

4.2、Websocket的特点

建立在 TCP 协议之上,服务器端的实现比较容易。

与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

数据格式比较轻量,性能开销小,通信高效。

可以发送文本,也可以发送二进制数据。

没有同源限制,客户端可以与任意服务器通信。

协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

4.3、Websocket的优势

是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。

HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。

# 五、Websocket实例

后台消息推送是很多系统中重要的功能,我现在工作的项目是一个网管项目,当网管服务器收到设备发过来的告警时,需要将告警信息推送到客户端,这里面就用到了消息推送。

之前网管的推送是用flash实现的,但flash经常出现被禁用等问题,导致客户端收不到服务器推送来的消息,加上很多浏览器对flash已经不再更新,了解了Websocket的优势后,于是将后台的消息推送用Websocket实现。

网管这边是用Jetty做服务器,比较新的Jetty也已经兼容了Websocket,所以就采用了Jetty的api实现。

5.1、前台代码

将通用的Websocket连接放到MyWebsocket.js中:

function MyWebSocket(serviceName, config) {
    this.serviceName = serviceName;
    this.config = config;

    this.connect();
}

MyWebSocket.prototype.connect = function() {
    var _me = this;
    var serverIp = location.hostname;
    var _config = _me.config;

    // 需要判断是否支持websocket,如果不支持,使用flash版本的
    if (typeof WebSocket != 'undefined') {
        initWebSocket();
        _me.supportWebSocket = true;
    } else {
        console.log("not support Websocket");
        _me.supportWebSocket = false;
        return;
    }

    function initWebSocket() {
        var url = 'ws://localhost:8080/example/websocket/' + _me.serviceName;

        var firstParam = true;
        if(_config.params) {
            for(var key in _config.params) {
                if(firstParam) {
                    url += '?' + key + '=' + _config.params[key];
                    firstParam = false;
                } else {
                    url += '&' + key + '=' + _config.params[key];
                }
            }
        }
        var socket = new WebSocket(url);
        _me.socket = socket;

        socket.onopen = function() {
            _config.onopen();

            // 开启心跳检测,以免一段时间后收不到消息自动失联
            heartbeat_timer = setInterval(function () {
                keepalive(socket)
            }, 10000);

            function keepalive(socket) {
                socket.send('~H#B~');
            }
        };

        socket.onmessage = function (message) {
            _config.onmessage(message.data);
        };

        socket.onclose = function() {
            _config.onclose();
            clearInterval(heartbeat_timer);
        };

        socket.onerror = function(err) {
            _config.onerror(err);
        };
    }
}

MyWebSocket.prototype.send = function(message) {
    if(this.supportWebSocket) {
        this.socket.send(JSON.stringify(message));
    } else {
        this.socket.sendRequest(this.serviceName, message, true);
    }
}

MyWebSocket.prototype.close = function() {
    if(this.supportWebSocket) {
        this.socket.close();
    } else {
        this.socket.disconnect();
    }
}

MyWebSocket.prototype.reconnect = function() {
    this.close();
    this.connect();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

然后在需要消息推送的页面引入该js,并做相应配置:

<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>消息推送</title>
    <style type="text/css">
        .greenTxt{ color:#008200;}
        .redTxt{ color:#820803; }
    </style>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="js/MyWebsocket.js"></script>
    <script>
        var ws;
        $(document).ready(function(){
            var $content = $("#content");
            window.GLOBAL_SOCKET_CONNECT_ID = random( 25 );
            ws = new MyWebSocket("messagePusher",{
                params: {
                    JCONNECTID : window.GLOBAL_SOCKET_CONNECT_ID,
                },
                onmessage: function (messagestr) {
                    var content = JSON.parse(messagestr);
                    $content.append("<pre>"+content.data+"</pre>");
                },
                onopen: function() {
                    $content.append("<p class='greenTxt'>SOCKET已连接上...</p>");
                },
                onclose: function() {
                    $content.append("<p class='redTxt'>SOCKET连接已中断!</p>");
                },
                onerror: function(err) {
                    $content.append("<pre>"+err+"</pre>");
                }
            });
        });

        function random(len) {
            len = len || 32;
            /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
            var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
            var $maxPos = $chars.length;
            var $data = '';
            for (i = 0; i < len; i++) {
                $data += $chars.charAt(Math.floor(Math.random() * $maxPos));
            }
            return $data;
        }
    </script>
</head>
    <body>
    <div id="content"></div>
    </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
上次更新: 2022/8/19 下午5:52:22