打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
WEB即时通讯/消息推送

写在前面

通常进行的Web开发都是由客户端主动发起的请求,然后服务器对请求做出响应并返回给客户端。但是在很多情况下,你也许会希望由服务器主动对客户端发送一些数据。

那么,该如何实现这个需求,或者说该如何向网页推送消息呢?

一、推送方式


我们知道,HTTP/HTTPS协议是被设计基于“请求-相应”模型的,尽管HTTP/HTTPS可以在任何互联网协议或网络上实现,但这里我们只讨论在Internet网上的万维网中的情况。

由于在Internet中,HTTP协议在传输层使用的是TCP协议。由此可知,只要我们能保持TCP连接不随一次“请求-响应”结束而结束,使得服务器可以主动发送数据,那么我们就能够实现向网页的数据推送。事与愿违,在2011年WebSocket(详见下文)出现之前我们对此是无能为力的。

不过,在那时虽然不能直接实现推送,但是还是有曲线救国路线的,基本上有4类这种间接方式。当然现在我们还有了1种直接方式-WebSocket ,接下来我来依次介绍下。


模拟推送

1. 轮询(Polling)

AJAX 定时(可以使用JS的 setTimeout 函数)去服务器查询是否有新消息,从而进行增量式的更新。这种方式间隔多长时间再查询是个问题,因为性能和即时性是反比关系。间隔太短,海量的请求会拖垮服务器,间隔太长,服务器上的新数据就需要更长的时间才能到达客户机。

  • 优点:服务端逻辑简单;
  • 缺点:大多数请求是无效请求,在轮询很频繁的情况下对服务器的压力很大;

所以,除了一些简单练习项目外,这种方式不能被用于生产。


Comet

2和3属于:Comet (web技术),是广大开发者想出来的比较可行的推送技术。

2. 长轮询(Long-Polling)

客户端向服务器发送AJAX请求,服务器接到请求后hold住连接,直到有新消息或超时(设置)才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。

  • 优点:任意浏览器都可用;实时性好,无消息的情况下不会进行频繁的请求;
  • 缺点:连接创建销毁操作还是比较频繁,服务器维持着连接比较消耗资源;

微信网页版使用的就是这种方式,据我观察:

  • 微信把25秒作为超时时间;
  • 用两个请求来完成长轮询,一个用于25秒超时获取是否有新消息,当有新消息时会用另一个AJAX请求来获取具体数据。

这种方式是可以被用于生产的,并且已经被实践检验有比较高的可用性。

3. 基于iframe的方式

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 src 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。

iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如<script type="text/javascript">js_func("data from server")</script>。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。

每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

  • 优点:消息能够实时到达;
  • 缺点:使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行;

Google公司在一些产品中使用了iframe流,如Google Talk。


局限性方式

4. 插件提供的Socket方式

利用Flash XMLSocket,Java Applet套接口,Activex包装的socket。

  • 优点:原生socket的支持,和PC端和移动端的实现方式相似;
  • 缺点:浏览器端需要装相应的插件;

5. WebSocket

2011年,WebSocket被IETF定为标准RFC 6455,WebSocket API也被W3C定为标准。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket自然是极好的,更多细节我在下一节详细说明。


到这里,我们已经对WEB上的消息推送机制有了一个整体的了解。不过,仅仅只有了解对于我们来说显然还不够,由于我是Java程序员,接下来我将继续介绍WebSocket,并且用Java做服务端来做一个例子。


二、WebSocket

WebSocket 是独立的、创建在 TCP 上的协议。Websocket 通过 HTTP/1.1 协议的101状态码进行握手。为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。

1. ws请求

一个典型的WebSocket请求如下:

GET wss://xxx.xxx.com/push/ HTTP/1.1Host: xxx.xxx.com:portConnection:UpgradeUpgrade:websocketSec-WebSocket-Extensions:permessage-deflate; client_max_window_bitsSec-WebSocket-Key:rZGX8zZKTrdkhIJTCuW54Q==Sec-WebSocket-Version:13// Connection必须为:Upgrade,表示client希望升级连接;// Upgrade必须为:websocket,表示client希望升级到Websocket协议;// Sec-WebSocket-Key:是随机字符串,服务端会将其做一定运算,最后在Response中返回“Sec-WebSocket-Accept”头的值。用于避免普通http请求被当做WebSocket协议。// Sec-WebSocket-Version:表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当被弃用。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

响应如下:

HTTP/1.1 101 Switching ProtocolsUpgrade:websocketConnection:upgradeSec-WebSocket-Accept:QJsTRym36zHnArQ7FCmSdPhuK78=// Connection:upgrade 升级被服务器同意// Upgrade:websocket 指示客户端升级到websocket// Sec-WebSocket-Accept:参考上面请求的Sec-WebSocket-Key的注释
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面只是比较重要的点,其实只知道这些暂时就够了,更详细的细节请参看:
RFC 6455 WebSocket
wikipedia WebSocket

2. WebSocket在Java中

JavaEE 7的JSR-356:Java API for WebSocket,已经对WebSocket做了支持。不少Web容器,如Tomcat、Jetty等都支持WebSocket。Tomcat从7.0.27开始支持WebSocket,从7.0.47开始支持JSR-356。

但是如果使用Java EE的WebSocket API的话,还有很多自己需要封装的地方。所以接下来我要说的并不是Java官方的API,而是目前正在接触的一种推送框架:Socket.IO以及其Server端的Java实现netty-socketio。这个框架不仅支持WebSocket,还支持Long-Polling模式。

注意Socket.IO并不是一个标准的WebSocket的实现,只是说Socket.IO使用并很好的支持了WebSocket协议而已。

下面就说一下这两个框架。

3. SOCKET.IO

Socket.IO enables real-time bidirectional event-based communication. It consists in:

SOCKET.IO - 官网地址
SOCKET.IO - github地址

由于其Server端是用Node.js实现的,又没有提供Java版本的Server,所以我找到了一个比较流行的第三方实现:netty-socketio。

4. netty-socketio

netty-socketio - github地址

This project is an open-source Java implementation of Socket.IO server. Based on Netty server framework.

netty-socketio是一个开源的Socket.IO Server的Java实现,基于Netty。

接下来我就使用netty-socketio来做一个demo。


三、netty-socketio实例

建议先大致读一下Socket.IO和netty-socketio的官方网站相关信息,以有个整体的概念,然后再做Demo,我就不把那些搬过来了。

Socket.IO中的一些重要概念。

  1. Server:代表一个服务端服务器;

  2. Namespace:一个Server中可以包含多个Namespace。见名知意,Namespace代表一个个独立的空间。

  3. Socket/Client:基本上这两个词是一个概念。

    • JavaScript客户端叫Socket,在创建时必须确定加入哪个Namespace,使用Socket可以让你和服务器通信。注意这个和伯克利Socket是不同的,只是开发者借用了一样的名字、功能相似。
    • Java服务端用Client来表示连接上服务器的链接,它就代表了JavaScript连接时创建的那个Socket
  4. room:在服务端,一个Namespace中你可以创建任意个房间,房间就是给Client进行分组,以进行组范围的通信。Client可以选择加入某个房间,也可以不加入。

代码实例:两个Namespace,广播通讯。

  1. Java服务端

    public static void main(String[] args) throws InterruptedException {Configuration config = new Configuration();config.setHostname("localhost");config.setPort(9092);// 可重用地址,防止处于重启时处于TIME_WAIT的TCP影响服务启动final SocketConfig socketConfig = new SocketConfig();socketConfig.setReuseAddress(true);config.setSocketConfig(socketConfig);final SocketIOServer server = new SocketIOServer(config);final SocketIONamespace chat1namespace = server.addNamespace("/chat1");chat1namespace.addEventListener("message", ChatObject.class, new DataListener<ChatObject>() {    @Override    public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {        // broadcast messages to all clients        chat1namespace.getBroadcastOperations().sendEvent("message", data);    }});final SocketIONamespace chat2namespace = server.addNamespace("/chat2");chat2namespace.addEventListener("message", ChatObject.class, new DataListener<ChatObject>() {    @Override    public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {        // broadcast messages to all clients        chat2namespace.getBroadcastOperations().sendEvent("message", data);    }});server.start();Thread.sleep(Integer.MAX_VALUE);server.stop();}
    • 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
  2. JS客户端

    引用到的JS文件:

    js文件github下载页面
    时间格式化JS

    <!DOCTYPE html><html><head>    <title>Demo Chat</title>    <link href="bootstrap.css" rel="stylesheet"><style>body {    padding: 20px;}.console {    height: 400px;    overflow: auto;}.username-msg {    color: orange;}.connect-msg {    color: green;}.disconnect-msg {    color: red;}.send-msg {    color: #888}</style><script src="js/socket.io/socket.io.js"></script><script src="js/moment.min.js"></script><script src="//code.jquery.com/jquery-1.10.1.min.js"></script><script>    var userName1 = 'user1_' + Math.floor((Math.random() * 1000) + 1);    var userName2 = 'user2_' + Math.floor((Math.random() * 1000) + 1);    var chat1Socket = io.connect('http://localhost:9092/chat1');    var chat2Socket = io.connect('http://localhost:9092/chat2');    function connectHandler(parentId) {        return function() {            output('<span class="connect-msg">Client has connected to the server!</span>', parentId);        }    }    function messageHandler(parentId) {        return function(data) {            output('<span class="username-msg">' + data.userName + ':</span> '                    + data.message, parentId);        }    }    function disconnectHandler(parentId) {        return function() {            output('<span class="disconnect-msg">The client has disconnected!</span>', parentId);        }    }    function sendMessageHandler(parentId, userName, chatSocket) {        var message = $(parentId + ' .msg').val();        $(parentId + ' .msg').val('');        var jsonObject = {'@class': 'com.ddupa.service.push.model.ChatObject',                userName: userName,                message: message};        chatSocket.json.send(jsonObject);    }    chat1Socket.on('connect', connectHandler('#chat1'));    chat2Socket.on('connect', connectHandler('#chat2'));    chat1Socket.on('message', messageHandler('#chat1'));    chat2Socket.on('message', messageHandler('#chat2'));    chat1Socket.on('disconnect', disconnectHandler('#chat1'));    chat2Socket.on('disconnect', disconnectHandler('#chat2'));    function sendDisconnect1() {        chat1Socket.disconnect();    }    function sendDisconnect2() {        chat2Socket.disconnect();    }    function sendMessage1() {        sendMessageHandler('#chat1', userName1, chat1Socket);    }    function sendMessage2() {        sendMessageHandler('#chat2', userName2, chat2Socket);    }    function output(message, parentId) {        var currentTime = "<span class='time'>"                + moment().format('HH:mm:ss.SSS') + "</span>";        var element = $("<div>" + currentTime + " " + message + "</div>");        $(parentId + ' .console').prepend(element);    }    $(document).keydown(function(e) {        if (e.keyCode == 13) {            $('#send').click();        }    });</script></head><body>    <h1>Namespaces demo chat</h1>    <br />    <div id="chat1" style="width: 49%; float: left;">        <h4>chat1</h4>        <div class="console well"></div>        <form class="well form-inline" onsubmit="return false;">            <input class="msg input-xlarge" type="text"                placeholder="Type something..." />            <button type="button" onClick="sendMessage1()" class="btn" id="send">Send</button>            <button type="button" onClick="sendDisconnect1()" class="btn">Disconnect</button>        </form>    </div>    <div id="chat2" style="width: 49%; float: right;">        <h4>chat2</h4>        <div class="console well"></div>        <form class="well form-inline" onsubmit="return false;">            <input class="msg input-xlarge" type="text"                placeholder="Type something..." />            <button type="button" onClick="sendMessage2()" class="btn" id="send">Send</button>            <button type="button" onClick="sendDisconnect2()" class="btn">Disconnect</button>        </form>    </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
    • 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
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136

到这里,我们学习了一个能用于生产的推送框架的基本使用。不过,以上只是一个简单例子,仅做引路入门,更多参考可以直接去官方网站找到,我再写就是赘述了:

例外的一点是,由于分布式netty-socketio的部署方式文档中描述的不太清晰,且这部分实际中比较重要,我会在下面再继续描述下。


四、分布式服务器实例

1. 分布式环境下的问题

在分布式部署环境下假设有3台服务器分别为:PushServer001PushServer002PushServer003。有3个Client连接上了服务器且他们都在一个命名空间下的同一个room中(叫room1)。连接关系如下:

  • Client1 <———> PushServer001
  • Client2 <———> PushServer001
  • Client3 <———> PushServer003

此时Client1发送了一条消息,PushServer集群收到消息后显然需要将其推到Client2Client3上。

  • Client2好说:它和Client1连接的是同一个PushServer001PushServer001通过Client1可以获取到room,继而通过room获取到其下的所有Clients(其中必有Client2),然后推送即可。

  • Client3怎么办呢?它连接的是PushServer003,而003并没有收到Client1的推送事件。

2. 解决方案

其实解决方案也很简单,就是用发布/订阅 模式。

  1. 首先需要引入一个第三方的发布/订阅系统,比如这里使用Redis-PUB/SUB(如果Redis是主从复制的,注意PUB只能由Master做,SUB则Master和Slaves都行)

  2. 其次,每当服务器需要发送消息时:

    • 先将消息发送给本Server保存的某room中的所有Client
    • 接着再立即发布一个通知,例如叫PubSubStore.DISPATCH,并将消息内容放入其中。
    // 本服务器推送try {    Iterable<SocketIOClient> clients = pushNamespace.getRoomClients(room);    for (SocketIOClient socketIOClient : clients) {        socketIOClient.send(packet);    }} catch (Exception e) {    logger.error("当前服务直接推送失败", e);}// 分发消息(当前服务不会向client推送自己分发出去的消息)try {    pubSubStore.publish(PubSubStore.DISPATCH, new DispatchMessage(userId, packet, pushNamespace.getName()));} catch (Exception e) {    logger.error("分发消息失败", e);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  3. 最后,每台服务器启动时都订阅通知PubSubStore.DISPATCH。每当当前服务器收到此类订阅通知时,就将其中的消息分发到同一个房间名的所有Client去。在com.corundumstudio.socketio.store.pubsub.BaseStoreFactory.init(*)时:

    pubSubStore().subscribe(PubSubStore.DISPATCH, new PubSubListener<DispatchMessage>() {    @Override    public void onMessage(DispatchMessage msg) {        String room = msg.getRoom();        namespacesHub.get(msg.getNamespace()).dispatch(room, msg.getPacket());    }}, DispatchMessage.class);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如此便能解决此问题。附上netty-socket.io相关话题Wiki:How-To:-create-a-cluster-of-netty-socketio-servers


其它一些事

1. HTTP持久连接

所谓HTTP持久连接即是:HTTP persistent connection,意即TCP连接重用技术。HTTP 1.0 的连接本来是“短连接”:建立一次TCP做完请求-响应即关闭,这样频繁的创建、关闭TCP连接显然是很低效比较浪费资源。

所以HTTP协议后来就做了升级,允许使用一个请求和响应头Connection:keep-alive,来祈使服务器能够保持连接不中断。如此,一个TCP连接就能在你对同一个网站进行访问的时候被多次复用,请求网页HTML本身、网页中的JS、CSS和图片等都用这一个连接。

不过,到了HTTP 1.1 以上连接默认就是持久化的了。

值得注意的是HTTP服务器一般都有超时机制,服务器不可能容忍你一直不释放连接的。例如:Apache httpd 1.3/2.0是15秒、2.2是5秒。

持久连接做的是连接复用的工作,并不是解决全双工通讯、推送的。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
1024
哇靠,这些数据疯狂变化,该怎么爬取?
直播软件开发,主动的方式有很多种,你选哪个
认识HTML5的WebSocket
WebSocked、SSE、http1.0、http1.1和http2.0之间的关系
Python网络编程:构建网络应用与通信
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服