由于项目需要定时将消息从Web端推送至客户端
通常使用的方式有:AJAX轮询、XHR长轮询、iframe、Comet、websocket等
部分详情可见:https://blog.csdn.net/qq_43225978/article/details/105396640
考虑实现的难度及复杂度,最终选用WebSocket方式。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
以下 API 用于创建 WebSocket 对象。
WebSocket 简介及API
https://www.runoob.com/html/html5-websocket.html
http://www.ruanyifeng.com/blog/2017/05/websocket.html
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
// 心跳重试连接次数
var websocket_reconnected_count = 0;
var websocket = null;
// 初始化WebSocket连接
function initWebSocket() {
websocket = null;
// 判断当前环境是否支持websocket
if(window.WebSocket){
if(!websocket){
// 获取协议类型
var protocol = window.location.protocol;
// console.info(protocol)
// 通过访问协议类型,判断使用的websocket协议类型
var ws_url = protocol=='http:'?'ws://':'wss://'
// 获取域名
var host = window.location.host;
// 获取端口号
var port = window.location.port;
// 获取项目访问路由
var pathName = window.location.pathname;
// 截取项目名
var projectName = pathName.substring(0, pathName.substr(1).indexOf('/') + 1);
// 拼接websocket访问地址
ws_url += host + projectName + "/webSocket/user_1";
// console.info(ws_url);
// 创建websocket对象
websocket = new WebSocket(ws_url);
}
}else{
var content = '【当前浏览器不支持WEBSOCKET,无法获取预警提醒消息,为获得良好的使用体验,推荐您下载使用Chrome浏览器】';
alter(content);
}
//连接成功建立的回调方法
websocket.onopen = function () {
console.log('WebSocket连接成功');
// 成功建立连接后,重置心跳检测
heartCheck.reset().start();
}
//连接发生错误的回调方法
websocket.onerror = function () {
console.log('WebSocket连接发生错误');
};
//接收到消息的回调方法
websocket.onmessage = function (event) {
// console.log("=====WebSocket接收到消息=====");
// console.log(event);
// console.log(event.data);
// 当服务端的会话不存在时,返回标志
if(event.data=="\"sessionIsNulltrue\""){
// 断开ws
websocket.close();
// 因为是会话过期,所以心跳重试链接机制的次数websocket_reconnected_count归零
websocket_reconnected_count=0;
// 重置清空心跳
heartCheck.reset();
// 使用layui弹窗提醒
parent.layer.open({
content: "会话过期!请重新登录!"
,btn: '确定',
closeBtn : false,
yes: function(index, layero){
//按钮【按钮一】的回调
// console.info("=======")
parent.layer.closeAll();
// 跳转至主页
window.top.location.href = Common.ctxPath + "/"
}
});
}
// 如果获取到消息,说明连接是正常的,重置心跳检测
heartCheck.reset().start();
}
//连接关闭的回调方法
websocket.onclose = function () {
console.log('WebSocket连接关闭');
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
// 关闭WebSocket连接
websocket.close();
}
}
// 心跳检测, 每隔一段时间检测连接状态,如果处于连接中,就向server端主动发送消息,来重置server端与客户端的最大连接时间,如果已经断开了,发起重连。
var heartCheck = {
// 30s 发一次心跳,比server端设置的连接时间稍微小,在接近断开的情况下以通信的方式去重置连接时间。
timeout: 30000,
serverTimeoutObj: null,
reset: function(){
var self = this;
clearTimeout(self.timeout);
clearInterval(self.serverTimeoutObj);
return self;
},
start: function(){
var self = this;
this.serverTimeoutObj = setInterval(function(){
if(websocket.readyState == 1){
console.log("连接状态,心跳保持连接");
websocket.send("ping");
heartCheck.reset().start(); // 如果获取到消息,说明连接是正常的,重置心跳检测
}else{
if(websocket_reconnected_count>0){
--websocket_reconnected_count;
console.log("断开状态,尝试重连");
initWebSocket();
}else {
heartCheck.reset();
}
}
}, self.timeout)
}
}
对于这样一个URL
http://www.x2y2.com:80/fisker/post/0703/window.location.html?ver=1.0&id=6#imhere
我们可以用javascript获得其中的各个部分
1, window.location.href
整个URl字符串(在浏览器中就是完整的地址栏)
本例返回值: http://www.x2y2.com:80/fisker/post/0703/window.location.html?ver=1.0&id=6#imhere
2,window.location.protocol
URL 的协议部分
本例返回值:http:
3,window.location.host
URL 的主机部分
本例返回值:www.x2y2.com
4,window.location.port
URL 的端口部分
如果采用默认的80端口(update:即使添加了:80),那么返回值并不是默认的80而是空字符
本例返回值:""
5,window.location.pathname
URL 的路径部分(就是文件地址)
本例返回值:/fisker/post/0703/window.location.html
6,window.location.search
查询(参数)部分
除了给动态语言赋值以外,我们同样可以给静态页面,并使用javascript来获得相信应的参数值
本例返回值:?ver=1.0&id=6
来源 : https://www.cnblogs.com/chaoyuehedy/p/5708165.html
websocket在http与https不同协议下实际上按照标准来是有如下对应关系的:
http -> new WebSocket('ws://xxx')
https -> new WebSocket('wss://xxx')
也就是在https下应该使用wss协议做安全链接,且wss下不支持ip地址的写法,写成域名形式
部分报错的浏览器的确是因为这个原因导致的代码异常,即在https下把ws换成wss请求即可,看到这里心细的也许会发现,是部分浏览器,实际上浏览器并没有严格的限制http下一定使用ws,而不能使用wss,经过测试http协议下同样可以使用wss协议链接,https下同样也能使用ws链接,那么出问题的是哪一部分呢
1.Firefox环境下https不能使用ws连接
2.chrome内核版本号低于50的浏览器是不允许https下使用ws链接
3.Firefox环境下https下使用wss链接需要安装证书
实际上主要是问题出在Firefox以及低版本的Chrome内核浏览器上,于是在http与https两种协议都支持的情况下可以做兼容处理,即在http协议下使用ws,在https协议下使用wss
可使用以下方式拼接websocket访问地址:
var protocol = windows.location.protocol === 'https:' ? 'wss://localhost:8888' : 'ws://localhost:8889';
来源 : https://blog.csdn.net/garrettzxd/article/details/81674251
org.springframework.boot
spring-boot-starter-websocket
package com.xxx.xxxx.webSocket.server;
import com.alibaba.fastjson.JSONObject;
import com.xxx.xxxx.webSocket.MessageCoder.MessageDecoder;
import com.xxx.xxxx.webSocket.MessageCoder.MessageEncoder;
import com.xxx.xxxx.webSocket.config.WebSocketConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author xnz
* @date 2020/03/31
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@Component
@ServerEndpoint( value = "/webSocket/{id}",configurator = WebSocketConfig.class, encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } )
public class WebSocketServer {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
@PostConstruct
public void init() {
System.out.println("[WebSocket 加载]");
}
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static final AtomicInteger ONLINECOUNT = new AtomicInteger(0);
/**
* 线程安全map,实现服务端与单一客户端通信,其中Key为用户标识 用来存放 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
public static ConcurrentHashMap<String,Session> sessionMap = new ConcurrentHashMap<>();
/**
* 线程安全list,用来存放 在线客户端账号所属的组织id
*/
public static List<Long> orgIdList = new CopyOnWriteArrayList<>();
/**
* 对应客户端id
*/
private String sessionId = "";
// private static Long currentId = null;
/**
* 连接建立成功调用的方法
* @param id
* @param session
* @param config 用来获取WebSocketConfig中的配置信息
* @throws IOException
*/
@OnOpen
public void onOpen(@PathParam(value = "id") String id, Session session, EndpointConfig config) throws IOException {
// currentId = (Long) config.getUserProperties().get("currentId");
log.info("========" + id);
// 将当前会话账户所属组织id存储
String[] userId = id.split("_");
orgIdList.add(Long.valueOf(userId[1]));
id = UUID.randomUUID().toString()+id;
// 存储当前会话
sessionMap.put(id,session);
sessionId = id;
int count = ONLINECOUNT.incrementAndGet();
log.info("有连接加入,当前连接数为:{}", count);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
try {
session.close();
sessionMap.remove(sessionId);
String[] split = sessionId.split("_");
orgIdList.remove(Long.valueOf(split[1]));
int count = ONLINECOUNT.decrementAndGet();
log.info("有连接关闭,当前连接数为:{}", count);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("来自客户端 {} 的消息:{} , 当前连接数为:{}",sessionId,message,ONLINECOUNT.get());
// sendMessage(session, JSON.toJSONString(message));
}
/**
* 出现错误
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误,Session ID : {}",session.getId());
error.printStackTrace();
}
/**
* 指定Session发送消息,实践表明,每次浏览器刷新,session会发生变化。
* @param session
* @param message
*/
private static void sendMessage(Session session, Object message) {
try {
session.getBasicRemote().sendText(JSONObject.toJSONString(message));
// session.getBasicRemote().sendObject(message);
} catch (Exception e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 通过id 获取会话 发送消息
*
* @param message 发送的消息
*/
public static void sendMessageByOrgId(Object message,Long id) {
try {
Set<Map.Entry<String, Session>> entries = sessionMap.entrySet();
for (Map.Entry<String,Session> item : entries) {
String sessionId = item.getKey();
if(sessionId.contains(id)){
log.info("[===发送消息至===] " + sessionId);
Session session = item.getValue();
sendMessage(session,message);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.xxx.xxxx.webSocket.config;
import org.apache.catalina.session.StandardSessionFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
/**
* 主要的配置类
* 本类必须要继承Configurator,因为@ServerEndpoint注解中的config属性只接收这个类型
*/
@Configuration
public class WebSocketConfig extends ServerEndpointConfig.Configurator {
private static final Logger log = LoggerFactory.getLogger(WebSocketConfig.class);
/**
* 修改握手,就是在握手协议建立之前修改其中携带的内容
* @param sec
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
/*如果没有监听器,那么这里获取到的HttpSession是null*/
StandardSessionFacade ssf = (StandardSessionFacade) request.getHttpSession();
if (ssf != null) {
HttpSession session = (HttpSession) request.getHttpSession();
sec.getUserProperties().put("session", session);
log.info("获取到的SessionID:{}",session.getId());
// User currentUser = (User) session.getAttribute("currentUser");
// log.info("获取当前用户currentId:{}",currentUser.getId());
// sec.getUserProperties().put("currentId", currentUser.getId());
}else{
System.out.println("modifyHandshake 获取到null session");
}
super.modifyHandshake(sec, request, response);
}
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.xxx.xxxx.webSocket.MessageCoder;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
public class MessageDecoder implements Decoder.Text<String>{
private static Logger log = LoggerFactory.getLogger(MessageDecoder.class);
@Override
public String decode(String jsonMessage) throws DecodeException {
log.info("MessageDecoder decode");
return JSON.parseObject(jsonMessage, String.class);
}
@Override
public boolean willDecode(String jsonMessage) {
if(StringUtils.isBlank(jsonMessage))
return false;
try {
JSON.parseObject(jsonMessage);
return true;
} catch (Exception e) {
log.info("Message not jsonString");
return false;
}
}
@Override
public void init(EndpointConfig endpointConfig) {
log.info("MessageDecoder init");
}
@Override
public void destroy() {
log.info("MessageDecoder destroy");
}
}
package com.xxx.xxxx.webSocket.MessageCoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class MessageEncoder implements Encoder.Text<String>{
private static Logger log = LoggerFactory.getLogger(MessageEncoder.class);
@Override
public String encode(String s) throws EncodeException {
return null;
}
@Override
public void init(EndpointConfig endpointConfig) {
log.info("MessageEncoder init");
}
@Override
public void destroy() {
log.info("MessageEncoder destroy");
}
}
其他 WebSocket 客户端(javascript前端)及服务端(java后台)实现
https://www.cnblogs.com/freud/p/8397934.html
https://www.cnblogs.com/xdp-gacl/p/5193279.html
https://blog.csdn.net/Doctor_LY/article/details/81362718
spring boot Websocket(使用笔记):https://www.cnblogs.com/bianzy/p/5822426.html
在开发过程中想在 WebSocket服务端需要获取到用户使用数据库的用户信息登录后的HttpSession获取个人资料信息,通过搜索最后在WebSocketConfig类
中的modifyHandshake方法
中使用ServerEndpointConfig类
的sec.getUserProperties().put("currentId", currentUser.getId());
方法,然后在onOpen
方法中使用config.getUserProperties().get("currentId");
获取。
详情可见上述实现类。
参考:
https://www.cnblogs.com/smallfa/p/9285844.html
https://www.cnblogs.com/coder163/p/8605645.html
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpointExporter' defined in org.lwt.WebsocketServerTestApplication: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1745) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
...
原因:
WebSocket是servlet容器所支持的,所以需要加载servlet容器:
webEnvironment参数为springboot指定ApplicationContext类型。
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 表示内嵌的服务器将会在一个随机的端口启动。
@SpringBootTest(classes = WebsocketServerTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
org.springframework.boot
spring-boot-starter-tomcat
provided
org.apache.tomcat.embed
tomcat-embed-websocket
8.5.23
参考:
Error creating bean with name ‘serverEndpointExporter’ defined in class path —https://blog.csdn.net/kxj19980524/article/details/88751114
springboot整合websocket后运行测试类报错:javax.websocket.server.ServerContainer not available —https://blog.csdn.net/fggdgh/article/details/87185555
server {
listen 80;
server_name 域名;
location / {
# 代理转发地址
proxy_pass http://127.0.0.1:8080/;
# 表明使用http版本为1.1
proxy_http_version 1.1;
# 超时设置 表明连接成功以后等待服务器响应的时候,如果不配置默认为60s;
proxy_read_timeout 3600s;
# 启用支持websocket连接
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
其中重要的是这两行,它表明是websocket连接进入的时候,进行一个连接升级将http连接变成websocket的连接。
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
来源 : Nginx 支持websocket的配置 — https://blog.csdn.net/weixin_37264997/article/details/80341911
如果你的前端心跳间隔设置的大于60s,并且没有配置nginx超时时间,那么就会出现这个问题
proxy_read_timeout 3600s;
借鉴 : Nginx代理webSocket时60s自动断开, 怎么保持长连接 — https://blog.csdn.net/cm786526/article/details/79939687
WebSocket使用注意事项:
心跳包切记不要放到initWebSocket()方法中,否则会产生递归循环调用并无限创建WebSocket对象,引发的结果就是nginx的连接数被刷爆,导致使用当前nginx的所以服务无法使用(相当于攻击),之前没注意,心已碎,偶买噶达!!!!!!
9811 [localhost-startStop-1] ERROR org.springframework.boot.SpringApplication reportFailure 826 - Application run failed
java.lang.IllegalStateException: Failed to register @ServerEndpoint class: class com.motor.gdcollectioncms.webSocket.server.WebSocketServer
at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoint(ServerEndpointExporter.java:159)
at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoints(ServerEndpointExporter.java:134)
at org.springframework.web.socket.server.standard.ServerEndpointExporter.afterSingletonsInstantiated(ServerEndpointExporter.java:112)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:896)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.web.servlet.support.SpringBootServletInitializer.run(SpringBootServletInitializer.java:152)
at org.springframework.boot.web.servlet.support.SpringBootServletInitializer.createRootApplicationContext(SpringBootServletInitializer.java:132)
at org.springframework.boot.web.servlet.support.SpringBootServletInitializer.onStartup(SpringBootServletInitializer.java:92)
at org.springframework.web.SpringServletContainerInitializer.onStartup(SpringServletContainerInitializer.java:172)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5245)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:754)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:730)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:734)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:985)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1857)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path [/webSocket/{id}] : existing endpoint was [class com.motor.gdcollectioncms.webSocket.server.WebSocketServer] and new endpoint is [class com.motor.gdcollectioncms.webSocket.server.WebSocketServer]
at org.apache.tomcat.websocket.server.WsServerContainer.addEndpoint(WsServerContainer.java:170)
at org.apache.tomcat.websocket.server.WsServerContainer.addEndpoint(WsServerContainer.java:234)
at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoint(ServerEndpointExporter.java:156)
... 25 common frames omitted
javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path [/webSocket/{id}] : existing endpoint was [class com.motor.gdcollectioncms.webSocket.server.WebSocketServer] and new endpoint is [class com.motor.gdcollectioncms.webSocket.server.WebSocketServer]
即
javax.websocket。DeploymentException:多个端点可能没有部署到相同的路径[/webSocket/{id}]:现有的端点是[class com.motor.gdcollectioncms.webSocket.server]。新的端点是[class com.motor.gdcollectioncms. WebSocketServer .server.WebSocketServer]
// @Bean
// public ServerEndpointExporter serverEndpointExporter() {
// return new ServerEndpointExporter();
// }
借鉴 : 解决Multiple Endpoints may not be deployed to the same path — https://blog.csdn.net/weixin_42323802/article/details/86528844