本文参与「新人创作礼」活动,一起开启掘金创作之路。
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、配置websocket连接端点及其他配置
package com.dwk.config;
import com.dwk.properties.WebSocketProperties;
import com.dwk.socket.manager.WebSocketManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import java.util.List;
/**
* websocket配置
*/
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketProperties webSocketProperties;
@Autowired
private WebSocketManager webSocketManager;
@Value("${spring.application.name}")
private String applicationName;
/**
* 用户发送请求url="127.0.0.1:12003/socket"与STOMP server进行连接。之后再转发到订阅url;
* PS:端点的作用——客户端在订阅或发布消息到目的地址前,要连接该端点。
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
List<String> endPointLiist = webSocketProperties.getEndPoint();
log.error(applicationName + "服务 websocket连接端点:" + endPointLiist);
if (endPointLiist.size() == 0 || endPointLiist == null){
throw new RuntimeException("请先配置websocket连接端点!websocket.endPoint");
}
String[] endPoints = endPointLiist.stream().toArray(String[]::new);
//设置websocket连接端点
//stompEndpointRegistry.addEndpoint(endPoints).setAllowedOrigins("*").withSockJS();
//spring boot 2.4以上版本使用此行配置
stompEndpointRegistry.addEndpoint(endPoints).setAllowedOriginPatterns("*").withSockJS();
}
/**
* 配置了一个简单的消息代理,如果不重载,默认情况下会自动配置一个简单的内存消息代理,用来处理以"/topic"为前缀的消息。这里重载configureMessageBroker()方法,
* 消息代理将会处理前缀为"/topic"和"/queue"的消息。
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
List<String> websocketPrefixList = webSocketProperties.getWebsocketPrefix();
log.error(applicationName + "服务 websocket前缀:" + websocketPrefixList);
if (websocketPrefixList == null || websocketPrefixList.size() == 0){
throw new RuntimeException("请先配置websocket连接前缀:websocket.websocketPrefix");
}
//1、前端请求需要在请求前加上setApplicationDestinationPrefixes内设置的值,即socket连接前缀
String[] websocketPrefixs = websocketPrefixList.stream().toArray(String[]::new);
registry.setApplicationDestinationPrefixes(websocketPrefixs);
//2、消息代理前缀,如果消息的前缀为"/topic"、"/queue",就会将消息转发给消息代理(broker)再由消息代理广播给当前连接的客户端
registry.enableSimpleBroker("/topic","/queue");
//3、给指定用户发送一对一的主题前缀 默认为/user,可修改
registry.setUserDestinationPrefix("/user");
}
/**
* 添加websocket连接管理
* @param registry
*/
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
registry.addDecoratorFactory(webSocketManager);
WebSocketMessageBrokerConfigurer.super.configureWebSocketTransport(registry);
}
}
3、自定义配置文件 WebSocketProperties:
package com.dwk.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Data
@ConfigurationProperties(prefix = "websocket")
public class WebSocketProperties {
/**websocket前缀地址*/
private List<String> websocketPrefix;
/**websocket连接端点*/
private List<String> endPoint;
}
4、定义websocket管理类
package com.dwk.socket.manager;
import com.dwk.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* websocket 连接管理
*/
@Slf4j
@Component
public class WebSocketManager implements WebSocketHandlerDecoratorFactory {
@Autowired
private WebSocketService webSocketService;
/**存储连接*/
private static final ConcurrentHashMap<String,WebSocketSession> sessionMap = new ConcurrentHashMap<>();
/**连接统计*/
private static final AtomicLong size = new AtomicLong();
/**添加*/
public static void addSession(String key, WebSocketSession webSocketSession){
log.info("添加websocket连接,key = {},session = {}",key,webSocketSession);
size.compareAndSet(1,1);
sessionMap.put(key,webSocketSession);
}
/**移除*/
public static void removeSession(String key){
log.info("移除websocket连接,key = {}",key);
sessionMap.remove(key);
}
/**获取*/
public static WebSocketSession getSession(String key){
if (sessionMap.contains(key)){
return sessionMap.get(key);
}
throw new RuntimeException("该session不存在");
}
/**websocket连接握手,此处用于监听连接是否成功*/
@Override
public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {
return new WebSocketHandlerDecorator(webSocketHandler) {
/**连接建立成功后执行*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("websocket connection key = {},request uri = {},request from = {}",session.getId(),session.getUri(),session.getRemoteAddress());
webSocketService.responseAll(session.getRemoteAddress() + "连接成功!","/topic/greetings");
//此处可以根据session中缓存的身份验证校验session
addSession(session.getId(),session);
super.afterConnectionEstablished(session);
}
/**连接关闭后执行*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("websocket close key = {},uri = {},request from = {}",session.getId(),session.getUri(),session.getRemoteAddress());
removeSession(session.getId());
super.afterConnectionClosed(session, closeStatus);
}
};
}
}
5、编写websocket控制类
package com.dwk.socket;
import cn.hutool.json.JSON;
import com.dwk.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.web.bind.annotation.RestController;
/**
* websocket 控制器
*/
@Slf4j
@RestController
public class WebSocketEndPoint {
@Autowired
private WebSocketService webSocketService;
/**
* 表示服务端可以接收客户端通过主题“/topic/greetings”发送过来的消息,客户端需要在主题"/topic/greetings"上监听并接收服务端发回的消息
* @param
* @param
*/
@MessageMapping("/sendAllConnect")
@SendTo("/topic/greetings")
public void greeting(@Payload String message) {
log.info("客户端消息:" + message);
webSocketService.responseAll(message,"/topic/greetings");
}
/**
* 这里用的是@SendToUser,发送给单一客户端注解。
* 服务端和客户端点对点通信方式即限定请求地址:/user /izn5ursn /queue/message
* 前缀 session-ID 【 通信主题 】
* @return
*/
@MessageMapping("/message")
@SendToUser("/queue/message")
public JSON handleSubscribe(@Payload String message) {
return webSocketService.pointToPoint(message,"/queue/message");
}
}
6、实现具体业务逻辑
package com.dwk.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.dwk.info.WebSocketInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class WebSocketService implements WebSocketInfo {
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;
/**点对点消息*/
@Override
public JSON pointToPoint(String message, String topic) {
log.info("客户端请求message接口发来消息"+message);
Map<String,String> response = new HashMap<>();
response.put("message", "服务端收到:" + message);
JSON parse = JSONUtil.parse(response);
String now = DateUtil.now();
log.info(now + "响应主题:" + topic);
return parse;
}
/**广播消息*/
@Override
@Scheduled(cron = "0/3 * * * * ?")
public void responseAllByScheduled() {
// log.info("广播消息.....");
// Map<String,String> message = new HashMap<>();
// message.put("message", "服务端定时发送的广播消息!");
// JSON parse = JSONUtil.parse(message);
//simpMessageSendingOperations.convertAndSend("/topic/greetings",parse);
}
@Override
public void responseAll(String message,String topic) {
log.info("连接成功.....");
Map<String,String> msg = new HashMap<>();
msg.put("message", message);
JSON parse = JSONUtil.parse(msg);
simpMessageSendingOperations.convertAndSend(topic,parse);
}
}
7、前端请求
<!DOCTYPE html>
<html lang="en">
<meta charset = "utf-8">
<head>
<title>Hello WebSocket</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.0.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('response').innerHTML = '';
}
//网关地址
var gatewayUrl = "http://172.18.2.98:12002";
//请求路由
var serviceName = "/exampleSocket";
//websocket连接端点
var websocketEndPoint = "/socket";
//连接
function connect() {
var socket = new SockJS("http://192.168.0.101:12003/socket");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
//订阅后端广播接口
stompClient.subscribe('/topic/greetings',function(greeting){
var message = "订阅服务端【广播】接口/topic/greetings,接收到消息:" + JSON.parse(greeting.body).message;
console.log(message);
showGreeting(message);
});
/**
* 订阅后端点对点接口
*
* /user/queue/message : /user为前缀,可设为静态 /queue/message
*
*/
stompClient.subscribe('/user/queue/message',function(greeting){
var message = "订阅服务端【点对点】接口/user/queue/message,接收到消息:"+JSON.parse(greeting.body).message;
console.log(message);
showGreeting(message);
});
});
}
//向后端接口发送消息
function sendName() {
var name = document.getElementById('name').value;
var message = { 'name': name };
console.log("向点对点主题发送消息:" + message)
//example 是后端配置的目标前缀
stompClient.send("/example/message", {}, JSON.stringify(message));
}
//向所有订阅了该主题的连接发送消息
function sendAll() {
var name = document.getElementById('name').value;
var message = { 'name': name };
console.log("向广播主题发送消息:" + message)
//example 是后端配置的目标前缀
stompClient.send("/example/sendAllConnect", {}, JSON.stringify(message));
}
//关闭连接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function showGreeting(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
</script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">关闭连接</button>
</div>
<div id="conversationDiv">
<label>向服务端发送消息:</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">发送</button>
<button id="sendAll" onclick="sendAll();">向所有人发送</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
8、运行结果:
9、扩展:结合网关实现websocket通信