特别说明:1. 本文基于Springboot spring-boot-starter-parent 1.5.1.RELEASE编码,在不同的版本中部分方法有区别。2. 因为博客字数限制,拆分成了两篇文章
第一篇地址:Spring Springboot实现websocket通讯-1
第二篇地址:Spring Springboot实现websocket通讯-2
前面两种建立websocket通讯,不管是用javax的包还是spring的包都是用的比较底层的协议,下面我们来看看用上层的STOMP来建立websocket通讯
SockJs+Spring-WebSocket时,由于SockJs与Spring WebSocket之间采用JSON通讯,需要引入jackson 2的相关jar包
com.fasterxml.jackson.core jackson-core 2.6.3com.fasterxml.jackson.core jackson-databind 2.6.3com.fasterxml.jackson.core jackson-annotations 2.6.3
前面已经提到了STOMP是一个上层协议,STOMP 在 WebSocket 之上提供了一个基于 帧的线路格式层,用来定义消息语义。STOMP 帧:该帧由命令,一个或多个 头信息 以及 负载所组成。如下就是发送 数据的一个 STOMP帧:
SENDdestination:/app/marcocontent-length:20 {"message":"hello word!"}
SEND:STOMP命令,表明会发送一些内容;destination:头信息,用来表示消息发送到哪里;content-length:头信息,用来表示 负载内容的 大小;空行:帧内容(负载)内容
要使用STOMP 通讯,服务端,和客户端都必须支持,服务端的准备步骤
服务端准备工作
- 我们已经配置了STOMP通讯的配置类 WebSocketStompConfig
- 配置了WebSocketChannelInterceptor 和 WebSocketHandshakeInterceptor 两个自定义拦截器
- 一个WebSocketStompController 用于接收客户端消息和响应客户端
- 一个简单的MVC controller 用于跳转websocket 页面
在Spring中启用STOMP通讯不用我们自己去写原生态的帧,spring的消息功能是基于代理模式构建,其实说得复杂,都是封装好了的,如果需要开启SOMP,只需要在websocket配置类上使用@EnableWebSocketMessageBroker (注解的作用为能够在 WebSocket 上启用 STOMP),并实现WebSocketMessageBrokerConfigurer接口,有些教程在这一步会继承AbstractWebSocketMessageBrokerConfigurer 类,我们看一下AbstractWebSocketMessageBrokerConfigurer类的源码,可以看到都是空方法,也是实现的接口,这里推荐自己实现接口,因为官方API上AbstractWebSocketMessageBrokerConfigurer已经标记为废弃
AbstractWebSocketMessageBrokerConfigurer 抽象类
public abstract class AbstractWebSocketMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer { public AbstractWebSocketMessageBrokerConfigurer() { } public void configureWebSocketTransport(WebSocketTransportRegistration registration) { } public void configureClientInboundChannel(ChannelRegistration registration) { } public void configureClientOutboundChannel(ChannelRegistration registration) { } public boolean configureMessageConverters(List messageConverters) { return true; } public void addArgumentResolvers(List argumentResolvers) { } public void addReturnValueHandlers(List returnValueHandlers) { } public void configureMessageBroker(MessageBrokerRegistry registry) { }}
WebSocketMessageBrokerConfigurer接口
public interface WebSocketMessageBrokerConfigurer { // 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs void registerStompEndpoints(StompEndpointRegistry var1); // 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间 void configureWebSocketTransport(WebSocketTransportRegistration var1); // 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 void configureClientInboundChannel(ChannelRegistration var1); // 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 void configureClientOutboundChannel(ChannelRegistration var1); // 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false,不会添加消息转换器,返回true,会添加默认的消息转换器,当然也可以把自己写的消息转换器添加到转换链中 boolean configureMessageConverters(List var1); // 配置消息代理,哪种路径的消息会进行代理处理 void configureMessageBroker(MessageBrokerRegistry var1); // 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法 void addArgumentResolvers(List var1); // 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法 void addReturnValueHandlers(List var1);}
在registerStompEndpoints 方法中,我们可以设置websocket服务的地址,同样,我们也可以根据自身业务需求,去添加拦截器,例如前文我们写的WebSocketHandshakeInterceptor拦截器,可以获取到httpsession,同样,当我们把信息存入map 后,都可以通过通过WebSocketSession的getAttributes()下提供get方法获取
/** * 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务, * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs * * @param registry */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { /* * 1. 将 /serviceName/stomp/websocketJs路径注册为STOMP的端点, * 用户连接了这个端点后就可以进行websocket通讯,支持socketJs * 2. setAllowedOrigins("*")表示可以跨域 * 3. withSockJS()表示支持socktJS访问 * 4. 添加自定义拦截器,这个拦截器是上一个demo自己定义的获取httpsession的拦截器 */ registry.addEndpoint("/stomp/websocketJS") .setAllowedOrigins("*") .withSockJS() .setInterceptors(new WebSocketHandshakeInterceptor()) ; /* * 看了下源码,它的实现类是WebMvcStompEndpointRegistry , * addEndpoint是添加到WebMvcStompWebSocketEndpointRegistration的集合中, * 所以可以添加多个端点 */ registry.addEndpoint("/stomp/websocket"); }
如果我们业务关心,用户的数量,在线数量,连接状况等数据,我们也可以通过ChannelRegistration对象的setInterceptors方法添加监听,这里先展示一个完整的实现类,监听接口在后面会介绍,代码中的WebSocketHandshakeInterceptor 拦截器,是上一个例子已经实现的,用于存储httpsession,WebSocketChannelInterceptor 拦截器 ,在这个拦截器中可以做一些在线人数统计等操作,后面会介绍
package com.wzh.demo.websocket.config;import com.wzh.demo.websocket.handler.MyPrincipalHandshakeHandler;import com.wzh.demo.websocket.interceptor.WebSocketChannelInterceptor;import com.wzh.demo.websocket.interceptor.WebSocketHandshakeInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.converter.MessageConverter;import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;import org.springframework.messaging.simp.config.ChannelRegistration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;import org.springframework.util.AntPathMatcher;import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;import java.util.List;/** * * * @author wzh * @version 2018-08-12 18:38 * @see [相关类/方法] (可选) **/@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { /** * 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务, * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs * * @param registry */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { /* * 1. 将 /serviceName/stomp/websocketJs路径注册为STOMP的端点, * 用户连接了这个端点后就可以进行websocket通讯,支持socketJs * 2. setAllowedOrigins("*")表示可以跨域 * 3. withSockJS()表示支持socktJS访问 * 4. addInterceptors 添加自定义拦截器,这个拦截器是上一个demo自己定义的获取httpsession的拦截器 * 5. addInterceptors 添加拦截处理,这里MyPrincipalHandshakeHandler 封装的认证用户信息 */ registry.addEndpoint("/stomp/websocketJS") //.setAllowedOrigins("*") .addInterceptors(new WebSocketHandshakeInterceptor()) .setHandshakeHandler(new MyPrincipalHandshakeHandler()) .withSockJS() ; /* * 看了下源码,它的实现类是WebMvcStompEndpointRegistry , * addEndpoint是添加到WebMvcStompWebSocketEndpointRegistration的集合中, * 所以可以添加多个端点 */ registry.addEndpoint("/stomp/websocket"); } /** * 配置消息代理 * @param registry */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { /* * enableStompBrokerRelay 配置外部的STOMP服务,需要安装额外的支持 比如rabbitmq或activemq * 1. 配置代理域,可以配置多个,此段代码配置代理目的地的前缀为 /topicTest 或者 /userTest * 我们就可以在配置的域上向客户端推送消息 * 3. 可以通过 setRelayHost 配置代理监听的host,默认为localhost * 4. 可以通过 setRelayPort 配置代理监听的端口,默认为61613 * 5. 可以通过 setClientLogin 和 setClientPasscode 配置账号和密码 * 6. setxxx这种设置方法是可选的,根据业务需要自行配置,也可以使用默认配置 */ //registry.enableStompBrokerRelay("/topicTest","/userTest") //.setRelayHost("rabbit.someotherserver") //.setRelayPort(62623); //.setClientLogin("userName") //.setClientPasscode("password") //; // 自定义调度器,用于控制心跳线程 ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); // 线程池线程数,心跳连接开线程 taskScheduler.setPoolSize(1); // 线程名前缀 taskScheduler.setThreadNamePrefix("websocket-heartbeat-thread-"); // 初始化 taskScheduler.initialize(); /* * spring 内置broker对象 * 1. 配置代理域,可以配置多个,此段代码配置代理目的地的前缀为 /topicTest 或者 /userTest * 我们就可以在配置的域上向客户端推送消息 * 2,进行心跳设置,第一值表示server最小能保证发的心跳间隔毫秒数, 第二个值代码server希望client发的心跳间隔毫秒数 * 3. 可以配置心跳线程调度器 setHeartbeatValue这个不能单独设置,不然不起作用,要配合setTaskScheduler才可以生效 * 调度器我们可以自己写一个,也可以自己使用默认的调度器 new DefaultManagedTaskScheduler() */ registry.enableSimpleBroker("/topicTest","/userTest") .setHeartbeatValue(new long[]{10000,10000}) .setTaskScheduler(taskScheduler); /* * "/app" 为配置应用服务器的地址前缀,表示所有以/app 开头的客户端消息或请求 * 都会路由到带有@MessageMapping 注解的方法中 */ registry.setApplicationDestinationPrefixes("/app"); /* * 1. 配置一对一消息前缀, 客户端接收一对一消息需要配置的前缀 如“'/user/'+userid + '/message'”, * 是客户端订阅一对一消息的地址 stompClient.subscribe js方法调用的地址 * 2. 使用@SendToUser发送私信的规则不是这个参数设定,在框架内部是用UserDestinationMessageHandler处理, * 而不是而不是 AnnotationMethodMessageHandler 或 SimpleBrokerMessageHandler * or StompBrokerRelayMessageHandler,是在@SendToUser的URL前加“user+sessionId"组成 */ registry.setUserDestinationPrefix("/user"); /* * 自定义路径分割符 * 注释掉的这段代码添加的分割符为. 分割是类级别的@messageMapping和方法级别的@messageMapping的路径 * 例如类注解路径为 “topic”,方法注解路径为“hello”,那么客户端JS stompClient.send 方法调用的路径为“/app/topic.hello” * 注释掉此段代码后,类注解路径“/topic”,方法注解路径“/hello”,JS调用的路径为“/app/topic/hello” */ //registry.setPathMatcher(new AntPathMatcher(".")); } /** * 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间 * @param registration */ @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { /* * 1. setMessageSizeLimit 设置消息缓存的字节数大小 字节 * 2. setSendBufferSizeLimit 设置websocket会话时,缓存的大小 字节 * 3. setSendTimeLimit 设置消息发送会话超时时间,毫秒 */ registration.setMessageSizeLimit(10240) .setSendBufferSizeLimit(10240) .setSendTimeLimit(10000); } /** * 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 * @param registration */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { /* * 配置消息线程池 * 1. corePoolSize 配置核心线程池,当线程数小于此配置时,不管线程中有无空闲的线程,都会产生新线程处理任务 * 2. maxPoolSize 配置线程池最大数,当线程池数等于此配置时,不会产生新线程 * 3. keepAliveSeconds 线程池维护线程所允许的空闲时间,单位秒 */ registration.taskExecutor().corePoolSize(10) .maxPoolSize(20) .keepAliveSeconds(60); /* * 添加stomp自定义拦截器,可以根据业务做一些处理 * springframework 4.3.12 之后版本此方法废弃,代替方法 interceptors(ChannelInterceptor... interceptors) * 消息拦截器,实现ChannelInterceptor接口 */ registration.setInterceptors(webSocketChannelInterceptor()); } /** *设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 * @param registration */ @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(10) .maxPoolSize(20) .keepAliveSeconds(60); //registration.setInterceptors(new WebSocketChannelInterceptor()); } /** * 添加自定义的消息转换器,spring 提供多种默认的消息转换器, * 返回false,不会添加消息转换器,返回true,会添加默认的消息转换器,当然也可以把自己写的消息转换器添加到转换链中 * @param list * @return */ @Override public boolean configureMessageConverters(List list) { return true; } /** * 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法 * @param list */ @Override public void addArgumentResolvers(List list) { } /** * 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法 * @param list */ @Override public void addReturnValueHandlers(List list) { } /** * 拦截器加入 spring ioc容器 * @return */ @Bean public WebSocketChannelInterceptor webSocketChannelInterceptor() { return new WebSocketChannelInterceptor(); }}
WebSocketChannelInterceptor 的实现步骤
如果需要添加监听,我们的监听类需要实现ChannelInterceptor接口,在 springframework包5.0.7之前这一步我们一般是实现ChannelInterceptorAdapter 抽象类,不过这个类已经废弃了,文档也推荐直接实现接口。
首先我们看一下,ChannelInterceptor 哪些方法
package org.springframework.messaging.support;import org.springframework.messaging.Message;import org.springframework.messaging.MessageChannel;public interface ChannelInterceptor { // 在消息发送之前调用,方法中可以对消息进行修改,如果此方法返回值为空,则不会发生实际的消息发送调用 Message> preSend(Message> var1, MessageChannel var2); // 在消息发送后立刻调用,boolean值参数表示该调用的返回值 void postSend(Message> var1, MessageChannel var2, boolean var3); /* * 1. 在消息发送完成后调用,而不管消息发送是否产生异常,在次方法中,我们可以做一些资源释放清理的工作 * 2. 此方法的触发必须是preSend方法执行成功,且返回值不为null,发生了实际的消息推送,才会触发 */ void afterSendCompletion(Message> var1, MessageChannel var2, boolean var3, Exception var4); /* 1. 在消息被实际检索之前调用,如果返回false,则不会对检索任何消息,只适用于(PollableChannels), * 2. 在websocket的场景中用不到 */ boolean preReceive(MessageChannel var1); /* * 1. 在检索到消息之后,返回调用方之前调用,可以进行信息修改,如果返回null,就不会进行下一步操作 * 2. 适用于PollableChannels,轮询场景 */ Message> postReceive(Message> var1, MessageChannel var2); /* * 1. 在消息接收完成之后调用,不管发生什么异常,可以用于消息发送后的资源清理 * 2. 只有当preReceive 执行成功,并返回true才会调用此方法 * 2. 适用于PollableChannels,轮询场景 */ void afterReceiveCompletion(Message> var1, MessageChannel var2, Exception var3);}
上面有说道,在ChannelInterceptor接口中的preSend能在消息发送前做一些处理,例如可以获取到用户登录的唯一token令牌,这里的令牌是我们业务传递给客户端的,例如用户在登录成功后跳转到websocket建立连接的页面,我们后台生成的一个标识符,客户端在和服务端建立websocket连接的时候,我们可以从消息头中获取到这种业务参数,并做一系列后续处理,如果要做这种业务操作,我们还需要一个Authentication对象,这个对象是我们自己写的,这个类必须实现java.security.Principal,这里只是做一个简单的token存储,可以根据实际的业务 逻辑进行扩展。
import java.security.Principal;/** * * * @author wzh * @version 2018-08-26 23:30 * @see [相关类/方法] (可选) **/public class WebSocketUserAuthentication implements Principal{ /** * 用户身份标识符 */ private String token; public WebSocketUserAuthentication(String token) { this.token = token; } public WebSocketUserAuthentication() { } /** * 获取用户登录令牌 * @return */ @Override public String getName() { return token; }}
一个消息头拦截器,用于获取用户的认证信息
package com.wzh.demo.websocket.handler;import com.wzh.demo.domain.WebSocketUserAuthentication;import org.apache.commons.lang.StringUtils;import org.apache.log4j.Logger;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.DefaultHandshakeHandler;import javax.servlet.http.HttpSession;import java.security.Principal;import java.util.Map;/** * * * @author wzh * @version 2018-09-18 23:55 * @see [相关类/方法] (可选) **/public class MyPrincipalHandshakeHandler extends DefaultHandshakeHandler{ private static final Logger log = Logger.getLogger(MyPrincipalHandshakeHandler.class); @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map attributes) { HttpSession httpSession = getSession(request); // 获取登录的信息,就是controller 跳转页面存的信息,可以根据业务修改 String user = (String)httpSession.getAttribute("loginName"); if(StringUtils.isEmpty(user)){ log.error("未登录系统,禁止登录websocket!"); return null; } log.info(" MyDefaultHandshakeHandler login = " + user); return new WebSocketUserAuthentication(user); } private HttpSession getSession(ServerHttpRequest request) { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; return serverRequest.getServletRequest().getSession(false); } return null; }}
下面我们做个拦截器,在preSend方法中获取封装首次登陆后的令牌信息,在postSend方法中统计在线人数
WebSocketChannelInterceptor 拦截登录时消息头中的信息
package com.wzh.demo.websocket.interceptor;import com.wzh.demo.domain.WebSocketUserAuthentication;import org.apache.log4j.Logger;import org.springframework.messaging.Message;import org.springframework.messaging.MessageChannel;import org.springframework.messaging.simp.stomp.StompCommand;import org.springframework.messaging.simp.stomp.StompHeaderAccessor;import org.springframework.messaging.support.ChannelInterceptor;import org.springframework.messaging.support.MessageHeaderAccessor;import javax.servlet.http.HttpSession;import static org.springframework.messaging.simp.stomp.StompCommand.CONNECT;/** * * * @author wzh * @version 2018-08-25 23:39 * @see [相关类/方法] (可选) **/public class WebSocketChannelInterceptor implements ChannelInterceptor { public WebSocketChannelInterceptor() { } Logger log = Logger.getLogger(WebSocketChannelInterceptor.class); // 在消息发送之前调用,方法中可以对消息进行修改,如果此方法返回值为空,则不会发生实际的消息发送调用 @Override public Message> preSend(Message> message, MessageChannel messageChannel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); /** * 1. 判断是否为首次连接请求,如果已经连接过,直接返回message * 2. 网上有种写法是在这里封装认证用户的信息,本文是在http阶段,websockt 之前就做了认证的封装,所以这里直接取的信息 */ if(StompCommand.CONNECT.equals(accessor.getCommand())) { /* * 1. 这里获取就是JS stompClient.connect(headers, function (frame){.......}) 中header的信息 * 2. JS中header可以封装多个参数,格式是{key1:value1,key2:value2} * 3. header参数的key可以一样,取出来就是list * 4. 样例代码header中只有一个token,所以直接取0位 */ String token = accessor.getNativeHeader("token").get(0); /* * 1. 这里直接封装到StompHeaderAccessor 中,可以根据自身业务进行改变 * 2. 封装大搜StompHeaderAccessor中后,可以在@Controller / @MessageMapping注解的方法中直接带上StompHeaderAccessor * 就可以通过方法提供的 getUser()方法获取到这里封装user对象 * 2. 例如可以在这里拿到前端的信息进行登录鉴权 */ WebSocketUserAuthentication user = (WebSocketUserAuthentication) accessor.getUser(); System.out.println("认证用户:" + user.toString() + " 页面传递令牌" + token); }else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { } return message; } // 在消息发送后立刻调用,boolean值参数表示该调用的返回值 @Override public void postSend(Message> message, MessageChannel messageChannel, boolean b) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); /* * 拿到消息头对象后,我们可以做一系列业务操作 * 1. 通过getSessionAttributes()方法获取到websocketSession, * 就可以取到我们在WebSocketHandshakeInterceptor拦截器中存在session中的信息 * 2. 我们也可以获取到当前连接的状态,做一些统计,例如统计在线人数,或者缓存在线人数对应的令牌,方便后续业务调用 */ HttpSession httpSession = (HttpSession) accessor.getSessionAttributes().get("HTTP_SESSION"); // 这里只是单纯的打印,可以根据项目的实际情况做业务处理 log.info("postSend 中获取httpSession key:" + httpSession.getId()); // 忽略心跳消息等非STOMP消息 if(accessor.getCommand() == null) { return; } // 根据连接状态做处理,这里也只是打印了下,可以根据实际场景,对上线,下线,首次成功连接做处理 System.out.println(accessor.getCommand()); switch (accessor.getCommand()) { // 首次连接 case CONNECT: log.info("httpSession key:" + httpSession.getId() + " 首次连接"); break; // 连接中 case CONNECTED: break; // 下线 case DISCONNECT: log.info("httpSession key:" + httpSession.getId() + " 下线"); break; default: break; } } /* * 1. 在消息发送完成后调用,而不管消息发送是否产生异常,在次方法中,我们可以做一些资源释放清理的工作 * 2. 此方法的触发必须是preSend方法执行成功,且返回值不为null,发生了实际的消息推送,才会触发 */ @Override public void afterSendCompletion(Message> message, MessageChannel messageChannel, boolean b, Exception e) { } /* 1. 在消息被实际检索之前调用,如果返回false,则不会对检索任何消息,只适用于(PollableChannels), * 2. 在websocket的场景中用不到 */ @Override public boolean preReceive(MessageChannel messageChannel) { return true; } /* * 1. 在检索到消息之后,返回调用方之前调用,可以进行信息修改,如果返回null,就不会进行下一步操作 * 2. 适用于PollableChannels,轮询场景 */ @Override public Message> postReceive(Message> message, MessageChannel messageChannel) { return message; } /* * 1. 在消息接收完成之后调用,不管发生什么异常,可以用于消息发送后的资源清理 * 2. 只有当preReceive 执行成功,并返回true才会调用此方法 * 2. 适用于PollableChannels,轮询场景 */ @Override public void afterReceiveCompletion(Message> message, MessageChannel messageChannel, Exception e) { }}
服务端发送消息大体有两种场景,公告和私信,实现的方式蛮多的,这里只是举例说明,具体可以看此篇博文。
服务端处理消息的场景:
- 公告就是只要订阅了此路径的的用户都能收到,我们使用@SendTo 注解实现,如果不使用注解指定,
会默认交给broker进行处理,例如@MessageMapping("/demo1/twoWays") 这种,就会拼接代理域+路径相当于配置了@SendTo("/topicTest/twoWays"),也可以使用SimpMessagingTemplate.convertAndSend
- 私信就是指定人员才能收到,可以用@SendToUser 注解或者SimpMessagingTemplate 模板类(框架提供)的convertAndSendToUser进行处理
@SendToUser 多用于资源的请求,如果我只是想简单的用websocket向服务器请求资源而已,然后服务器你就把资源给我就行了,别的用户就不用你广播推送了,简单点,就是我请求,你就推送给我
SimpMessagingTemplate.convertAndSendToUser 可以用户发送指定的人员
使用指定人员发送的时候,前缀必须为配置的setUserDestinationPrefix 配置的“/user”,在spring 框架内部以"/user" 为前缀的消息将会通过 UserDestinationMessageHandler 进行处理,而不是 AnnotationMethodMessageHandler 或 SimpleBrokerMessageHandler or StompBrokerRelayMessageHandler。UserDestinationMessageHandler 的主要任务: 是 将用户消息重新路由到 某个用户独有的目的地上。 在处理订阅的时候,它会将目标地址中的 "/user" 前缀去掉,并基于用户 的会话添加一个后缀。如,对 "/user/userTest/notifications" 的订阅最后可能路由到 名为 "/userTest/notifacations-user65a4sdfa" 目的地上
服务端controller 用于接收客户端消息和响应客户端
package com.wzh.demo.controller;import com.alibaba.fastjson.JSON;import com.wzh.demo.domain.WebSocketUserAuthentication;import org.apache.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.handler.annotation.DestinationVariable;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.messaging.simp.annotation.SendToUser;import org.springframework.messaging.simp.stomp.StompHeaderAccessor;import org.springframework.stereotype.Controller;import java.util.HashMap;import java.util.Map;/** * * * @author wzh * @version 2018-08-29 22:58 * @see [相关类/方法] (可选) **/@Controllerpublic class WebSocketStompController { Logger log = Logger.getLogger(WebSocketStompController.class); private final SimpMessagingTemplate messagingTemplate; /** * 实例化Controller的时候,注入SimpMessagingTemplate * @param messagingTemplate */ @Autowired public WebSocketStompController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } /** * 发送广播消息,所有订阅了此路径的用户都会收到此消息 * 这里做个restful风格,其实无所谓,根据项目实际情况进行配置 * restful风格的接口,在springMVC中,我们使用@PathVariable注解, * 在websocket stomp接口中,restful要使用@DestinationVariable * @param groupId * @param json * @param headerAccessor * @return */ @MessageMapping("/sendChatMsg/{groupId}") @SendTo("/topicTest/hello") public Map sendChatMsg(@DestinationVariable(value = "groupId") String groupId, String json, StompHeaderAccessor headerAccessor) { // 这里拿到的user对象是在WebSocketChannelInterceptor拦截器中绑定上的对象 WebSocketUserAuthentication user =(WebSocketUserAuthentication)headerAccessor.getUser(); log.info("公告controller 中获取用户登录令牌:" + user.getName()); log.info("公告拿到客户端传递分组参数:" + groupId); // 这里拿到的json 字符串,其实可以自动绑定到对象上 System.out.println("公告获取客户端传递过来的JSON 字符串:" + json); Map msg = (Map) JSON.parse(json); Map data = new HashMap(); data.put("msg", "公告服务器收到客户端请求,发送广播消息:"+ msg.get("msg")); return data; } /** * 发送私信消息,只是想简单的用websocket向服务器请求资源而已, * 然后服务器你就把资源给我就行了,别的用户就不用你广播推送了,简单点,就是我请求,你就推送给我 * 如果一个帐号打开了多个浏览器窗口,也就是打开了多个websocket session通道, * 这时,spring webscoket默认会把消息推送到同一个帐号不同的session, * 可以利用broadcast = false把避免推送到所有的session中 * @param json * @param headerAccessor * @return */ @MessageMapping("/sendChatMsgByOwn") @SendToUser(value = "/userTest/own") public Map sendChatMsgByOwn(String json, StompHeaderAccessor headerAccessor) { // 这里拿到的user对象是在WebSocketChannelInterceptor拦截器中绑定上的对象 WebSocketUserAuthentication user = (WebSocketUserAuthentication)headerAccessor.getUser(); log.info("SendToUser controller 中获取用户登录令牌:" + user.getName() + " socketId:" + headerAccessor.getSessionId()); // 这里拿到的json 字符串,其实可以自动绑定到对象上 System.out.println("SendToUser获取客户端传递过来的JSON 字符串:" + json); Map msg = (Map)JSON.parse(json); Map data = new HashMap(); data.put("msg", "SendToUser服务器收到客户端请求,发送私信消息:" + msg.get("msg")); return data; } /** * 根据ID 把消息推送给指定用户 * 1. 这里用了 @SendToUser 和 返回值 其意义是可以在发送成功后回执给发送放其信息发送成功 * 2. 非必须,如果实际业务不需要关心此,可以不用@SendToUser注解,方法返回值为void * 3. 这里接收人的参数是用restful风格带过来了,websocket把参数带到后台的方式很多,除了url路径, * 还可以在header中封装用@Header或者@Headers去取等多种方式 * @param accountId 消息接收人ID * @param json 消息JSON字符串 * @param headerAccessor * @return */ @MessageMapping("/sendChatMsgById/{accountId}") @SendToUser(value = "/userTest/callBack") public Map sendChatMsgById( @DestinationVariable(value = "accountId") String accountId, String json, StompHeaderAccessor headerAccessor) { Map msg = (Map)JSON.parse(json); Map data = new HashMap(); // 这里拿到的user对象是在WebSocketChannelInterceptor拦截器中绑定上的对象 WebSocketUserAuthentication user = (WebSocketUserAuthentication)headerAccessor.getUser(); log.info("SendToUser controller 中获取用户登录令牌:" + user.getName() + " socketId:" + headerAccessor.getSessionId()); // 向用户发送消息,第一个参数是接收人、第二个参数是浏览器订阅的地址,第三个是消息本身 // 如果服务端要将消息发送给特定的某一个用户, // 可以使用SimpleMessageTemplate的convertAndSendToUser方法(第一个参数是用户的登陆名username) String address = "/userTest/callBack"; messagingTemplate.convertAndSendToUser(accountId, address, msg.get("msg")); data.put("msg", "callBack 消息已推送,消息内容:" + msg.get("msg")); return data; }}
一个springMVC的controller 用户跳转websocket页面,并封装简单的认证信息
package com.wzh.demo.controller;import com.wzh.demo.websocket.handler.WebSocketHander;import org.springframework.boot.autoconfigure.web.ServerProperties;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.socket.TextMessage;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;import java.util.Date;/** * * * @author wzh * @version 2018-07-09 22:53 * @see [相关类/方法] (可选) **/@Controller@RequestMapping("/websocket")public class WebSocketController { // 跳转stomp websocket 页面 @RequestMapping(value = "/spring/stompSocket.do",method = RequestMethod.GET) public String toStompWebSocket(HttpSession session, HttpServletRequest request, Model model) { // 这里封装一个登录的用户组参数,模拟进入通讯后的简单初始化 model.addAttribute("groupId","user_groupId"); model.addAttribute("session_id",session.getId()); System.out.println("跳转:" + session.getId()); session.setAttribute("loginName",session.getId()); return "/test/springWebSocketStomp"; }}
Html 客户端,客户端需要引入额外的两个js,sockjs.js和stomp.js
Github 地址:
https://github.com/sockjs/sockjs-client
https://github.com/stomp-js/stomp-websocket
API地址:https://stomp-js.github.io/stomp-websocket/codo/extra/docs-src/Usage.md.html
API中文翻译博文:
Title
消息 接收人
这样就可以通过页面做简单的消息推送了