SpringBoot 框架添加 WebSocket 支持
环境要求和关键技术
开发关键步骤
在 Spring Boot 项目中添加 websocket 支持
//gradle 添加方式
compile('org.springframework.boot:spring-boot-starter-websocket')
<!-- maven 添加方式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
在项目中添加 websocket 的 Spring 配置
- 向SpringBoog注册websocket服务。
- 连接前使用握手拦截器处理用户认证信息(非必要)。
- 设置通信规范(非必要)。
@Configuration //注册为 Spring 配置类
/*
* 开启使用STOMP协议来传输基于代理(message broker)的消息
* 启用后控制器支持@MessgeMapping注解
*/
@EnableWebSocketMessageBroker
//继承 AbstractWebSocketMessageBrokerConfigurer 的配置类实现 WebSocket 配置
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
//注册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<String, Object> attributes) throws Exception {
ServletServerHttpRequest req = (ServletServerHttpRequest) request;
//通过url的query参数获取认证参数
String token = req.getServletRequest().getParameter("token");
//根据token认证用户,不通过返回拒绝握手
Principal user = authenticate(token);
if(user == null){
return false;
}
//保存认证用户
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<String, Object> 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");
}
/**
* 根据token认证授权
* @param token
*/
private Principal authenticate(String token){
//TODO 实现用户的认证并返回用户信息,如果认证失败返回 null
//用户信息需继承 Principal 并实现 getName() 方法,返回全局唯一值
return null;
}
}
WebSocket的 消息处理
- 使用 @MessageMapping 接收客户端消息。
- 使用 @SendTo 发送广播消息。
- 使用 @SendToUser 反馈客户端发送消息结果。
- 使用 SimpMessagingTemplate 消息模板发送广播消息和给指定客户端发消息。
@Controller //注册一个Controller,WebSocket的消息处理需要放在Controller下
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate; //Spring WebSocket消息发送模板
//发送广播通知
@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);
//向客户端发送广播消息(方式二),客户端订阅消息地址为:/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;
}
}
消息处理对象
public class WsMessage {
//消息接收人,对应认证用户Principal.name(全局唯一)
private String toName;
//消息发送人,对应认证用户Principal.name(全局唯一)
private String fromName;
//消息内容
private Object content;
public String getToName() { return toName; }
public void setToName(String toName) { this.toName = toName; }
public String getFromName() { return fromName; }
public void setFromName(String fromName) { this.fromName = fromName; }
public Object getContent() { return content; }
public void setContent(Object content) { this.content = content; }
}
本案例各个通信地址
通信地址除了握手请求地址最好写完整的地址,其它地址均不用写域名或IP
- 握手请求(connect):http://域名或IP/websocket?token=认证token
- 广播订阅地址(subscribe):/topic/notice
- 个人消息订阅地址(subscribe):/user/queue/msg/new
- 发送广播通知(send):/app/addNotice
- 发送点对点消息(send):/app/msg
- 获取消息发送结果(subscribe):/user/queue/msg/result
js 连接 WebSocket
依赖
var sock = new SockJS(contextPath+'websocket?token=' + token); //连接节点
var stomp = Stomp.over(sock);
stomp.connect({}, function(frame){ //连接成功后订阅消息接口
//订阅个人消息
stomp.subscribe('/user/queue/msg/new', function(response){
var result = response.body;
//TODO something
});
//消息发送结果
stomp.subscribe('/user/queue/msg/result', function(response){});
//订阅广播消息
stomp.subscribe('/topic/notice', function(response){});
});
//发送广播
stomp.send('/app/addNotice', {}, '广播内容');
//发送消息
var msg = {
toName: '接收人',
content: '消息内容'
};
stomp.send('/app/msg', {}, JSON.stringify(msg));