在有的业务中,当更改状态时,可能需要大量的轮询来实现,用websocket能够很好的实现,但是因为工作中很多都是采取服务器集群来实现的,所以对集群情况下的websocket进行了学习,在围观大佬之后,进行了改造,使之贴合我们公司架构,springmvc。
github地址:https://github.com/onthewayw/springmvc_websocket_mq.git
1:pom.xml
<properties>
<spring-version>4.3.18.RELEASE</spring-version>
<servlet-version>3.1.0</servlet-version>
<log4j-version>1.2.17</log4j-version>
<slf4j-log4j12-version>1.7.9</slf4j-log4j12-version>
<slf4j-api-version>1.7.9</slf4j-api-version>
<junit-version>4.12</junit-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12-version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api-version}</version>
</dependency>
<!-- servlet
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-version}</version>
<scope>provided</scope>
</dependency>-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit-version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.7</version>
</dependency>
<!-- 添加MQ依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>1.4.5.RELEASE</version>
</dependency>
<!--这里需要引入jackson库,否则jsonMessageConverter实例化就会报错,如果使用Gson需要实现jsonMessageConverter的父类方法自己手动转一次,然后jsonMessageConverter的配置换成自己实现的类-->
<!--Jackson 核心库-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.9</version>
</dependency>
<!--Jackson 序列化库-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.9</version>
</dependency>
<!--Jackson 注解支持-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.9</version>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.redis.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:redis="http://www.springframework.org/schema/redis"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd
">
<context:component-scan base-package="com.wang.*" />
<context:property-placeholder location="classpath:spring-rabbitMQ.properties" ignore-unresolvable="true"/>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="minIdle" value="${redis.minIdle}" />
<property name="maxTotal" value="${redis.maxTotal}" />
<property name="testOnBorrow" value="true" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name="${redis.ip}" p:port="${redis.port}" p:password="${redis.password}"
p:pool-config-ref="jedisPoolConfig" p:usePool="true"/>
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate" p:connection-factory-ref="jedisConnectionFactory"/>
<!-- Redis连接-->
<!--<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name="192.168.19.129" p:port="6379" p:password="123456">
<constructor-arg ref="jedisPoolConfig" />
</bean>-->
<!-- 缓存序列化方式 -->
<bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="valueSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
<property name="keySerializer" ref="keySerializer" />
<property name="valueSerializer" ref="valueSerializer" />
<property name="hashKeySerializer" ref="keySerializer" />
<property name="hashValueSerializer" ref="valueSerializer" />
</bean>
<!--<bean id="redisListenerContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>-->
<bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
<!--序列化-->
<bean id="jdkSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<!-- 配置监听器,redis在订阅消息时,会根据 redis:listener标签指定的方法名和通道(topic)调用不同的方法-->
<bean id="listener" class="com.wang.listener.SubscribeListener" />
<redis:listener-container connection-factory="jedisConnectionFactory">
<!-- the method attribute can be skipped as the default method name is
"handleMessage"
topic代表监听的通道,是一个正规匹配 -->
<redis:listener ref="listener" serializer="jdkSerializer" method="handleMessage" topic="im-*" />
</redis:listener-container>
</beans>
3 订阅监听类,对消息进行监听
public class SubscribeListener implements MessageListener {
private final Logger logger = LoggerFactory.getLogger(SubscribeListener.class);
private StringRedisTemplate redisTemplate;
private Session session;
public Logger getLogger() {
return logger;
}
public StringRedisTemplate getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@Override
public void onMessage(Message message, byte[] pattern) {
String msg = new String(message.getBody());
logger.info(new String(pattern) + "主题发布:" + msg);
if(null!=session){
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4 websocket端点
@ServerEndpoint(value = "/im_webSocket/{topic}/{username}")
public class WebsocketEndpoint {
/**
* 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例
*/
private StringRedisTemplate redisTemplate = SpringUtils.getBean(StringRedisTemplate.class);
private RedisMessageListenerContainer redisMessageListenerContainer = SpringUtils.getBean(RedisMessageListenerContainer.class);
//存放该服务器该ws的所有连接。用处:比如向所有连接该ws的用户发送通知消息。
private static CopyOnWriteArraySet<WebsocketEndpoint> sessions = new CopyOnWriteArraySet<>();
private Session session;
@OnOpen
public void onOpen(Session session, @PathParam("topic")String topic){
System.out.println("java websocket:打开连接:topic------"+topic);
this.session = session;
sessions.add(this);
SubscribeListener subscribeListener = new SubscribeListener();
subscribeListener.setSession(session);
subscribeListener.setRedisTemplate(redisTemplate);
//设置订阅topic
try{
redisMessageListenerContainer.addMessageListener(subscribeListener,new ChannelTopic(topic));
}catch (Exception e){
e.printStackTrace();
}
}
@OnMessage
public void onMessage(Session session, String message,@PathParam("topic")String topic,@PathParam("username") String username){
System.out.println("websocket 收到消息:"+message);
PulishService pulishService = SpringUtils.getBean(PulishService.class);
pulishService.publish(topic, message);
}
@OnClose
public void onClose(Session session){
System.out.println("java websocket:关闭连接");
sessions.remove(this);
}
@OnError
public void onError(Session session,Throwable error){
System.out.println("java websocket 出现错误");
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
}
5.service
@Component
public class PulishService {
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
public void publish(String channel, Object message){
//该方法封装的 connection.publish(rawChannel, rawMessage);
redisTemplate.convertAndSend(channel,message);
}
}
6.controller
@Controller
@RequestMapping("webSocket")
public class WebsocketController {
@RequestMapping("/index/{topic}/{username}")
public ModelAndView index(@PathVariable("topic") String topic, @PathVariable("username") String username) {
ModelAndView mv = new ModelAndView("/websocket_1");
mv.addObject("topic", topic);
mv.addObject("username", username);
return mv;
}
}
7.客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"></meta>
<title>websocket+redis集群</title>
</head>
<input type="hidden" id="port" value='${port }'>
<input type="hidden" id="topic" value='${topic }'>
<input type="hidden" id="username" value='${username }'>
<body>
${topic} 频道 聊天中。。。<br/>
<input id="input_id" type="text" /><button onclick="sendMessage()">发送</button> <button onclick="closeWebsocket()">关闭</button>
<div id="message_id"></div>
</body>
<script>
var topic = document.getElementById("topic").value;
var username = document.getElementById("username").value;
var websocket = new WebSocket('ws://127.0.0.1:8081/im_webSocket/'+topic+'/'+username);
console.log(websocket);
websocket.onopen = function(event){
setMessage("打开连接");
}
websocket.onclose = function(event){
setMessage("关闭连接");
}
websocket.onmessage = function(event){
setMessage(event.data);
}
websocket.onerror = function(event){
setMessage("连接异常");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
closeWebsocket();
}
//关闭websocket
function closeWebsocket(){
//3代表已经关闭
if(3!=websocket.readyState){
websocket.close();
}else{
alert("websocket之前已经关闭");
}
}
//将消息显示在网页上
function setMessage(message){
document.getElementById('message_id').innerHTML += message + '<br/>';
}
//发送消息
function sendMessage(){
//1代表正在连接
if(1==websocket.readyState){
var message = document.getElementById('input_id').value;
//setMessage(message);
websocket.send(message);
}else{
alert("websocket未连接");
}
document.getElementById('input_id').value="";
document.getElementById('input_id').focus();
}
</script>
</html>
在写的时候出现了如下错误:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.listener.RedisMessageListenerContainer' available: expected single matching bean but found 2: redisListenerContainer,org.springframework.data.redis.listener.RedisMessageListenerContainer#0
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1041)
那是因为在这里多注入了一次。
因为@ServerEndpoint注解之下并不能用@Autowired进行注入,所以用springUtil工具类进行注入
springUtil工具类
package com.wang.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Repository;
@Repository
public final class SpringUtils implements BeanFactoryPostProcessor {
private static ConfigurableListableBeanFactory beanFactory; // Spring应用上下文环境
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
SpringUtils.beanFactory = beanFactory;
}
public static ConfigurableListableBeanFactory getBeanFactory() {
return beanFactory;
}
/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) getBeanFactory().getBean(name);
}
/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*
*/
public static <T> T getBean(Class<T> clz) throws BeansException {
T result = (T) getBeanFactory().getBean(clz);
return result;
}
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*
* @param name
* @return boolean
*/
public static boolean containsBean(String name) {
return getBeanFactory().containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().isSingleton(name);
}
/**
* @param name
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getAliases(name);
}
}
或者还有一种方法,如下:
在其中加入如上代码,就可以进行注入。
Redis发布订阅与ActiveMQ的比较
(1)ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持;
(2)ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;
(3)ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
总之,ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的,但是如果系统中已经有了Redis,并且需要基本的发布订阅功能,就没有必要再安装ActiveMQ了,因为可能ActiveMQ提供的功能大部分都用不到,而Redis的发布订阅机制就能满足需求。
等有时间测试mq集成的例子。希望不足之处多多指正