websocket+rabbitmq实战
1. websocket+rabbitmq实战
1.1. 前言
接到的需求是后台定向给指定web登录用户推送消息,且可能同一账号会登录多个客户端都要接收到消息
1.2. 遇坑
- 基于springboot环境搭建的websocket+rabbitmq,搭建完成后发现websocket每隔一段时间会断开,看网上有人因为nginx的连接超时机制断开,而我这似乎是因为长连接空闲时间太长而断开
- 经过测试,如果一直保持每隔段时间发送消息,那么连接不会断开,所以我采用了断开重连机制,分三种情况
- 服务器正常,客户端正常且空闲时间不超过1分钟,则情况正常,超过一分钟会断线,前端发起请求重连
- 服务器正常,客户端关闭或注销,服务器正常收到通知,去除对应客户端session
- 服务器异常,客户端正常,客户端发现连不上服务器会尝试重连3次,3次都连不上放弃重连
- rabbitmq定向推送,按需求需要一台机器对应一批用户,所以定制化需要服务启动的时候定向订阅该ip对应的队列名,简单说就是动态队列名的设定,所以又复杂了点,不能直接在注解写死。同时因为使用的apollo配置中心,同一集群应该相同的配置,所以也不能通过提取配置的方式设定值,为了这个点设置apollo的集群方式有点小题大做,所以采用动态读取数据库对应的ip取出对应的队列名。
- 部署线上tomcat的话,不需要加上一块代码
/**
* 使用tomcat启动无需配置
*/
//@Configuration
//@ConditionalOnProperty(name="websocket.enabled",havingValue = "true")
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
1.3. 正式代码
1.3.1. rabbimq部分
- application.properties配置
spring.rabbitmq.addresses = i.tzxylao.com:5672
spring.rabbitmq.username = admin
spring.rabbitmq.password = 123456
spring.rabbitmq.virtual-host = /
spring.rabbitmq.connection-timeout = 15000
- 交换机和队列配置
/**
* @author laoliangliang
* @date 2019/3/29 11:41
*/
@Configuration
@ConditionalOnProperty(name="websocket.enabled",havingValue = "true")
public class RabbitmqConfig {
final public static String EXCHANGENAME = "websocketExchange";
/**
* 创建交换器
*/
@Bean
FanoutExchange exchange() {
return new FanoutExchange(EXCHANGENAME);
}
@Bean
public Queue queue(){
return new Queue(orderQueueName());
}
@Bean
Binding bindingExchangeMessage(Queue queue,FanoutExchange exchange) {
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public SimpleMessageListenerContainer messageListenerContainer(OrderReceiver orderReceiver, @Qualifier("rabbitConnectionFactory") CachingConnectionFactory cachingConnectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory);
// 监听队列的名称
container.setQueueNames(orderQueueName());
container.setExposeListenerChannel(true);
// 设置每个消费者获取的最大消息数量
container.setPrefetchCount(100);
// 消费者的个数
container.setConcurrentConsumers(1);
// 设置确认模式为自动确认
container.setAcknowledgeMode(AcknowledgeMode.AUTO);
container.setMessageListener(orderReceiver);
return container;
}
/**
* 在这里写获取订单队列名的具体过程
* @return
*/
public String orderQueueName(){
return "orderChannel";
}
}
- 消息监听类
/**
* @author laoliangliang
* @date 2019/3/29 11:38
*/
@Component
@Slf4j
@ConditionalOnProperty(name="websocket.enabled",havingValue = "true")
public class OrderReceiver implements ChannelAwareMessageListener {
@Autowired
private MyWebSocket myWebSocket;
@Override
public void onMessage(Message message, Channel channel) throws Exception {
byte[] body = message.getBody();
log.info("接收到消息:" + new String(body));
try {
myWebSocket.sendMessage(new String(body));
} catch (IOException e) {
log.error("send rabbitmq message error", e);
}
}
}
1.3.2. websocket部分
- 配置服务端点
@Configuration
@ConditionalOnProperty(name="websocket.enabled",havingValue = "true")
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 核心代码
/**
* @author laoliangliang
* @date 2019/3/28 14:40
*/
public abstract class AbstractWebSocket {
protected static Map<String, CopyOnWriteArraySet<Session>> sessionStore = new HashMap<>();
public void sendMessage(String message) throws IOException {
List<String> userCodes = beforeSendMessage();
for (String userCode : userCodes) {
CopyOnWriteArraySet<Session> sessions = sessionStore.get(userCode);
//阻塞式的(同步的)
if (sessions !=null && sessions.size() != 0) {
for (Session s : sessions) {
if (s != null) {
s.getBasicRemote().sendText(message);
}
}
}
}
}
/**
* 删选给谁发消息
* @return
*/
protected abstract List<String> beforeSendMessage();
protected void clearSession(Session session) {
Collection<CopyOnWriteArraySet<Session>> values = sessionStore.values();
for (CopyOnWriteArraySet<Session> sessions : values) {
for (Session session1 : sessions) {
if (session.equals(session1)) {
sessions.remove(session);
}
}
}
}
}
@ServerEndpoint(value = "/websocket")
@Component
@ConditionalOnProperty(name="websocket.enabled",havingValue = "true")
public class MyWebSocket extends AbstractWebSocket {
private static Logger log = LogManager.getLogger(MyWebSocket.class);
@Autowired
private AmqpTemplate amqpTemplate;
@PostConstruct
public void init() {
/*ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(new Runnable() {
int i = 0;
@Override
public void run() {
amqpTemplate.convertAndSend(RabbitFanout.EXCHANGENAME, "",("msg num : " + i).getBytes());
i++;
}
}, 50, 1, TimeUnit.SECONDS);*/
}
/**
* 连接建立成功调用的方法
*
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session) throws TimeoutException {
log.info("websocket connect");
//10M
session.setMaxTextMessageBufferSize(10485760);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
clearSession(session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("from client request:" + message);
CopyOnWriteArraySet<Session> sessions = sessionStore.get(message);
if (sessions == null) {
sessions = new CopyOnWriteArraySet<>();
}
sessions.add(session);
sessionStore.put(message, sessions);
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
clearSession(session);
}
/**
* 这里返回需要给哪些用户发送消息
* @return
*/
@Override
protected List<String> beforeSendMessage() {
//TODO 给哪些用户发送消息
return Lists.newArrayList("6");
}
}
1.3.3. 前端代码
var websocket = null;
var reconnectCount = 0;
function connectSocket(){
var data = basicConfig();
if(data.websocketEnable !== "true"){
return;
}
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
if(data.localIp && data.localIp !== "" && data.serverPort && data.serverPort !== ""){
websocket = new WebSocket("ws://"+data.localIp+":"+data.serverPort+data.serverContextPath+"/websocket");
}else{
return;
}
}else {
alert('当前浏览器 不支持WebSocket')
}
//连接发生错误的回调方法
websocket.onerror = function () {
console.log("连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
reconnectCount = 0;
console.log("连接成功");
};
//接收到消息的回调方法,此处添加处理接收消息方法,当前是将接收到的信息显示在网页上
websocket.onmessage = function (event) {
console.log("receive message:" + event.data);
};
//连接关闭的回调方法
websocket.onclose = function () {
console.log("连接关闭,如需登录请刷新页面。");
if(reconnectCount === 3) {
reconnectCount = 0;
return;
}
connectSocket();
basicConfig();
reconnectCount++;
};
//添加事件监听
websocket.addEventListener('open', function () {
websocket.send(data.userCode);
});
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
console.log("closeWebSocket");
};
}
connectSocket();
function basicConfig(){
var result = {};
$.ajax({
type: "post",
async: false,
url: "${request.contextPath}/basicConfig",
data: {},
success: function (data) {
result = data;
}
});
return result;
}
1.3.4. 后端提供接口
@ApolloConfig
private Config config;
@RequestMapping(value = {"/basicConfig"})
@ResponseBody
public Map<String, Object> getUserCode(HttpSession session) {
Map<String, Object> map = new HashMap<>(2);
map.put("userCode",String.valueOf(session.getAttribute("userCode")));
String websocketEnable = config.getProperty("websocket.enabled", "false");
String serverContextPath = config.getProperty("server.context-path", "");
map.put("websocketEnable", websocketEnable);
map.put("serverContextPath", serverContextPath);
String localIp = config.getProperty("local.ip", "");
String serverPort = config.getProperty("server.port", "80");
map.put("localIp", localIp);
map.put("serverPort", serverPort);
return map;
}
websocket+rabbitmq实战的更多相关文章
- rabbitMQ实战(一)---------使用pika库实现hello world
rabbitMQ实战(一)---------使用pika库实现hello world 2016-05-18 23:29 本站整理 浏览(267) pika是RabbitMQ团队编写的官方Pyt ...
- Java SpringBoot集成RabbitMq实战和总结
目录 交换器.队列.绑定的声明 关于消息序列化 同一个队列多消费类型 注解将消息和消息头注入消费者方法 关于消费者确认 关于发送者确认模式 消费消息.死信队列和RetryTemplate RPC模式的 ...
- celery+RabbitMQ 实战记录2—工程化使用
上篇文章中,已经介绍了celery和RabbitMQ的安装以及基本用法. 本文将从工程的角度介绍如何使用celery. 1.配置和启动RabbitMQ 请参考celery+RabbitMQ实战记录. ...
- RabbitMQ实战经验分享
前言 最近在忙一个高考项目,看着系统顺利完成了这次高考,终于可以松口气了.看到那些即将参加高考的学生,也想起当年高三的自己. 下面分享下RabbitMQ实战经验,希望对大家有所帮助: 一.生产消息 关 ...
- 【RabbitMQ 实战指南】一 RabbitMQ 开发
1.RabbitMQ 安装 RabbitMQ 的安装可以参考官方文档:https://www.rabbitmq.com/download.html 2.管理页面 rabbitmq-management ...
- 【RabbitMQ 实战指南】一 延迟队列
1.什么是延迟队列 延迟队列中存储延迟消息,延迟消息是指当消息被发送到队列中不会立即消费,而是等待一段时间后再消费该消息. 延迟队列很多应用场景,一个典型的应用场景是订单未支付超时取消,用户下单之后3 ...
- RabbitMQ实战应用技巧
1. RabbitMQ实战应用技巧 1.1. 前言 由于项目原因,之后会和RabbitMQ比较多的打交道,所以让我们来好好整理下RabbitMQ的应用实战技巧,尽量避免日后的采坑 1.2. 概述 Ra ...
- rabbitmq实战:一、天降奇兵
缘由,最近换了工作,而新的项目中使用了celery+rabbitmq来实现一个分布式任务队列系统,为了能够维护好这套系统,只能来学习一下这两个组件,顺便把学习笔记记录下来,留作以后回顾,当然如果碰巧能 ...
- Spring Boot 集成 RabbitMQ 实战
Spring Boot 集成 RabbitMQ 实战 特别说明: 本文主要参考了程序员 DD 的博客文章<Spring Boot中使用RabbitMQ>,在此向原作者表示感谢. Mac 上 ...
随机推荐
- 解决问题SyntaxError: Unexpected token import
ES6语法的模块导入导出(import/export)功能,我们在使用它的时候,可能会报错: SyntaxError: Unexpected token import 语法错误:此处不应该出现impo ...
- Windows 2008 如何启用 TLS1.1 1.2
1.下载工具:下载>>> 2.在服务器运行本软件,点击“Bes”按钮,然后必须确保有TLS 1.2被选中,SSL2 SSL3可以不用选择,然后点击Apply按钮,这时系统提醒您重启, ...
- CentOS系统下搭建tomcat服务器
下载相应的linux版jdk和tomcat,本文讲解jdk版本jdk-7u79-linux-x64.tar.gz,tomcat版本apache-tomcat-7.0.69.tar.gz [配置jdk] ...
- Pycharm 项目无法导入自己写的模块(问题记录贴)
问题背景: 从外部导入一个Python项目后,发现包错误,如图: 解决步骤: 1.将目录下的"5-6——apriori.py"复制一份,重命名为”apriori.py": ...
- Android进阶:五、RxJava2源码解析 2
上一篇文章Android进阶:四.RxJava2 源码解析 1里我们讲到Rxjava2 从创建一个事件到事件被观察的过程原理,这篇文章我们讲Rxjava2中链式调用的原理.本文不讲用法,仍然需要读者熟 ...
- notes for lxf(四)
类名首字母通常大写 创建实例 类名 +() __init__方法 创建实例时把一些属性绑上去 __init__方法第一参数永远是self 表示船舰的实例本身 类是实例的模板 实例是一个一个具体的对象 ...
- Maven导入项目时报错 Could not calculate build plan
Could not calculate build plan: Plugin org.apache.maven.plugins:maven-war-plugin:2.2 or one of its d ...
- FCC学习笔记(一)
除了像素,你还可以使用百分比来指定border-radius边框半径的值. 给你的猫咪图片一个50%的border-radius. a元素,也叫anchor(锚点)元素,既可以用来链接到外部地址实现页 ...
- input标签实现小数点后两位保留小数
短短一行代码就可以实现 <input type="number" min="0" max="100" step="0.01& ...
- jqurey.running.min.js运动的字体效果
参考网址: http://yanshi.sucaihuo.com/jquery/22/2226/demo/ 里面有详细的解释 下面是案例效果demo,其中jquery.running.css与jque ...