1.使用场景
我们在与前端即时推送一些消息的时候,基本都是靠WebSocket实现的,而当前端的项目中需要建立多个WebSocket或者给特定的用户发送不同的消息时,传统的实现方式,我们在WebSocket中会传一些特定的参数当做标识,而今天我们所说的这种基于stomp协议的WebSocket,可以支持客户端订阅不同的主题,我们只要向不同的主题发送信息就行了。
2.SpringBoot中的实现
2.1 Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
2.2 配置基于stomp协议的WebSocket
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{
//某些特定业务中,可能要对WebSocket传入的值做一些操作,或者在刚刚建立连接的时候做一些操作,
//这里支持我们配置自定义的拦截器,去做拦截的操作。
@Autowired
private ConnWebSocketInterceptor connWebSocketInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册一个STOMP的endpoint,并指定使用SockJS协议
registry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/app");
//客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
registry.enableSimpleBroker("/topic", "/queue");
//服务端通知客户端的前缀,可以不设置,默认为user
registry.setUserDestinationPrefix("/user");
}
// 注册拦截器
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(connWebSocketInterceptor);
}
}
2.3(视业务而定)配置WebSocket拦截器
@Component
public class ConnWebSocketInterceptor implements ChannelInterceptor {
// 这里使用的是消息模板,注意,这里的消息模板需要等上面的配置类配置结束之后才能使用
// 但是上面的配置类同样需要我们拦截器的Bean,如何这里不加@Lazy注解,会导致上面的配置类配置失败。
@Lazy
@Autowired
private SimpMessagingTemplate messagingTemplate;
// 这里实现的是在客户端与服务端建立连接之后,直接发送一条信息给客户端
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
messagingTemplate.convertAndSend("/topic/mytopic","Hello Client!");
}
}
2.4 通过握手拦截器校验安全
public class HandleShakeInterceptors implements HandshakeInterceptor {
/**
* 在握手之前执行该方法, 继续握手返回true, 中断握手返回false.
* 通过attributes参数设置WebSocketSession的属性
*/
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
//这里写业务代码,判断连接的用户名密码,或者有无Token等,判断让不让连接,不让连接返回false
return true;
}
/**
* 在握手之后执行该方法. 无论是否握手成功都指明了响应状态码和相应头.
*/
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
2.5 服务端配置接收消息的方法
@Controller
public class TestController
{
// 消息发送模板
@Autowired
private SimpMessagingTemplate messagingTemplate;
// 接收后缀为/test的消息,可以是/app/test
//@SendTo 发送给/topic/test的主题
@MessageMapping("/test")
@SendTo("/topic/test")
public void test(String str){
messagingTemplate.convertAndSend("receive:" + str);
}
//点对点式的发送消息
public void P2P(){
User user=new User();
user.setUserId(1);
messagingTemplate.convertAndSendToUser(user.getUserId,"/queue/p2p","Hello" + user.getUserId);
}
}
3.前端实现
这里只写JS中的方法实现
<script src="https://cdn.bootcss.com/sockjs-client/1.4.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.2/stomp.min.js"></script>
订阅主题
var stompClient = null;
// 订阅主题
function connect() {
var socket = new SockJS('http://192.168.1.42:8080/endpoint');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/test', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
// 订阅点对点
function connect() {
var userId = 1;
var socket = new SockJS('http://192.168.1.42:8080/endpoint');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/user/' + userId + '/queue/getResponse', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
//断开连接
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}