简介
- WebSocket:是一种网络通信协议,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息详情
- sockjs-client:js库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持github
- STOMP:简单(流)文本定向消息协议 介绍
- stomp-websocket:js库,提供一个基于STOMP客户端的WebSocket gihub
CODE
已下代码在demo中都有,但是有的为了博客效果没有简化。
Maven 增加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot 配置
注意事项
- 重写DefaultHandshakeHandler的determineUser方法来自己实现生成用户频道名称,如使用的是spring Security则可忽略此条
- enableSimpleBroker:设置客户端接收消息的前缀
- setUserDestinationPrefix:指定用户频道的前缀,这个前缀必须在enableSimpleBroker中设置过
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//设置端点,前端通过 /ContextPath/端点 进行连接
stompEndpointRegistry.addEndpoint("/any-socket").addInterceptors(new HandshakeInterceptor() {
/**
* 握手前拦截,往attributes存储用户信息,后续用户频道使用
* @param request
* @param response
* @param wsHandler
* @param attributes
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
boolean result = false;
HttpSession session =getSession(request);
if (session != null){
User user =(User)getSession(request).getAttribute("user");
if(user != null){
attributes.put("user",user);
result = true;
}
}
return result;
}
@Nullable
private HttpSession getSession(ServerHttpRequest request) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
return serverRequest.getServletRequest().getSession();
}
return null;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}).setHandshakeHandler(new DefaultHandshakeHandler() {
/**
* 指定握手主体生成规则,后续接收用户消息时会使用,默认用户频道为/UserDestinationPrefix/{Principal.getName}/频道
* @param request
* @param wsHandler
* @param attributes
* @return
*/
@Nullable
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
return new UserPrincipal((User) attributes.get("user"));
}
//支持SockJS
}).withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) {
messageBrokerRegistry.setApplicationDestinationPrefixes("/app");
// 客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
messageBrokerRegistry.enableSimpleBroker("/queue", "/topic");
//指定用户频道前缀,默认为user可修改
messageBrokerRegistry.setUserDestinationPrefix("/queue");
}
@Override
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
String username = session.getPrincipal().getName();
//上线相关操作
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
throws Exception {
String username = session.getPrincipal().getName();
//离线相关操作
super.afterConnectionClosed(session, closeStatus);
}
};
}
});
}
}
服务端代码
** 说明 **
- @MessageMapping("/sendMsg"),对应前端发送消息时调用的路径,访问路径为/ApplicationDestinationPrefixes/sendMsg,此时已与ContextPath无关。
- convertAndSendToUser:向指定用户发送消息,对应设置中的determineUser,和指定的用户频道前缀,最终发送的路径:/用户频道前缀/Principal.getName/后缀
- convertAndSend:向指定频道发送消息,可以使用@SendTo代替
- @SendToUser:向请求的用户对应的用户频道发送消息,与convertAndSendToUser不能互换
@MessageMapping("/sendMsg")
public void sendMsg(Principal principal, String message) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Message msg = JsonUtils.toObject(message, Message.class);
try {
msg.setSendTime(sdf.format(new Date()));
} catch (Exception e) {
}
if (!"TO_ALL".equals(msg.getReceiver())) {
template.convertAndSendToUser(msg.getReceiver(), "/chat", JsonUtils.toJson(msg));
} else {
template.convertAndSend("/notice", JsonUtils.toJson(msg));
}
}
前端代码
function connect() {
//连接端点,此时需加上项目路径
var socket = new SockJS(_baseUrl+'any-socket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
//监听/topic/notice这个频道的消息
stompClient.subscribe('/topic/notice', function (message) {
showMessage(JSON.parse(message.body));
});
//监听当前登录用户这个频道的消息,对应服务端convertAndSendToUser
stompClient.subscribe("/queue/"+_username+"/chat", function (message) {
showMessage(JSON.parse(message.body));
});
});
}
$("#send").click(function () {
debugger;
var msg = {
"username":_username
,"avatar":_avatar
,"content":$("#message").val()
,"receiver":target
};;
//向MessageMapping对应路径发送消息
stompClient.send("/app/sendMsg", {}, JSON.stringify(msg));
$("#message").val("");
});
Demo
demo参考了这个博客,去掉了Spring Security的部分,修改了一对一消息发送的规则,写的比较简陋,第一个用户登录的时候会报错大家将就看看就行。第二个用户登录后刷新第一个登陆用户的页面会加载用户,就可以点对点的聊天了。
https://gitee.com/MeiJM/stompDemo
参考资料
https://www.jianshu.com/p/4ef5004a1c81
https://www.jianshu.com/p/0f498adb3820
https://spring.io/guides/gs/messaging-stomp-websocket/
https://www.callicoder.com/spring-boot-websocket-chat-example/
http://www.ruanyifeng.com/blog/2017/05/websocket.html
https://github.com/spring-guides/gs-messaging-stomp-websocket