这里主要基于上一篇介绍一下基于SocketJS+Stomp来实现的的长连接。我借鉴了其他的一些博客,只是把我用的知识总结在了一起方便我自己以后回顾。
概述:
WebSocket协议提供了通过一个套接字实现全双工通信的功能。除了其他的功能之外,它能够实现Web浏览器和服务器之间的异步通信。全双工意味着服务器可以发送消息给浏览器,浏览器也可以发送消息给服务器。
使用Spring的低层级WebSocketAPI
按照其最简单的形式,WebSocket只是两个应用之间通信的通道。位于WebSocket一端的应用发送消息,另一端接收消息。因为它是全双工的,所以每一端都可以发送和处理消息。
WebSocket通信可以应用于任何类型的应用中,但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。由于websocket协议是个低层协议, 不是应用层协议, 未对payload的格式进行规范, 导致我们需要自己定义消息体格式, 自己解析消息体, 成本高, 扩展性也不好, 所以我们引入了已被很多库和消息队列厂商实现的stomp协议, 将websocket协议与stomp协议结合。
我们再总结一下websocket与stomp的优点
websocket相对于http的优点:
全双工. 相对于http协议只能由client发送消息. 全双工的websocket协议, server与client都可以发送消息.
消息体更轻量. http的一个请求比websocket的请求大不少. 主要因为http的每次请求都要加很多的header.
stomp over websocket相对于websocket的优点:
不需要自己去规定消息的格式, 以及对消息的格式做解析.
由于stomp是一个统一的标准, 有很多库与厂商都对stomp协议进行了支持. 拿来用就可以. 成本低, 扩展好
maven依赖
4.0.0
com.aisino
message-push
1.0-SNAPSHOT
org.springframework.cloud
spring-cloud-dependencies
Finchley.RC2
pom
import
org.springframework.boot
spring-boot-starter-parent
2.0.2.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
org.projectlombok
lombok
1.16.10
com.alibaba
fastjson
1.2.8
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-thymeleaf
spring-milestones
Spring Milestones
https://repo.spring.io/libs-milestone
false
前段JS代码
Title
测试
首先要建立前段与后端服务的长连接,我这里采用的token认证,拿着token去认证授权再成功拿到用户信息后才能连接成功。
package com.wyc.messagepush.configure;
import com.wyc.messagepush.entity.MyPrincipal;
import com.wyc.messagepush.service.PrinalInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
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.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.util.Map;
import java.security.Principal;
@Configuration //注册为 Spring 配置类
/*
* 开启使用STOMP协议来传输基于代理(message broker)的消息
* 启用后控制器支持@MessgeMapping注解
*/
@EnableWebSocketMessageBroker
//继承 AbstractWebSocketMessageBrokerConfigurer 的配置类实现 WebSocket 配置或实现WebSocketMessageBrokerConfigurer接口
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);
// feign实例调用用户中心获取Principal或税号等信息对象
@Autowired
private PrinalInterface prinalInterface;
//注册STOMP协议节点并映射url
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket") //注册一个 /websocket 的 websocket 节点
.addInterceptors(myHandshakeInterceptor()) //添加 websocket握手拦截器
.setHandshakeHandler(myDefaultHandshakeHandler()) //添加 websocket握手处理器
.setAllowedOrigins("*") //设置允许可跨域的域名
.withSockJS(); //指定使用SockJS协议
}
/**
* WebSocket 握手拦截器
* 可做一些用户认证拦截处理
*/
private HandshakeInterceptor myHandshakeInterceptor(){
return new HandshakeInterceptor() {
/**
* websocket握手连接
* @return 返回是否同意握手
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
ServletServerHttpRequest req = (ServletServerHttpRequest) request;
//通过url的query参数获取认证参数
String token = req.getServletRequest().getParameter("token");
//根据token认证用户并拿到用户信息,不通过返回拒绝握手
Principal user = authenticate();
if(user == null){
logger.info("Authentication is failed!!! Connection rejection.");
return false;
}
logger.info("Authentication is Ok,Saving Authenticated Users,Username is "+user.getName());
//保存认证用户
attributes.put("token", token);
attributes.put("user", user);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
//WebSocket 握手处理器
private DefaultHandshakeHandler myDefaultHandshakeHandler(){
return new DefaultHandshakeHandler(){
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map attributes) {
//设置认证通过的用户到当前会话中
return (Principal)attributes.get("user");
}
};
}
/**
* 定义一些消息连接规范(也可不设置)
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//设置客户端接收消息地址的前缀(可不设置)
registry.enableSimpleBroker(
"/topic", //广播消息前缀
"/queue" //点对点消息前缀
);
//设置客户端接收点对点消息地址的前缀,默认为 /user
registry.setUserDestinationPrefix("/user");
//设置客户端向服务器发送消息的地址前缀(可不设置)
registry.setApplicationDestinationPrefixes("/app");
// Use this for enabling a Full featured broker like RabbitMQ
/*生产环境相关配置信息
registry.enableStompBrokerRelay("/topic")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest");
*/
}
/**
* 根据token认证授权
* @param token
*/
private Principal authenticate(){
//TODO 实现用户的认证并返回用户信息,如果认证失败返回 null
// 一种:用户信息需继承 Principal 并实现 getName() 方法,返回全局唯一值
// 二种:这里实现的是用token换取用户信息
String username = prinalInterface.member();
MyPrincipal principal = prinalInterface.test();
if (principal == null)
{
logger.error("Failed to invoke authentication service!");
return null;
}
principal.setUsername("12345678900"+username);
System.out.println(principal.toString());
System.out.println(principal.getName());
logger.info("FeignClient is succeed,Username is:"+username);
if(principal != null)
return principal;
return null;
}
}
在上面握手处理完毕后建立连接,我写了一个监听类,可以监听建立成功或断开连接的状态,可以进行一些处理。比如sessionid的存储和删除。
package com.wyc.messagepush.controller;
import com.wyc.messagepush.entity.MyPrincipal;
import com.wyc.messagepush.entity.SocketSessionRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import java.util.Map;
/**
* Created by wyc on 25/07/19.
*/
@Component
public class WebSocketEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
/**session操作类*/
@Autowired
SocketSessionRegistry webAgentSessionRegistry;
//Spring WebSocket消息发送模板
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketConnectListener( SessionConnectedEvent event) {
String sessionId = (String) event.getMessage().getHeaders().get("simpSessionId");
GenericMessage genericMessage = (GenericMessage) event.getMessage().getHeaders().get("simpConnectMessage");
Map mapUser = (Map) genericMessage.getHeaders().get("simpSessionAttributes");
String username = ((MyPrincipal)mapUser.get("user")).getName();
logger.info("Successful Connection Establishment,Username is:"+username);
webAgentSessionRegistry.registerSessionId(username,sessionId);
logger.info("Received a new web socket connection");
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
String sessionId = (String) event.getMessage().getHeaders().get("simpSessionId");
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
// String username = (String) headerAccessor.getSessionAttributes().get("user");
String username = ((MyPrincipal) headerAccessor.getSessionAttributes().get("user")).getName();
if(username != null) {
// 连接断开时,将session从全局HashMap移除
webAgentSessionRegistry.unregisterSessionId(username,sessionId);
logger.info("User Disconnected : " + username);
/*
// 如果是聊天室项目退出时可以发消息进行广播通知
ChatMessage chatMessage = new ChatMessage();
chatMessage.setType(ChatMessage.MessageType.LEAVE);
chatMessage.setSender(username);
messagingTemplate.convertAndSend("/topic/public", chatMessage);*/
}
}
}
到这连接建立算是成功了。下面是controller层和实体类的主要实现:
package com.wyc.messagepush.controller;
import com.wyc.messagepush.entity.SocketSessionRegistry;
import com.wyc.messagepush.entity.WsMessage;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import org.springframework.messaging.MessageHeaders;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
@Controller //注册一个Controller,WebSocket的消息处理需要放在Controller下
public class WsController {
// 开启日志
private static final Logger logger = LoggerFactory.getLogger(WsController.class);
//Spring WebSocket消息发送模板
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**session操作类*/
@Autowired
private SocketSessionRegistry webAgentSessionRegistry;
//发送广播通知
@MessageMapping("/addNotice") //接收客户端发来的消息,客户端发送消息地址为:/app/addNotice
@SendTo("/topic/notice") //向客户端发送广播消息(方式一),客户端订阅消息地址为:/topic/notice
public WsMessage notice(String notice, Principal fromUser) {
//TODO 业务处理
WsMessage msg = new WsMessage();
// msg.setFromName(fromUser.getName());
msg.setContent(notice);
System.out.println("notice");
//向客户端发送广播消息(方式二),客户端订阅消息地址为:/topic/notice
// messagingTemplate.convertAndSend("/topic/notice", msg);
return msg;
}
//发送点对点消息
@MessageMapping("/msg") //接收客户端发来的消息,客户端发送消息地址为:/app/msg
@SendToUser("/queue/msg/result") //向当前发消息客户端(就是自己)发送消息的发送结果,客户端订阅消息地址为:/user/queue/msg/result
public boolean sendMsg(WsMessage message, Principal fromUser){
//TODO 业务处理
message.setFromName(fromUser.getName());
//向指定客户端发送消息,第一个参数Principal.name为前面websocket握手认证通过的用户name(全局唯一的),客户端订阅消息地址为:/user/queue/msg/new
messagingTemplate.convertAndSendToUser(message.getToName(), "/queue/msg/new", message);
return true;
}
//广播推送消息
// @Scheduled(fixedRate = 10000)
//@SendTo("/topic/notice")
public void sendTopicMessage() {
System.out.println("后台广播推送!");
WsMessage wsMessage=new WsMessage();
wsMessage.setToName("oyzc");
wsMessage.setContent("一百万");
this.messagingTemplate.convertAndSend("/topic/notice",wsMessage);
}
/**
* 同样的发送消息 只不过是ws版本 http请求不能访问
* 根据用户key发送消息
* @param
* @return
* @throws Exception
*/
@MessageMapping("/msg/hellosingle")
public void greeting2() throws Exception {
Map params = new HashMap(1);
params.put("test","test");
System.out.println("单点推送!");
WsMessage message=new WsMessage();
message.setToName("2");
message.setContent("您有新消息待查看!");
//这里没做校验
String sessionId=webAgentSessionRegistry.getSessionIds(message.getToName()).stream().findFirst().get();
// String sessionId=webAgentSessionRegistry.getSessionIds(message.getToName());
System.out.println("sessionId:"+sessionId);
messagingTemplate.convertAndSendToUser(sessionId,"/queue/msg/new",message,createHeaders(sessionId));
}
@RequestMapping(value = "/message", method = RequestMethod.POST)
@ResponseBody
public String messageInform(@RequestParam("Json")String messageJson) {
Map map = (Map) JSON.parse(messageJson);
System.out.println("单点推送!");
WsMessage message=new WsMessage();
// message.setToName("1");
message.setContent("1您有新消息待查看!");
message.setCTaxNo(map.get("No"));
message.setIMachineNo(Integer.valueOf(map.get("MachineNo")));
ConcurrentMap> concurrentMap = webAgentSessionRegistry.getAllSessionIds();
for (String key: concurrentMap.keySet()){
if (key.toUpperCase().contains(message.getCTaxNo()))
{
//这里没做校验
String sessionId=webAgentSessionRegistry.getSessionIds(key).stream().findFirst().get();
// String sessionId=webAgentSessionRegistry.getSessionIds(message.getToName());
System.out.println("sessionId:"+sessionId);
messagingTemplate.convertAndSendToUser(sessionId,"/queue/msg/new",message,createHeaders(sessionId));
}
}
return "200";
}
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
/*@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
// Add username in web socket session
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage;
}*/
}
package com.wyc.messagepush.entity;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
*
* 用户session记录类,用来保存用户名与sessionId的映射
*/
@Component
public class SocketSessionRegistry{
//this map save every session
//这个集合存储session
private final ConcurrentMap> userSessionIds = new ConcurrentHashMap();
private final Object lock = new Object();
public SocketSessionRegistry() {
}
/**
*
* get sessionId
* @param user
* @return
*/
public Set getSessionIds(String user) {
Set set = (Set)this.userSessionIds.get(user);
return set != null?set: Collections.emptySet();
}
/**
* get all session
* @return
*/
public ConcurrentMap> getAllSessionIds() {
return this.userSessionIds;
}
/**
* register session
* @param user
* @param sessionId
*/
public void registerSessionId(String user, String sessionId) {
Assert.notNull(user, "User must not be null");
Assert.notNull(sessionId, "Session ID must not be null");
Object var3 = this.lock;
synchronized(this.lock) {
Object set = (Set)this.userSessionIds.get(user);
if(set == null) {
set = new CopyOnWriteArraySet();
this.userSessionIds.put(user, (Set) set);
}
((Set)set).add(sessionId);
}
}
/**
* remove session
* @param userName
* @param sessionId
*/
public void unregisterSessionId(String userName, String sessionId) {
Assert.notNull(userName, "User Name must not be null");
Assert.notNull(sessionId, "Session ID must not be null");
Object var3 = this.lock;
synchronized(this.lock) {
Set set = (Set)this.userSessionIds.get(userName);
if(set != null && set.remove(sessionId) && set.isEmpty()) {
this.userSessionIds.remove(userName);
}
}
}
}
package com.wyc.messagepush.entity;
import java.security.Principal;
/**
* 实现Principal接口用来接收Feign调用的OAuth2的Principal的值
* 所有的序列化操作必须要有默认构造器,可以不写,这里做一下说明
*
*/
public class MyPrincipal implements Principal {
// 用户名
private String username;
public MyPrincipal(){
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getName() {
return username;
}
}
package com.wyc.messagepush.entity;
import lombok.Data;
/**
* 消息实体类
*/
@Data
public class WsMessage {
//消息接收人,对应认证用户Principal.name(全局唯一)
private String toName;
//消息发送人,对应认证用户Principal.name(全局唯一)
private String fromName;
//消息内容
private Object content;
// token
private String token;
// 主号码
private String No;
// 次号
private int MachineNo;
}
启动类,我这里用到了feign声明式调用和自动调度:
package com.wyc;
import org.springframework.boot.SpringApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableScheduling
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.wyc.messagepush.service")
public class PushApplication {
public static void main(String[] args) {
SpringApplication.run(PushApplication.class,args);
}
}
概述
WebSocket是一个相对比较新的规范,在Web浏览器和应用服务器上没有得到一致的支持。所以我们需要一种WebSocket的备选方案。
而这恰恰是SockJS所擅长的。SockJS是WebSocket技术的一种模拟,在表面上,它尽可能对应WebSocket API,但是在底层非常智能。如果WebSocket技术不可用的话,就会选择另外的通信方式。
使用SockJS
WebSocketConfig.java
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(marcoHandler(), "/marco").withSockJS();
}
只需加上withSockJS()方法就能声明我们想要使用SockJS功能,如果WebSocket不可用的话,SockJS的备用方案就会发挥作用。
JavaScript客户端代码
要在客户端使用SockJS,需要确保加载了SockJS客户端库。
除了加载SockJS客户端库外,要使用SockJS只需要修改两行代码即可:
var url = 'marco';
var sock = new SockJS(url); //SockJS所处理的URL是http://或https://,不再是ws://和wss://
//使用相对URL。例如,如果包含JavaScript的页面位于"http://localhost:8080/websocket"的路径下
// 那么给定的"marco"路径将会形成到"http://localhost:8080/websocket/marco"的连接
运行效果一样,但是客户端–服务器之间通信的方式却有了很大的变化。
概述
STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息的语义。STOMP帧由命令、一个或多个头信息以及负载所组成。例如如下就是发送数据的一个STOMP帧:
>>> SEND
destination:/app/marco
content-length:20
{"message":"Maeco!"}
在这个简单的样例中,STOMP命令是SEND,表明会发送一些内容。紧接着是两个头信息:一个用来表示消息要发送到哪里的目的地,另外一个则包含了负载的大小。然后,紧接着是一个空行,STOMP帧的最后是负载内容。
STOMP帧中最有意思的是destination头信息了。它表明STOMP是一个消息协议。消息会发布到某个目的地,这个目的地实际上可能真的有消息代理作为支撑。另一方面,消息处理器也可以监听这些目的地,接收所发送过来的消息。
启用STOMP消息功能
WebSocketStompConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/marcopolo").withSockJS();//为/marcopolo路径启用SockJS功能
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry)
{
//表明在topic、queue、users这三个域上可以向客户端发消息。
registry.enableSimpleBroker("/topic","/queue","/users");
//客户端向服务端发起请求时,需要以/app为前缀。
registry.setApplicationDestinationPrefixes("/app");
//给指定用户发送一对一的消息前缀是/users/。
registry.setUserDestinationPrefix("/users/");
}
}
@Override
protected Class>[] getServletConfigClasses() {
return new Class>[] {WebSocketStompConfig.class,WebConfig.class};
}
WebSocketStompConfig 重载了registerStompEndpoints()方法,将/marcopolo注册为STOMP端点。这个路径与之前接收和发送消息的目的地路径有所不同。这是一个端点,客户端在订阅或发布消息到目的地前,要连接该端点。
WebSocketStompConfig还通过重载configureMessageBroker()方法配置了一个简单的消息代理。这个方法是可选的,如果不重载它的话,将会自动配置一个简单的内存消息代理,用它来处理以“/topic”为前缀的消息。
处理来自客户端的STOMP消息
testConroller.java
@Controller
public class testConroller {
@MessageMapping("/marco")
public void handleShout(Shout incoming)
{
System.out.println("Received message:"+incoming.getMessage());
}
@SubscribeMapping("/subscribe")
public Shout handleSubscribe()
{
Shout outing = new Shout();
outing.setMessage("subscribes");
return outing;
}
}
@MessageMapping注解,表明handleShout()方法能够处理指定目的地上到达的消息。本例中目的地也就是“/app/marco”。(“/app”前缀是隐含 的,因为我们将其配置为应用的目的地前缀)
@SubscribeMapping注解,与@MessageMapping注解相似,当收到了STOMP订阅消息的时候,带有@SubscribeMapping注解的方法将会被触发。
Shout.java
public class Shout {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
客户端JavaScript代码
Received message:Marco!
发送消息到客户端
如果你想要在接收消息的时候,同时在响应中发送一条消息,那么需要做的仅仅是将内容返回就可以了。
@MessageMapping("/marco")
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}
当@MessageMapping注解标示的方法有返回值的时候,返回的对象将会进行转换(通过消息转换器)并放到STOMP帧的负载中,然后发给消息代理。
默认情况下,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会加上“/topic”前缀。
stomp.subscribe('/topic/marco', function(message){ 订阅后将会接收到消息。
});
不过我们可以通过为方法添加@SendTo注解,重载目的地:
@MessageMapping("/marco")
@SendTo("/queue/marco")
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}
stomp.subscribe('/queue/marco', function(message){
});
在应用的任意地方发送消息
Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。
使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessageSendingOperations)自动装配到所需的对象中。
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;
@RequestMapping("/test")
public void sendMessage()
{
simpMessageSendingOperations.convertAndSend("/topic/test", "测试SimpMessageSendingOperations ");
}
访问/test后:
为目标用户发送消息
使用@SendToUser注解,表明它的返回值要以消息的形式发送给某个认证用户的客户端。
@MessageMapping("/message")
@SendToUser("/topic/sendtouser")
public Shout message()
{
Shout outing = new Shout();
outing.setMessage("SendToUser");
return outing;
}
stomp.subscribe('/users/topic/sendtouser', function(message){//给指定用户发送一对一的消息前缀是/users/。
});
这个目的地使用了/users作为前缀,以/users作为前缀的目的地将会以特殊的方式进行处理。以/users为前缀的消息将会通过UserDestinationMessageHandler进行处理。
UserDestinationMessageHandler的主要任务是将用户消息重新路由到某个用户独有的目的地上。在处理订阅的时候,它会将目标地址中的/users前缀去掉,并基于用户的会话添加一个后缀。
为指定用户发送消息
SimpMessagingTemplate还提供了convertAndSendToUser()方法。convertAndSendToUser()方法能够让我们给特定用户发送消息。
simpMessageSendingOperations.convertAndSendToUser("1", "/message", "测试convertAndSendToUser");
stomp.subscribe('/users/1/message', function(message){
});
客户端接收一对一消息的主题是"/users/"+usersId+"/message",这里的用户Id可以是一个普通字符串,只要每个客户端都使用自己的Id并且服务器端知道每个用户的Id就行了。