spring boot整合spring websocket和本例子一致,只是不需要在xml加入扫描配置(需要引入相应的依赖)
先引入spring websocket需要的两个依赖
org.springframework
spring-websocket
4.1.5.RELEASE
org.springframework
spring-messaging
4.1.5.RELEASE
在mvc 的xml加入一下代码,扫描注解方式的配置
webSocketConfig:
import com.demo.websocket.config.HttpSessionIdHandshakeInterceptor;
import com.demo.websocket.config.PresenceChannelInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
/**
* web socket配置
* Created by earl on 2017/4/11.
*/
@Configuration
//开启对websocket的支持,使用stomp协议传输代理消息,
// 这时控制器使用@MessageMapping和@RequestMaping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
/**
* 服务器要监听的端口,message会从这里进来,要对这里加一个Handler
* 这样在网页中就可以通过websocket连接上服务了
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//注册stomp的节点,映射到指定的url,并指定使用sockjs协议
stompEndpointRegistry.addEndpoint("/contactChatSocket").withSockJS().setInterceptors(httpSessionIdHandshakeInterceptor()); ;
}
//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// queue、topic、user代理
registry.enableSimpleBroker("/queue", "/topic","/user");
registry.setUserDestinationPrefix("/user/");
}
/**
* 消息传输参数配置
*/
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
registry.setMessageSizeLimit(8192) //设置消息字节数大小
.setSendBufferSizeLimit(8192)//设置消息缓存大小
.setSendTimeLimit(10000); //设置消息发送时间限制毫秒
}
/**
* 输入通道参数设置
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.taskExecutor().corePoolSize(4) //设置消息输入通道的线程池线程数
.maxPoolSize(8)//最大线程数
.keepAliveSeconds(60);//线程活动时间
registration.setInterceptors(presenceChannelInterceptor());
}
/**
* 输出通道参数设置
*/
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.taskExecutor().corePoolSize(4).maxPoolSize(8);
registration.setInterceptors(presenceChannelInterceptor());
}
@Bean
public HttpSessionIdHandshakeInterceptor httpSessionIdHandshakeInterceptor() {
return new HttpSessionIdHandshakeInterceptor();
}
@Bean
public PresenceChannelInterceptor presenceChannelInterceptor() {
return new PresenceChannelInterceptor();
}
}
import com.demo.websocket.constants.Constants;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* websocket握手(handshake)接口
* Created by earl on 2017/4/17.
*/
@Component
public class HttpSessionIdHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
//解决The extension [x-webkit-deflate-frame] is not supported问题
if(request.getHeaders().containsKey("Sec-WebSocket-Extensions")) {
request.getHeaders().set("Sec-WebSocket-Extensions", "permessage-deflate");
}
//检查session的值是否存在
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
String accountId = (String) session.getAttribute(Constants.SKEY_ACCOUNT_ID);
//把session和accountId存放起来
attributes.put(Constants.SESSIONID, session.getId());
attributes.put(Constants.SKEY_ACCOUNT_ID, accountId);
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
监听用户连接情况PresenceChannelInterceptor:
说明:这里用到的CacheManager的缓存技术是ehcache,大家请结合实际项目保存。(CacheManager代码就不给了)
import club.yelv.component.CacheManager;
import com.demo.websocket.constants.CacheConstant;
import com.demo.websocket.constants.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.stereotype.Component;
/**
* stomp连接处理类
* Created by earl on 2017/4/19.
*/
@Component
public class PresenceChannelInterceptor extends ChannelInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(PresenceChannelInterceptor.class);
@Override
public void postSend(Message> message, MessageChannel channel, boolean sent) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
// ignore non-STOMP messages like heartbeat messages
if(sha.getCommand() == null) {
return;
}
//这里的sessionId和accountId对应HttpSessionIdHandshakeInterceptor拦截器的存放key
String sessionId = sha.getSessionAttributes().get(Constants.SESSIONID).toString();
String accountId = sha.getSessionAttributes().get(Constants.SKEY_ACCOUNT_ID).toString();
//判断客户端的连接状态
switch(sha.getCommand()) {
case CONNECT:
connect(sessionId,accountId);
break;
case CONNECTED:
break;
case DISCONNECT:
disconnect(sessionId,accountId,sha);
break;
default:
break;
}
}
//连接成功
private void connect(String sessionId,String accountId){
logger.debug(" STOMP Connect [sessionId: " + sessionId + "]");
//存放至ehcache
String cacheName = CacheConstant.WEBSOCKET_ACCOUNT;
//若在多个浏览器登录,直接覆盖保存
CacheManager.put(cacheName ,cacheName+accountId,sessionId);
}
//断开连接
private void disconnect(String sessionId,String accountId, StompHeaderAccessor sha){
logger.debug("STOMP Disconnect [sessionId: " + sessionId + "]");
sha.getSessionAttributes().remove(Constants.SESSIONID);
sha.getSessionAttributes().remove(Constants.SKEY_ACCOUNT_ID);
//ehcache移除
String cacheName = CacheConstant.WEBSOCKET_ACCOUNT;
if (CacheManager.containsKey(cacheName,cacheName+accountId) ){
CacheManager.remove(cacheName ,cacheName+accountId);
}
}
}
StompSubscribeEventListener:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
/**
* 监听订阅地址的用户
*/
@Component
public class StompSubscribeEventListener implements ApplicationListener {
private static final Logger logger = LoggerFactory.getLogger(StompSubscribeEventListener.class);
@Override
public void onApplicationEvent(SessionSubscribeEvent sessionSubscribeEvent) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(sessionSubscribeEvent.getMessage());
//这里的sessionId对应HttpSessionIdHandshakeInterceptor拦截器的存放key
// String sessionId = headerAccessor.getSessionAttributes().get(Constants.SESSIONID).toString();
logger.info("stomp Subscribe : "+headerAccessor.getMessageHeaders() );
}
}
常量类:
public class Constants {
private Constants(){}
/**
* SessionId
*/
public static final String SESSIONID = "sessionid";
/**
* Session对象Key, 用户id
*/
public static final String SKEY_ACCOUNT_ID = "accountId";
}
/**
* 缓存常量名称
* Created by earl on 2017/4/17.
*/
public class CacheConstant {
private CacheConstant(){}
/**
* websocket用户accountId
*/
public static final String WEBSOCKET_ACCOUNT = "websocket_account";
}
Controller:
下面的两个方法不要放到service层,亲测在controller层才能够使用
//通过SimpMessagingTemplate向用户发送消息
@Autowiredprivate
SimpMessagingTemplate messagingTemplate;
/**
* 发送消息
*@MessageMapping是stomp提供发送消息的注解,类似@RequestMappeing
* @param json json文本
*/
@MessageMapping("/sendChatMsg")
public void sendChatMsg( String json)throws Exception{
if (StringUtils.isNotBlank(json) ) {
//json转换为实体,JsonUtils代码不给了,自己到网上拿一个
MsgWeixin po = JsonUtil.toObject(json, MsgWeixin.class);
/*浏览器用户通讯订阅的地址.拼接聊天对话id,准确将消息发到某一个聊天中
*防止一个对话能接收多个联系人的消息
*/
String chatAddress = "/queue/contactMessage" + po.getChatId();
//保存到数据库
msgWeixinService .insert();
//若缓存已不存在accountId,证明是连接断开,不推送消息
String cacheName = CacheConstant.WEBSOCKET_ACCOUNT;
if (!CacheManager.containsKey(cacheName,cacheName+po.getToUserId()) ){
return ;
}
//向用户发送消息,第一个参数是接收人、第二个参数是浏览器订阅的地址,第三个是消息本身
messagingTemplate.convertAndSendToUser(po.getFromUserId(),chatAddress,po.getContent());
}
}
//处理发送消息的错误,json是sendChatMsg方法的参数,传递到这里
@MessageExceptionHandler
public void handleExceptions(Exception e,String json) {
log.error("Error handling message: " + e.getMessage());
MsgWeixin po = JsonUtil.toObject(json,MsgWeixin.class);
String errorJson = ResultUtil.getError(ResultCode.UN_KNOW_EXCEPTION);
//把错误信息发回给发送人
messagingTemplate.convertAndSendToUser( po.getFromUserId(),"/queue/contactErrors"+po.getChatId(),errorJson);
}
消息实体:
MsgWeixin(请根据项目修改)
public class MsgWeixin {
private Date createtime ;
private String schoolId ;
private String wxMsgId ;
private String toUserId ;
private String appId ;
private String content ;
private String fromUserId ;
private String status ;
private String chatId ;
private String msgType ;
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public String getSchoolId() {
return schoolId;
}
public void setSchoolId(String schoolId) {
this.schoolId = schoolId;
}
public String getWxMsgId() {
return wxMsgId;
}
public void setWxMsgId(String wxMsgId) {
this.wxMsgId = wxMsgId;
}
public String getToUserId() {
return toUserId;
}
public void setToUserId(String toUserId) {
this.toUserId = toUserId;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getFromUserId() {
return fromUserId;
}
public void setFromUserId(String fromUserId) {
this.fromUserId = fromUserId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getChatId() {
return chatId;
}
public void setChatId(String chatId) {
this.chatId = chatId;
}
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
}
前端页面JS代码(需引进jquery)
说明:
${???}等都是后台页面模板语言的参数值
${resLocal!} :资源路径
${accountId}:后台设置好的当前用户id,spring mvc设置到页面
${chatId}:后台设置好的聊天对话的id
${receiverId}:接收人用户id