spring mvc 4整合spring websocket,点对点式通讯,stomp监听连接
来源:互联网 发布:用友医疗器械软件 编辑:程序博客网 时间:2024/05/21 11:15
spring boot整合spring websocket和本例子一致,只是不需要在xml加入扫描配置(需要引入相应的依赖)
先引入spring websocket需要的两个依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-websocket</artifactId><version>4.1.5.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId><version>4.1.5.RELEASE</version></dependency>
在mvc 的xml加入一下代码,扫描注解方式的配置
<!--扫描配置--><context:component-scan base-package="com.demo.websocket.config" />
配置类
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一样@EnableWebSocketMessageBrokerpublic 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(); }}
HttpSessionIdHandshakeInterceptor:
点对对式发送消息到某一个用户,需要把某一个用户的id存放在session中(项目使用accountId表示用户的唯一性,已在登录的时候,把这个accountId保存到HttpSession中),这里直接获取accountId就可以
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. */@Componentpublic class HttpSessionIdHandshakeInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> 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. */@Componentpublic 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;/** * 监听订阅地址的用户 */@Componentpublic class StompSubscribeEventListener implements ApplicationListener<SessionSubscribeEvent> { 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方法的参数,传递到这里@MessageExceptionHandlerpublic 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
<script src="${resLocal!}js/plugins/socket/sockjs.js"></script> <script src="${resLocal!}js/plugins/socket/stomp.js"></script><script type="text/javascript">var stomp,sock;$(document).ready(function () {if (window.WebSocket){websocketConfig();} else {alert("错误","浏览器不支持websocket技术通讯.");}}); //websocket配置function websocketConfig() {//连接url为endpointChat的endpoint,对应后台WebSoccketConfig的配置 sock = new SockJS("${webPath}contactChatSocket");sockHandle();stompConfig();} function stompConfig() {//使用STOMP子协议的websocket客户端stomp = Stomp.over(sock);stomp.connect({accountId:'${accountId}'}, function(frame) {//这里的订阅地址和controller的convertAndSendToUser地址一致,多出的user必须的,指的是用户之间发送消息 //这里的地址和后台的Controller sendChatMsg方法的chatAddress 一致 stomp.subscribe("/user/${accountId}/queue/contactMessage${chatId}", function (message) {//处理响应消息var json = JSON.parse(message.body); console.log(json); });//订阅处理错误的消息地址,接收错误消息stomp.subscribe("/user/${accountId}/queue/contactErrors${chatId}", function (message) {var response = JSON.parse(message.body);var error = response.error;alert("发送失败"+error);});});} //发送消息function sendMessage() { //文本框的内容 var content = $("#content").val();if ($.trim($("#content").val()).length <= 0){return ;}//拼接成json传到后台var data = { chatId:'${chatId}', schoolId:'${schoolId}', toUserId:'${receiverId}', content:content, fromUserId:'${accountId}', appId:'${appId}', msgType:1};//发送stomp.send("/sendChatMsg",{},JSON.stringify(data) );} function sockHandle() { sock.onopen = function () { console.log("------连接成功------"); }; sock.onmessage = function (event) { console.log('-------Received: ' + event.data); }; sock.onclose = function (event) { console.log('--------Info: connection closed.------'); }; //连接发生错误 sock.onerror = function () { alert("连接错误", "网络超时或通讯地址错误."); disconnect(); } ; } //关闭websocketfunction disconnect() {if (sock != null) {sock.close();sock = null;}}
发生的错误说明
1: 若页面初始化websocket出现错误,打开浏览器的控制台看地址带有info地址,是否显示200。
2: 若不是200证明是路径不对,针对后台websocketConfig配置类和页面的连接地址进行检查。
比如:
websocketConfig配置类
stompEndpointRegistry.addEndpoint("/contactChatSocket")
页面
${webPath}:是后台设置好的路径,请修改项目配置路径
var sock = new SockJS("${webPath}contactChatSocket");
3: 若是200,后面还是报错,检查浏览器的页面地址端口是否填上。
4: 若是200连接成功,发送消息没有提示是否成功或者失败的(没有接收到消息的)
如:
解决方法:
请在后台用logback的debug级别打印spring websocket,会有一个详细的地址,检查页面接收消息的地址是否和这个地址一样。如
后台日志:
15:24:51.394 [clientInboundChannel-2] DEBUG org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler - Processing MESSAGE destination=/user/2016091817033128922/queue/contactMessage2017041515541301674 session=null payload={"isSelf":true,"content":"15","msgType":1,"createtime":"2017-04-20 15:24:51.383"...(truncated)
15:24:51.395 [clientInboundChannel-2] DEBUG org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler - Processing MESSAGE destination=/user/2017021210143438633/queue/contactMessage2017041515541301674 session=null payload={"isSelf":false,"content":"15","msgType":1,"createtime":"2017-04-20 15:24:51.383...(truncated)
蓝色标记部分对应
stomp.subscribe事件的address:
stomp.subscribe(address, function (message) {}
);
检查蓝色标记部分和address是否一致,若一致,就可以接收其他用户发送过来的消息
5:运行项目时,异常信息含有
(spring mvc整合才有可能发生的异常,spring boot没有)
<async-supported>true</async-supported>
这段代码等字样,在web.xml的<filter>和<servlet>两个标签里面加入这段代码就可以解决
<async-supported>true</async-supported>
- spring mvc 4整合spring websocket,点对点式通讯,stomp监听连接
- Spring websocket stomp
- Spring websocket over STOMP使用指南
- Spring boot:WebSocket+SockJS+Stomp实现广播和点对点消息传送
- Spring+STOMP实现WebSocket广播订阅、权限认证、一对一通讯(附源码)
- Stomp Over Websocket的Spring实现
- Spring boot WebSocket stomp简单例子
- 使用maven搭建XML风格的Spring MVC+WebSocket+sockjs+STOMP+ActiveMQ应用
- 使用maven搭建XML风格的Spring MVC+WebSocket+sockjs+STOMP+ActiveMQ应用
- spring websocket stomp 连接开启了用户名和密码认证的ActiveMQ
- Spring 4+Stomp WebSocket+RabbitMQ 3.6.5+Tomcat 8搭建WebSocket服务
- spring boot +WebSocket(三) 点对点式
- spring整合webSocket
- websocket与Spring整合
- spring+websocket整合使用
- Spring整合Websocket
- spring之整合websocket
- Spring容器整合WebSocket
- 享元模式--拒绝多对象,优化第一步(结构型模式05)
- gradle 每次运行都会下载依赖的解决办法
- Linux 文件与目录管理
- 寻找数组中多余元素
- C++ STL入门教程(1)——vector(向量容器)的使用(附完整程序代码)
- spring mvc 4整合spring websocket,点对点式通讯,stomp监听连接
- 实现UTF-8、UCS2编码和解码
- 数据库基础
- CentOS 6.6系统安装配置图文教程
- java des加密解密
- centos下常用指令
- 2017iOS真机调试步骤(获取上线证书步骤)
- 机器学习之正则化(Regularization)
- Tomcat修改service.xml性能调优 增加最大并发连接数