SSE和WebSocket
SSE
SSE:server send event。服务端发送事件,指服务端主动给客户端推送消息(单向)
服务端代码:
package com.example.demo.sse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@CrossOrigin
@Controller
@RequestMapping(path = "sse")
public class Sse {
/**
* key为用户Id,内层Map key为客户端ID(一个用户可能在多个客户端中登陆)
*/
private static Map<String, ConcurrentHashMap<String, SseEmitter>> sseCache = new ConcurrentHashMap<>();
/**
* sse订阅方法,注意返回类型为:text/event-stream
* @param userId 连接用户的ID
* @param clientId 连接的客户端D
* @return SseEmitter
* @throws IOException
*/
@ResponseBody
@GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(String userId,String clientId) throws IOException {
// 超时时间设置为30s,用于演示客户端自动重连
SseEmitter sseEmitter = new SseEmitter(1000L * 30);
// 设置前端的重试时间为1s
sseEmitter.send(SseEmitter.event().reconnectTime(1_000L).data("连接成功"));
sseCache.putIfAbsent(userId,new ConcurrentHashMap<>());
sseCache.get(userId).put(clientId,sseEmitter);
System.out.println("注册 userd:" + userId + "," + "clientId:" + clientId);
sseEmitter.onTimeout(() -> {
System.out.println(userId + "--" + clientId + " 超时");
sseCache.getOrDefault(userId,new ConcurrentHashMap<>()).remove(clientId);
});
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
/**
* 模拟服务端主动给客户端推送消息
* @param userId 客户端的连接ID(需要接受消息的客户端ID)
* @param content 发送消息内容
* @return String
* @throws IOException
*/
@GetMapping(path = "push")
@ResponseBody
public String push(String userId, String clientId, String content) throws IOException {
ConcurrentHashMap<String, SseEmitter> sseCacheOrDefault = sseCache.getOrDefault(userId, new ConcurrentHashMap<>());
if(Objects.nonNull(clientId)){
sendContent(content,sseCacheOrDefault.get(clientId));
}else {
Collection<SseEmitter> values = sseCacheOrDefault.values();
for (SseEmitter sse: values){
sendContent(content, sse);
}
}
return "over";
}
/**
* 发送消息
* @param content 消息内容
* @param sse SseEmitter
* @throws IOException
*/
private void sendContent(String content, SseEmitter sse) throws IOException {
if(Objects.nonNull(sse)){
sse.send(content);
}
}
/**
* 结束连接
* @param userId 要结束的用户ID
* @param clientId 要结束的客户端ID
* @return String
*/
@GetMapping(path = "over")
@ResponseBody
public String over(String userId,String clientId) {
ConcurrentHashMap<String, SseEmitter> sseCacheOrDefault = sseCache.getOrDefault(userId, new ConcurrentHashMap<>());
if(Objects.nonNull(clientId)){
SseEmitter sseEmitter = sseCacheOrDefault.get(clientId);
if (sseEmitter != null) {
sseEmitter.complete();
}
sseCacheOrDefault.remove(clientId);
}else {
Collection<SseEmitter> values = sseCacheOrDefault.values();
for (SseEmitter sseEmitter: values){
if (sseEmitter != null) {
sseEmitter.complete();
}
}
sseCacheOrDefault.remove(userId);
}
return "over";
}
}
前端代码:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket</title>
<script type="text/javascript">
var host = "127.0.0.1";
var port = '8080';
var userId = "test";
//获取浏览器信息
var clientId = window.btoa(window.navigator.userAgent);
// sse方法
function sseTest(){
var source = new EventSource('http://' + host + ':' + port + '/sse/subscribe?userId=' + userId + '&clientId=' + clientId);
source.onmessage = function (event) {
console.log("SSE:"+ event.data)
};
<!-- 添加一个开启回调 -->
source.onopen = function (event) {
console.log("SSE:"+event);
};
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:sseTest()">运行 SSE</a>
</div>
</body>
</html>
WebSocket
WebSocket:客户端和服务端实现双工通信(双向),多用于即时通信
服务端代码:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
注册ServerEndpointExporter
/**
* 会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
* 要注意,如果使用独立的servlet容器,
* 而不是直接使用springboot的内置容器,
* 就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
*/
@Bean
@ConditionalOnMissingBean(ServerEndpointExporter.class)
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
WebSocket服务代码:(服务端核心逻辑)
package com.example.demo.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@ServerEndpoint(value = "/websocket/{userId}/{clientId}")
public class WebSocket {
private static final AtomicInteger ONLINE_CLIENTS = new AtomicInteger(0);
/**
* key为用户Id,内层Map key为客户端ID(一个用户可能在多个客户端中登陆)
*/
private static final Map<String, Map<String, Session>> CLIENTS = new ConcurrentHashMap<>();
/**
* 打开webSocket连接(注册)
* @param userId 用户ID
* @param clientId 客户端ID
* @param session 连接session
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session) {
addOnlineCount();
CLIENTS.putIfAbsent(userId, new ConcurrentHashMap<>());
CLIENTS.get(userId).put(clientId,session);
System.out.println("注册 userd:" + userId + "," + "clientId:" + clientId);
}
/**
* 关闭webSocket连接(注销)
* @param userId 用户ID
* @param clientId 客户端ID
* @param session 连接session
* @throws IOException
*/
@OnClose
public void onClose(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session) throws IOException {
System.out.println("websocket close userd:" + userId + "," + "clientId:" + clientId);
session.close();
CLIENTS.getOrDefault(userId,new ConcurrentHashMap<>()).remove(clientId);
subOnlineCount();
}
/**
* 接受前端信息
* @param userId 用户ID
* @param clientId 客户端ID
* @param content 消息内容
* @throws IOException
*/
@OnMessage
public void onMessage(@PathParam("userId") String userId, @PathParam("clientId") String clientId , String content) throws InterruptedException {
System.out.println("接受到数据:" + content);
sendContentToUser(userId,clientId,"已接受到数据:" + content);
while (true){
sendContentToUser(userId,clientId, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
Thread.sleep(5000L);
}
}
/**
* 接受前端信息
* @param userId 用户ID
* @param clientId 客户端ID
* @param session 报错到sessin连接
* @param error 抛出到异常
*/
@OnError
public void onError(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session, Throwable error) {
System.out.println("websocket err userd:" + userId + "," + "clientId:" + clientId);
error.printStackTrace();
}
/**
* 发送消息给指定用户和客户端
* @param userId 用户ID
* @param clientId 客户端ID
* @param content 消息内容
*/
public static void sendContentToUser(String userId, String clientId, String content) {
Map<String, Session> clients = CLIENTS.getOrDefault(userId, new ConcurrentHashMap<>());
if(Objects.nonNull(clientId)){
Session session = clients.get(clientId);
sendContent(content, session);
}else {
Collection<Session> values = clients.values();
for (Session session: values){
sendContent(content,session);
}
}
}
/**
* 用给指定session发送消息
* @param content 消息内容
* @param session 指定到session连接
*/
private static void sendContent(String content, Session session) {
if (Objects.nonNull(session)) {
session.getAsyncRemote().sendText(content);
}
}
/**
* 发送消息给所有用户
* @param content 消息内容
*/
public static void sendContentAll(String content) {
Collection<Map<String, Session>> values = CLIENTS.values();
values.forEach(m -> m.values().forEach(s -> sendContent(content,s)));
}
private static int getOnlineCount() {
return ONLINE_CLIENTS.get();
}
private static void addOnlineCount() {
ONLINE_CLIENTS.addAndGet(1);
}
private static void subOnlineCount() {
ONLINE_CLIENTS.decrementAndGet();
}
}
模拟接口:(模拟服务端主动给客户端推送消息)
package com.example.demo.controller;
import com.example.demo.websocket.WebSocket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WebSocketController {
/**
* 模拟给指定用户和客户端发送消息
* @param userId 用户Id
* @param clientId 客户端ID
* @param content 消息内容
* @return String
*/
@RequestMapping("/push")
public String push(String userId,String clientId,String content){
WebSocket.sendContentToUser(userId,clientId,content);
return "OK";
}
/**
* 模拟给所有客户端发消息
* @param content 消息内容
* @return String
*/
@RequestMapping("/pushAll")
public String pushAll(String content){
WebSocket.sendContentAll(content);
return "OK";
}
}
前端代码:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket</title>
<script type="text/javascript">
var host = "127.0.0.1";
var port = '8080';
var userId = "test";
//获取浏览器信息
var clientId = window.btoa(window.navigator.userAgent);
// webSocket方法
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://" + host+ ":" + port + "/websocket/" + clientId);
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
// alert("数据已接收..." + received_msg);
console.log("WebSocket:"+ received_msg);
};
ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="websocket">
<a href="javascript:WebSocketTest()">运行 WebSocket</a><br>
</div>
</body>
</html>