Controller
@Api(tags = "前后端长连接")
@RestController
@RequestMapping("/business/sseEmitter")
public class SseEmitterController extends BaseController {
/**
* 用于创建连接(将用户注册到server中)
*/
@ApiOperation(value = "创建连接")
@GetMapping("/connect/{userId}")
public SseEmitter connect(@PathVariable String userId) {
return SseEmitterServer.connect(userId);
}
@ApiOperation(value = "给所有的用户发送信息")
@GetMapping("/push/{message}")
public WebResponse push(@PathVariable(name = "message") String message) {
SseEmitterServer.batchSendMessage(message);
return WebResponse.ok();
}
}
Util
public class SseEmitterServer {
private static final Logger logger = LoggerFactory.getLogger(SseEmitterServer.class);
/**
* 当前连接数
*/
private static AtomicInteger count = new AtomicInteger(0);
/**
* 使用map对象,便于根据userId来获取对应的SseEmitter,或者放redis里面
*/
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 创建用户连接并返回 SseEmitter
* @param employeeCode 用户ID
* @return SseEmitter
*/
public static SseEmitter connect(String employeeCode) {
// 设置超时时间,0表示不过期。默认30秒,超过时间未完成会抛出异常:AsyncRequestTimeoutException
SseEmitter sseEmitter = new SseEmitter(0L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(employeeCode));
// sseEmitter.completeWithError(errorCallBack(employeeCode));
sseEmitter.onTimeout(timeoutCallBack(employeeCode));
sseEmitterMap.put(employeeCode, sseEmitter);
// 数量+1
count.getAndIncrement();
logger.info("创建新的sse连接,当前用户:{}", employeeCode);
return sseEmitter;
}
/**
* 给指定用户发送信息
* @param employeeCode
* @param jsonMsg
*/
public static void sendMessage(String employeeCode, String jsonMsg) {
try {
SseEmitter emitter = sseEmitterMap.get(employeeCode);
if (emitter == null) {
logger.warn("sse用户[{}]不在注册表,消息推送失败", employeeCode);
return;
}
emitter.send(jsonMsg, MediaType.APPLICATION_JSON);
} catch (IOException e) {
logger.error("sse用户[{}]推送异常:{}", employeeCode, e.getMessage());
removeUser(employeeCode);
}
}
/**
* 群发消息
* @param jsonMsg
* @param employeeCodes
*/
public static void batchSendMessage(String jsonMsg, List<String> employeeCodes) {
employeeCodes.forEach(employeeCode -> sendMessage(employeeCode, jsonMsg));
}
/**
* 群发所有人
* @param jsonMsg
*/
public static void batchSendMessage(String jsonMsg) {
sseEmitterMap.forEach((k, v) -> {
try {
v.send(jsonMsg, MediaType.APPLICATION_JSON);
} catch (IOException e) {
logger.error("用户[{}]推送异常:{}", k, e.getMessage());
removeUser(k);
}
});
}
/**
* 移除用户连接
*/
public static void removeUser(String employeeCode) {
SseEmitter emitter = sseEmitterMap.get(employeeCode);
if(emitter != null){
emitter.complete();
}
sseEmitterMap.remove(employeeCode);
// 数量-1
count.getAndDecrement();
logger.info("移除sse用户:{}", employeeCode);
}
/**
* 获取当前连接信息
*/
public static List<String> getIds() {
return new ArrayList<>(sseEmitterMap.keySet());
}
/**
* 获取当前连接数量
*/
public static int getUserCount() {
return count.intValue();
}
private static Runnable completionCallBack(String employeeCode) {
return () -> {
logger.info("结束sse用户连接:{}", employeeCode);
removeUser(employeeCode);
};
}
private static Runnable timeoutCallBack(String employeeCode) {
return () -> {
logger.info("连接sse用户超时:{}", employeeCode);
removeUser(employeeCode);
};
}
private static Throwable errorCallBack(String employeeCode) {
logger.info("sse用户连接异常:{}", employeeCode);
removeUser(employeeCode);
return new Throwable();
}
}
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SseEmitter</title>
</head>
<body>
<button onclick="closeSse()">关闭连接</button>
<div id="message"></div>
</body>
<script>
let source = null;
// 用时间戳模拟登录用户
const userId = new Date().getTime();
console.log(userId);
if (!!window.EventSource) {
// 建立连接
source = new EventSource('http://localhost:8090/business/sseEmitter/connect/' + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
/**
* 如果发生通信错误(比如连接中断),就会触发error事件
* 或者:
* 另一种写法:source.onerror = function (event) {}
*/
source.addEventListener('error', function (e) {
if (e.readyState === EventSource.CLOSED) {
setMessageInnerHTML("连接关闭");
} else {
console.log(e);
}
}, false);
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
// 监听窗口关闭事件,主动去关闭sse连接,如果服务端设置永不过期,浏览器关闭后手动清理服务端数据
window.onbeforeunload = function () {
closeSse();
};
// 关闭Sse连接
function closeSse() {
source.close();
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET', 'http://localhost:8090/sse/close/' + userId, true);
httpRequest.send();
console.log("close");
}
// 将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
</script>
</html>
上文引用:Java后端主动向前端推送消息_牧子与羊的博客-CSDN博客_java后端主动发送数据给前端
前端引入依赖
1,安装依赖 :npm install event-source-polyfill
2,引入: import { EventSourcePolyfill } from "event-source-polyfill";
完成上面之后在web.XML配置,如下:
在web.XML中servlet配置 <async-supported>true</async-supported>
注:只需要在springmvc 控制器的<srevlet>中配置
<!-- springmvc 控制器 -->
<servlet>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
在servlet配置完之后继续在XML里面的<filter>加上<async-supported>true</async-supported>
注:在XML里所有的<filter>的标签中加上<async-supported>true</async-supported>
<filter>
<filter-name>knife4jProductionFilter</filter-name>
<filter-class>ProductionSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>production</param-name>
<param-value>false</param-value>
</init-param>
</filter>
若出现部署在服务器上之后出现链接不上的问题则在nginx中配置
proxy_buffering off;
location /apis/ {
proxy_pass http://127.0.0.1:28080/huaian_warehouse_war/;
proxy_buffering off;//关闭缓冲
}