testCompile('org.springframework.boot:spring-boot-starter-test')
compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-web')
compile 'org.springframework.boot:spring-boot-starter-websocket'
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfigure implements WebSocketMessageBrokerConfigurer{
/**
* @Description: 注册stomp端点并将端点映射到特定的路径
**/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册一个断点 用于握手的地址
registry.addEndpoint("/websocket")
//允许websocket跨域
.setAllowedOrigins("*")
//启用websocket备选方案(浏览器不支持的话就会启动)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用简单的消息代理,用于客户端订阅,进行广播的发送的前缀,如下面/agent将会推送经理相关的消息
registry.enableSimpleBroker("/agent","/topic");
// 点对点时使用 给某个用户发信息时的前缀
registry.setUserDestinationPrefix("/user");
// 前端发送信息给后端的前缀
registry.setApplicationDestinationPrefixes("/app");
}
}
如果你们的应用式前后端分离的一般都需要配置跨域,否则会无法连接。然后要注意的是configureMessageBroker方法中的相关配置,订阅、点对点、以及前端发送消息的前缀。
依赖(使用npm install即可)
sockjs-client
stompjs
代码实现
// 主机地址以及registerStompEndpoints中配置的握手地址
const sock=new SockJS("http://localhost:9999/websocket")
//这个stompCLient对象主要是用于前后台交互用的
const stompClient = Stomp.over(sock)
// 连接服务器端
stompClient.connect(
// 请求头
{},
// 连接成功回调
function connectCallback(frame) {
console.log('连接成功')
},
// 连接失败回调
function errorCallBack(error) {
// 连接失败时(服务器响应 ERROR 帧)的回调方法
console.error('连接失败');
}
);
websocket的消息机制是基于订阅发布形式传输的,客户端可以直接发消息给服务端,但是服务端要传输消息给客户端只能通过消息的发布,而且客户端需要订阅相应的消息
// 注册为controller,返回值将自动处理为json
@Controller
@MessageMapping("/test")
public class WebSocketController {
// MessageMapping注解类似于RequestMapping,这里配置的是发送消息的路径
// 前端通过/app/test/agentInfo可以访问该方法
// 目前的话返回值是无效的
@MessageMapping("/agentInfo")
public Map getAgentInfo () {
final HashMap map = new HashMap<>();
map.put("name","xxc");
map.put("age",21);
System.out.println("发送客户经理消息");
return map;
}
}
// 发送消息格式 stompClient.send(destination,headers,body)
stompClient.send('/app/test/agentInfo',{},'')
由此便能调用WebSocketController的getAgentInfo方法,但是目前还不能返回信息
如果想要getAgentInfo返回的消息发送回客户端,则可以使用以下方式:
@MessageMapping("/agentInfo")
@SendTo("/agent/updateAgentInfo")
public Map<String,Object> getAgentInfo ()
通过@SendTo注解,方法返回时会将返回值发送给当前订阅了/agent/updateAgentInfo消息的用户
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/agentInfo")
public void getAgentInfo () {
final HashMap<String, Object> map = new HashMap<>();
map.put("name","xxc");
map.put("age",21);
System.out.println("发送客户经理消息");
// 使用api进行推送
simpMessagingTemplate.convertAndSend('/agent/updateAgentInfo', map);
}
那么前端如何订阅此消息呢?
前端可以通过subscribe方法订阅指定消息
// api stompClient.subscribe(destination, callback)
stompClient.subscribe('/agent/updateAgentInfo', (resp)=> {
// 这样可以在服务器广播消息的时候接受到并打印消息内容
console.log(resp.body)
// 打印: {"name":"xxc","age":21}
// 收到信息后可以手动ack确认一下,让服务器知道你收到了消息
resp.ack()
})
// 订阅的路径会自动加上发送消息的前缀,WebsocketConfigure.java中我配置的是/app
@SubscribeMapping("/agent/updateAgentInfo")
public String onAgentInfoSubscribe () {
System.out.println("hahah");
return "hello";
}
客户端只需要使用subscribe即可完成类似http请求的功能
stompClient.subscribe('/app/agent/updateAgentInfo',(resp) => {
// 处理响应
})
这种订阅是一次性的,用法类似于一次http请求
以上方式返回的消息是广播给所有订阅了该消息的客户端,大家都能收到,那如何做到类似于http的效果,发送消息后接受到想要的推送?
这时我们可以使用@SendToUser注解替换掉原来的@SendTo注解
@MessageMapping("/agentInfo")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
@SendToUser("/agent/updateAgentInfo")
public Map getAgentInfo ()
通过@SendToUser注解则可以做到谁发的消息,推送返回值给谁
客户端js需要做的修改就是将订阅目的地修改为/user/agent/updateAgentInfo,也就是加上点对点前缀/user
stompClient.subscribe('/user/agent/updateAgentInfo', (resp)=> {//...})
我的理解是@SendToUser注解在@SendTo注解的基础上,判断了是否为当前用户,但他使用的destination还是需要为广播的destination,只是在这个基础上加上点对点前缀
当一些用户相关的消息更新了,可能要及时推送给指定的用户,但不是广播给所有人,这怎么做到呢?
首先,在客户端订阅的时候,就要指定自己的用户标识,如用户名,可以根据自己的业务逻辑定义
stompClient.subscribe('/user/xxc/updateAgentInfo', (resp) => {//...})
// destination 格式 点对点前缀/{userid}/真实路径
则,在后端即可使用SimpMessagingTemplate发送消息给xxc
simpMessagingTemplate.convertAndSendToUser("xxc","/updateAgentInfo","推送消息");
在源码中SimpMessagingTemplate使用的仍是convertAndSend方法
@Override
public void convertAndSendToUser(String user, String destination, Object payload,
@Nullable Map headers, @Nullable MessagePostProcessor postProcessor)
throws MessagingException {
Assert.notNull(user, "User must not be null");
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
// 将点对点前缀、用户id和真实的路径进行了拼接
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
有的时候应用需要一定的安全性,而不是谁都可以连接的,这个时候就需要进行身份的验证
客户端需要发送身份信息
可以在connect的时候通过header传递一些自定义头,如token
stompClient.connect(
{
token: token // token是用户登录系统后返回的身份码,这里需要你自己定
},
function connectCallback(frame) {
console.log(frame);
},
function errorCallBack(error) {
// 连接失败时(服务器响应 ERROR 帧)的回调方法
console.error("连接失败");
}
);
服务器拦截验证
public class UserInterceptor implements ChannelInterceptor {
@Override
public Message> preSend(Message> message, MessageChannel channel) {
final StompHeaderAccessor wrap = StompHeaderAccessor.wrap(message);
final Object ls = wrap.getHeader("token");
if (ls == null) {
return null;
}
if (ls instanceof LinkedList) {
String token = (String) ((LinkedList) ls).get(0);
// 校验操作 ...
System.out.println(token);
// 如果没有token就返回null
if (token == null) {
return null;
}
}
return message;
}
}
在拦截器返回空则客户端连接成功的回调不会调用,但是也不会调用失败的回调
配置拦截器 WebsocketConfigure.java
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(userInterceptor());
}
/**
* WebSocket的拦截器,做用户认证操作
* @return
*/
@Bean
public UserInterceptor userInterceptor() {
return new UserInterceptor();
}
讲的比较体系化的文章
讲的比较好的一篇文章
比较完整的websocket、stomp、socketJs的介绍