长轮询,iframe和sse三种web消息实时推送demo实践
长轮询
@Controller
@RequestMapping("/polling")
public class PollingController {
private static final Long TIME_OUT = 10000l;
public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
@GetMapping(path = "watch/{id}")
@ResponseBody
public DeferredResult<String> watch(@PathVariable String id) {
DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
deferredResult.onCompletion(() -> {
watchRequests.remove(id, deferredResult);
});
watchRequests.put(id, deferredResult);
return deferredResult;
}
@GetMapping(path = "publish/{id}")
@ResponseBody
public String publish(@PathVariable String id) {
if (watchRequests.containsKey(id)) {
Collection<DeferredResult<String>> deferredResults = watchRequests.get(id);
System.out.println(deferredResults.size());
for (DeferredResult<String> deferredResult : deferredResults) {
deferredResult.setResult("我更新了" + new Date());
}
}
return "success";
}
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("异步请求超时");
return "304";
}
}
}
iframe
IframeController
@Controller
@RequestMapping("/iframe")
public class IframeController {
@GetMapping(path = "message")
public void message(HttpServletResponse response) throws IOException, InterruptedException {
int count = 0;
while (true) {
count++;
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print(" <script type=\"text/javascript\">\n" +
"parent.document.getElementById('clock').innerHTML = \"" + count + "\";" +
"parent.document.getElementById('count').innerHTML = \"" + count + "\";" +
"</script>");
}
}
@GetMapping(path = "toIframePage")
public String toIframePage() {
return "/iframe";
}
}
iframe.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>iframe长轮询</title>
</head>
<body>
<iframe src="/iframe/message" style="display:none"></iframe>
<h1 id="clock"></h1>
<h1 id="count"></h1>
</body>
</html>
sse
SseEmitterServer
@Slf4j
public class SseEmitterServer {
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
private static AtomicInteger count = new AtomicInteger(0);
public static SseEmitter connect(String userId) {
try {
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
count.getAndIncrement();
return sseEmitter;
} catch (Exception e) {
log.info("创建新的sse连接异常,当前用户:{}", userId);
}
return null;
}
private static Consumer<Throwable> errorCallBack(String userId) {
return throwable -> {
log.info("连接异常:{}", userId);
removeUser(userId);
};
}
private static Runnable timeoutCallBack(String userId) {
return () -> {
log.info("连接超时:{}", userId);
removeUser(userId);
};
}
private static Runnable completionCallBack(String userId) {
return () -> {
log.info("结束连接:{}", userId);
removeUser(userId);
};
}
public static void removeUser(String userId) {
sseEmitterMap.remove(userId);
count.getAndDecrement();
log.info("移除用户:{}", userId);
}
public static void sendMessage(String userId, String message) {
if (sseEmitterMap.containsKey(userId)) {
try {
sseEmitterMap.get(userId).send(message);
} catch (IOException e) {
log.error("用户[{}]推送异常:{}", userId, e.getMessage());
removeUser(userId);
}
}
}
}
SseEmitterController
@CrossOrigin
@RestController
@RequestMapping("/sse")
public class SseEmitterController {
@GetMapping("/connect/{userId}")
public SseEmitter connect(@PathVariable String userId) {
return SseEmitterServer.connect(userId);
}
@RequestMapping("/push_one")
public ResponseEntity<String> pushOne( String message, String userid) {
SseEmitterServer.sendMessage(userid, message);
return ResponseEntity.ok("WebSocket 推送消息给" + userid);
}
@GetMapping("/close/{userid}")
public ResponseEntity<String> close(@PathVariable("userid") String userid) {
SseEmitterServer.removeUser(userid);
return ResponseEntity.ok("连接关闭");
}
}
PageController
@Controller
public class PageController {
@RequestMapping("/toSsePage")
public String toSsePage() {
return "/sse";
}
}
sse.html
<!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 = "1";
if (window.EventSource) {
source = new EventSource('http://localhost:8080/sse/connect/' + userId);
source.addEventListener('open', function(e) {
setMessageInnerHTML("建立连接。。。");
}, false);
source.addEventListener('message', function(e) {
setMessageInnerHTML(e.data);
});
source.addEventListener('error', function(e) {
if (e.readyState === EventSource.CLOSED) {
setMessageInnerHTML("连接关闭");
} else {
console.log(e);
}
}, false);
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
window.onbeforeunload = function() {
closeSse();
};
function closeSse() {
source.close();
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET', 'http://localhost:8080/sse/close/' + userId, true);
httpRequest.send();
console.log("close");
}
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
</script>
</html>
上面三种消息推送的优缺点
三种消息推送的优缺点