写在前面

通常进行的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.1
Host: xxx.xxx.com:port
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-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,之前草案的版本均应当被弃用。

响应如下:

HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection:upgrade
Sec-WebSocket-Accept:QJsTRym36zHnArQ7FCmSdPhuK78=// Connection:upgrade 升级被服务器同意
// Upgrade:websocket 指示客户端升级到websocket
// Sec-WebSocket-Accept:参考上面请求的Sec-WebSocket-Key的注释

上面只是比较重要的点,其实只知道这些暂时就够了,更详细的细节请参看:
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:

  • a Node.js server (this repository)
  • a Javascript client library for the browser (or a Node.js client)

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>() {@Overridepublic void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {// broadcast messages to all clientschat1namespace.getBroadcastOperations().sendEvent("message", data);}
    });final SocketIONamespace chat2namespace = server.addNamespace("/chat2");
    chat2namespace.addEventListener("message", ChatObject.class, new DataListener<ChatObject>() {@Overridepublic void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {// broadcast messages to all clientschat2namespace.getBroadcastOperations().sendEvent("message", data);}
    });server.start();Thread.sleep(Integer.MAX_VALUE);server.stop();
    }
    
  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="http://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>
    

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

  • Socket.IO服务端API点这里

  • Socket.IO JS客户端API点这里

  • netty-socketio服务端API点这里

  • netty-socketio Demo点这里

例外的一点是,由于分布式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);
    }
  3. 最后,每台服务器启动时都订阅通知PubSubStore.DISPATCH。每当当前服务器收到此类订阅通知时,就将其中的消息分发到同一个房间名的所有Client去。在com.corundumstudio.socketio.store.pubsub.BaseStoreFactory.init(*)时:

    pubSubStore().subscribe(PubSubStore.DISPATCH, new PubSubListener<DispatchMessage>() {@Overridepublic void onMessage(DispatchMessage msg) {String room = msg.getRoom();namespacesHub.get(msg.getNamespace()).dispatch(room, msg.getPacket());}
    }, DispatchMessage.class);

    如此便能解决此问题。附上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秒。

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

WEB即时通讯/消息推送相关推荐

  1. 关于web端的消息推送方式转载

    引言: 在互联网高速发展的时代里,web应用大有取代桌面应用的趋势,不必再去繁琐的安装各种软件,只需一款主流浏览器即可完成大部分常规操作,这些原因都在吸引着软件厂商和消费者.而随着各大厂商浏览器版本的 ...

  2. Web即时通信技术 -- 服务器推送技术盘点

    介绍 Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web即时通讯方案主要有4种: 客户端轮询:传统意义上的短轮询(Short Polling) 服务器端轮询:长轮询( ...

  3. 即时通讯-Android推送方案(MQTT)

    这篇文章是居于前面的几篇博客,如果还不知道ActiveMQ服务器的请看:即时通讯-ActiveMQ环境搭建 1.什么是MQTT协议 MQTT(Message Queuing Telemetry Tra ...

  4. web中的GoEasy消息推送机制

    最近要用到消息推送机制,网上查了很多,什么websocket,pushlet,GoEasy等,最后发现还是GoEasy最简单方便,很容易入手,不到10分钟就可以进行web端的消息推送啦 话不多说,直接 ...

  5. java 消息推送的几种方式比较

    引言: 在互联网高速发展的时代里,web应用大有取代桌面应用的趋势,不必再去繁琐的安装各种软件,只需一款主流浏览器即可完成大部分常规操作,这些原因都在吸引着软件厂商和消费者.而随着各大厂商浏览器版本的 ...

  6. 【转】推送消息推送机制

    原文链接:推送消息&推送机制 - 知乎 消息推送(push)用一句话解释就是:服务端向客户端发送了一条消息,我们在通知栏.锁屏通知.微信消息等等之类的都是消息推送. 1/推送类型有哪些? 消息 ...

  7. Worktile中的实时消息推送服务实现

    在团队协同工具worktile的使用过程中,你会发现无论是右上角的消息通知,还是在任务面板中拖动任务,还有用户的在线状态,都是实时刷新.Worktile中的推送服务是采用的是基于xmpp协议.erla ...

  8. Worktile 中百万级实时消息推送服务的实现

    Worktile 中百万级实时消息推送服务的实现 转自:http://www.360doc.com/content/15/0907/19/1073512_497529854.shtml 这是一个创建于 ...

  9. 未读消息(小红点),前端 与 RabbitMQ 实时消息推送实践,贼简单~

    前几天粉丝群里有个小伙伴问过:web 页面的未读消息(小红点)怎么实现比较简单,刚好本周手头有类似的开发任务,索性就整理出来供小伙伴们参考,没准哪天就能用得上呢. 之前在 <springboot ...

最新文章

  1. 告别硬编码,SpringBoot实现动态增删启停定时任务
  2. ***测试技巧总结更新篇3
  3. 从NoSQL到Lakehouse,Apache Doris的13年技术演进之路
  4. 树形结构:优先级队列,堆
  5. ResourceLoader 获取资源
  6. 放假了,暂告一段落,迎接研究生
  7. jquery 延迟加载代码_延迟加载是一种代码气味
  8. Matlab R2010a 32bit 绿色免安装版
  9. arm架构linux进程调试,gdb-multiarch + gdbserver调试linux arm应用程序
  10. 定制 cobbler TITLE 信息
  11. !--more--搭建的博客设置主页内容高度
  12. 人工智能的未来:杀戮还是治愈?
  13. spring 监听器
  14. 新任项目主管如何带好IT团队?
  15. interface详解
  16. python随机生成4位验证码并判断是否正确_利用Python生成随机4位验证码
  17. Python数据类型(一)数字类型
  18. 怎么用html把字写到图片上,用HTML代码在图片上写字
  19. Shell中EOF的用法
  20. 远程桌面登录提示存储空间不足

热门文章

  1. 专门用于日常办公的计算机,某企业需要为普通员工每人购置一台计算机,专门用于日常办公,通常选购的机型是。...
  2. Dynamics 365 Workflow Tools的使用方法
  3. js过圆外一点的直线与圆相切的切点坐标计算
  4. 【笔记】软件测试02——移动测试基础02
  5. Python项目实践:蒙特卡罗方法计算圆周率
  6. 晶振噪声及杂散_晶振的原理及作用?
  7. 《虚拟聊天室(模仿QQ)》实验报告
  8. 中国金融科技50强之“冰鉴科技”基于大数据的小微企业征信
  9. linux下多进程聊天室,从0实现基于Linux socket聊天室-多线程服务器模型-1
  10. 苏州企业如何确定科技成果的权利归属