在浏览某些网页的时候,例如 WebQQ
、京东在线客服服务、CSDN私信消息等类似的情况下,我们可以在网页上进行在线聊天,或者即时消息的收取与回复,可见,这种功能的需求由来已久,并且应用广泛,和pc端web系统待办提醒 等。
Web
端 常见的消息推送实际上大多数都是模拟推送,之所以是模拟推送,是因为这种实现并不是服务器主动推送,本质依旧是客户端发起请求,服务端返回数据,起主动作用的是客户端。
可分为俩大类:
一、客户端实现
1.1短轮询
短轮询即浏览器定时向服务器发送请求,以此来更新数据的方法。如下图所示,原理就是客户端不断地向服务端发请求,如果服务端数据有更新,服务端就把数据发送回来,客户端就能接收到新数据了,
浏览器每隔一段时间向服务器发送一次请求,请求浏览器想要的数据。严格意义上讲:短轮询不是服务器推送的消息,获取的数据也不是实时的。
优点:前后端程序都很容易编写,没什么技术难度
缺点:这种方法因为需要对服务器进行持续不断的请求,就算你设置的请求间隔时间很长,但在用户访问量比较大的情况下,也很容易给服务器带来很大的压力,而且绝大部分情况下都是无效请求,浪费带宽和服务器资源,一般不会用于实际生产环境的,自己知道一下就行了。
1.2长轮询
长轮询是短轮询的一个翻版,或者叫改进版。浏览器向服务器发送一个请求看有没有数据,有数据就响应,没数据就保持该请求,知道有数据再返回。浏览器在服务器返回数据时再发送一个请求。这样浏览器就可以一直获取到最新的数据。长轮询的时间线如下图所示;在长轮询的情况下,服务器端在接到客户端请求之后,如果发现数据库中的数据并没有更新或者不符合要求,那么就不会立即响应客户端,而是 hold住这次请求,直到符合要求的数据到达或者因为超时等原因才会关闭连接,客户端在接收到新数据或者连接被关闭后,再次发起新的请求。
为了节约资源,一次长轮询的周期时间最好在 10s ~ 25s左右,长连接也是实际生产环境中,被广泛运用于实时通信的技术。
优点:
尽管长轮询不可能做到每一次的响应都是有用的数据,因为服务器超时或者客户端网络环境的变化,以及服务端为了更好的分配资源而自动在一个心跳
周期的末尾断掉连接等原因,而导致长轮询不可能一直存在,必须要不断地进行断开和连接操作,但无论如何,相比于短轮询来说,长轮询耗费资源明显小了很多
缺点:
服务器 hold
连接依旧会消耗不少的资源,特别是当连接数很大的时候,返回数据顺序无保证,难于管理维护。
1.3长连接
这种是基于 iframe 或者 script实现的,主要原理大概就是在主页面中插入一个隐藏的 iframe(script),然后这个 iframe(script)的 src属性指向服务端获取数据的接口,因为是iframe(script)是隐藏的,而且 iframe(script)的 刷新也不会导致 主页面刷新,所以可以为这个 iframe(script)设置一个定时器,让其每隔一段时间就朝服务器发送一次请求,这样就能获得服务端的最新数据了。
主要是在前端,一共两条 script
脚本,大致左右就是在一定的时间间隔内(示例为 3s
)就动态地在页面中增删一个链接为用于请求后端数据的 script
脚本。
后端则返回一段字符串,这段字符串在返回前端时,有一个 callback
字段调用前端的代码,类似于 jsonp
的请求。
二、服务端实现
2.1webSocket
Web Sockets 的是在一个单独的持久连接上提供全双工、双向通信。在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会从 HTTP 协议升级为 Web Socket 协议。
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
使用spring框架可以很容易实现websocket,这是spring实现websocket的官方教程(非常详细)地址:https://spring.io/guides/gs/messaging-stomp-websocket/
三、上代码
核心思想,使用rest 风格动态变化用户id,每个用户建立一次连接的session要保存到容器中,保证每个用户享有自己的session集合。
springBoot项目 集成
websocet 支持
一个用户下建立多个连接,例如:一个用户开启多个网页,每个网页都建立一个socket连接。
用户的多个页面,能收到统一的消息:
3.1、后端代码 加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency>
书写配置类,实例化Bean
package com.yxsd.cnooc.data.wb;import org.springframework.context.annotation.Bean;import org.springframework.stereotype.Component;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Componentpublic class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); }}
服务类
package com.yxsd.cnooc.data.service;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.CopyOnWriteArraySet;/** * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 */@Component@ServerEndpoint("/websocket/{userId}")public class WebSocketTest { private static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketTest>> userwebSocketMap = new ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketTest>>(); private static ConcurrentHashMap<String, Integer> count = new ConcurrentHashMap<String, Integer>(); private String userId; /* * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 连接建立成功调用的方法 * * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(Session session, @PathParam("userId") final String userId) { this.session = session; this.userId = userId; System.out.println("session:"+session); System.out.println("userId:"+userId); if (!exitUser(userId)) { initUserInfo(userId); } else { CopyOnWriteArraySet<WebSocketTest> webSocketTestSet = getUserSocketSet(userId); webSocketTestSet.add(this); userCountIncrease(userId); } System.out.println("有" + userId + "新连接加入!当前在线人数为" + getCurrUserCount(userId)); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { CopyOnWriteArraySet<WebSocketTest> webSocketTestSet = userwebSocketMap.get(userId); //从set中删除 webSocketTestSet.remove(this); //在线数减1 userCountDecrement(userId); System.out.println("有一连接关闭!当前在线人数为" + getCurrUserCount(userId)); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { CopyOnWriteArraySet<WebSocketTest> webSocketSet = userwebSocketMap.get(userId); /* System.out.println("来自客户端" + userId + "的消息:" + message); //群发消息 for (WebSocketTest item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); continue; } }*/ } /** * 发生错误时调用 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误"); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { System.out.println("服务端推送" + userId + "的消息:" + message); this.session.getAsyncRemote().sendText(message); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 我是在有代办消息时 调用此接口 向指定用户发送消息 * * @param message * @throws IOException */ public void sendMessage(String userId,String message) throws IOException { System.out.println("服务端推送" + userId + "的消息:" + message); CopyOnWriteArraySet<WebSocketTest> webSocketSet = userwebSocketMap.get(userId); //群发消息 for (WebSocketTest item : webSocketSet) { try { item.session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); continue; } } } public boolean exitUser(String userId) { return userwebSocketMap.containsKey(userId); } public CopyOnWriteArraySet<WebSocketTest> getUserSocketSet(String userId) { return userwebSocketMap.get(userId); } public void userCountIncrease(String userId) { if (count.containsKey(userId)) { count.put(userId, count.get(userId) + 1); } } public void userCountDecrement(String userId) { if (count.containsKey(userId)) { count.put(userId, count.get(userId) - 1); } } public void removeUserConunt(String userId) { count.remove(userId); } public Integer getCurrUserCount(String userId) { return count.get(userId); } private void initUserInfo(String userId) { CopyOnWriteArraySet<WebSocketTest> webSocketTestSet = new CopyOnWriteArraySet<WebSocketTest>(); webSocketTestSet.add(this); userwebSocketMap.put(userId, webSocketTestSet); count.put(userId, 1); }}
此处前端测试
客户端页面使用 http://www.jsons.cn/websocket/ 与项目建立连接,充当web客户端
启动项目
模拟俩个浏览器 用户1发送消息 到服务器 和 服务器 向用户1发送消息 俩个浏览器都能接受到消息
连接websocket
用户1发送消息
模拟服务器向用户1 发消息 写个接口 调用触发发送 只贴部分代码
@GetMapping("/sendMsage")public Result sendMsage(@RequestParam("userId") String userId, @RequestParam("msg") String msg){ try { webSocketTest.sendMessage(userId,msg); } catch (IOException e) { e.printStackTrace(); System.out.println("出现异常"); } return ResultUtils.success("查询成功!",null);}
http://127.0.0.1:9010/user/sendMsage?userId=1&msg=aa
用户1 俩浏览器都接受到消息
四、前端代码 参考 知其所以然
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title></head><body></body><script> let webSocket = null; // 创建一个变量 if ('WebSocket' in window){ // 判断当前的浏览器是否支持WebSocket // 如果支持则创建一个WebSocket赋值给刚才创建的变量 // 后面的路径实际上就是一次请求,但是这里用的是WebSocket协议 // 记住这个地方后面详细讲到怎么写 webSocket = new WebSocket('ws://localhost:8080/webSocket'); }else{ // 如果不兼容则弹框,该浏览器不支持 alert('该浏览器不支持') } /** * 当WebSocket创建连接(初始化)会触发该方法 */ webSocket.onopen = function (event){ console.log('建立连接') // 这个代表在浏览器打印日志,跟Java的System.out.println()意思一致 } /** * 当WebSocket关闭时候会触发该方法 */ webSocket.onclose = function (event){ console.log('关闭连接') // 同上 } /** * 当WebSocket接受消息会触发该方法 */ webSocket.onmessage = function (event){ console.log('收到消息:'+event.data) } /** * 当WebSocket连接出错触发该方法 */ webSocket.onerror = function (event){ console.log('websocket发生错误'); } /** * 页面关闭,WebSocket关闭 */ window.onbeforeunload = function (){ webSocket.close(); }</script></html>
参考
demo篇---同一个用户开启多个webSocket连接_茄子_土豆的博客-CSDN博客_websocket多个连接
Java版 WebSocket实现消息推送【保姆来了!】_地雷Java的博客-CSDN博客_java整合websocket实现实时推送
服务器推送消息方法总结及实现(java)_xubaodian的博客-CSDN博客
javaweb实现即时消息推送功能_飞亦浩然的博客-CSDN博客_java实时推送消息到前端