在日常的开发过程中我们经常遇到的一个场景就是消息提醒。传统的做法是通过ajax轮训访问后台,如果发现了新的消息则将数据返回给前端。这样的做法相对来说开发成本很低,逻辑也很简单。在实时性要求不高的前提下,使用ajax轮训的方式是一个不错的选择。我们可以将轮训的时间间隔设置得相对较长,比如10分钟甚至是30分钟向后台发起一个请求。但是如果你的业务场景是一个高并发且实时性要求高的场景。在这种情况下再使用ajax轮训的方式就很不合适了。首先如果你需要保持实时性,就不得不缩小轮训的时间间隔。轮训的时间间隔一旦缩小,单位时间内你发送的请求数量就很高,这样你的后台压力就会很大。产生的后果就是轻则响应时间长,用户体验不佳;重则服务器宕机。在这样的背景下,WebSocket就是一个不错的备选方案了。
首先,什么是WebSocket? 以下是我在百度百科上截取的一段有关WebSocket的解释
“WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。”
简单概括一下就是:在客户端(浏览器)和服务端(后台Tomcat)之间只建立一次连接(一次握手),然后这个连接是长时间生效的。当后台有数据的时候就会给前端浏览器发送数据。
具体实现:
新建一个maven web工程,引入如下需要使用到的jar包。最重要的是引入了spring-websocket的jar包支持。其他的就是基础的SSM jar包依赖了(我这里用的mybatis - plus)。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring-version>5.1.5.RELEASE</spring-version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<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-context-support</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</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-aspects</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
</dependencies>
设计思路:
一、使用mysql 或者redis(我这里使用的是MySQL)保存推送消息。然后使用spring的定时任务,比如每隔5秒钟扫一次表,如果有尚未发送的数据就将消息数据提取出来。判断消息需要被推送的对象是否已上线,如果确认用户已登录上线后,则使用websocket向用户发送推送消息。
创建Web项目启动器。制定spring以及springmvc 配置类。在springmvc配置类中开启对websocket的支持,在spring配置类中开启定时任务功能。
@Slf4j
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{AppConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
@Configuration
@ComponentScan(basePackages = {"com.itsu.service","com.itsu.dao","com.itsu.compoment"})
@EnableTransactionManagement
@MapperScan(basePackages = "com.itsu.dao")
@EnableScheduling
public class AppConfig {
@Bean
public DruidDataSource dataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://localhost:3306/local_db?useSSL=false&useUnicode=true&characterEncoding=UTF8");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
return druidDataSource;
}
@Bean
public MybatisSqlSessionFactoryBean sessionFactoryBean(){
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
// Resource mapperResource = new ClassPathResource("classpath:mappers/MessageMapper.xml");
// sqlSessionFactoryBean.setMapperLocations(mapperResource);
return sqlSessionFactoryBean;
}
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
}
@Configuration
@ComponentScan(basePackages = "com.itsu.controller")
@EnableWebMvc
@EnableAspectJAutoProxy
@EnableWebSocket
public class WebMvcConfig implements WebMvcConfigurer, WebSocketConfigurer {
@Bean
public FreeMarkerViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver markerViewResolver =
new FreeMarkerViewResolver();
markerViewResolver.setSuffix(".html");
markerViewResolver.setOrder(1);
markerViewResolver.setCache(false);
markerViewResolver.setViewClass(FreeMarkerView.class);
markerViewResolver.setContentType("text/html;charset=utf-8");
return markerViewResolver;
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer freeMarkerConfigurer =
new FreeMarkerConfigurer();
freeMarkerConfigurer.setTemplateLoaderPath("/WEB-INF/views/");
Properties settingProperties = new Properties();
// 刷新模板的周期,单位为秒
settingProperties.setProperty("template_update_delay", "5");
settingProperties.setProperty("default_encoding", "utf-8");
settingProperties.setProperty("datetime_format", "yyyy-MM-dd HH:mm:ss");
settingProperties.setProperty("time_format", "HH:mm:ss");
settingProperties.setProperty("url_escaping_charset", "utf-8");
freeMarkerConfigurer.setFreemarkerSettings(settingProperties);
return freeMarkerConfigurer;
}
//文件上传,bean必须写name属性且必须为multipartResolver,不然取不到文件对象
@Bean(name = "multipartResolver")
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver =
new CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(2097152); //2M
//单个文件大小限制
multipartResolver.setMaxUploadSizePerFile(0);
multipartResolver.setDefaultEncoding("UTF-8");
return multipartResolver;
}
//静态资源的处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/index").setViewName("index");
registry.addViewController("/").setViewName("login");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/submsg").setViewName("submsg");
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter msgConverter = new MappingJackson2HttpMessageConverter();
msgConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8, MediaType.TEXT_HTML, MediaType.APPLICATION_FORM_URLENCODED));
converters.add(msgConverter);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("/");
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(), "/websocket").addInterceptors(new HttpSessionHandshakeInterceptor()).setAllowedOrigins("*");
}
@Bean
public WebSocketHandler webSocketHandler() {
return new MyWebSocketHandler();
}
}
新建一张表tb_msg,以及这张表对应的java bean Message。其中最重要的字段为stat,表示这一条推送消息是否被推送成功。后续也需要用的这个状态来抓取数据。
@Data
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_msg")
public class Message implements Serializable {
private static final long serialVersionUID = -9137338846595226658L;
@TableId(type = IdType.AUTO)
private int id;
@TableField(value = "from_user_id")
private String fromUserId;
@TableField(value = "to_user_id")
private String toUserId;
@TableField(value = "content")
private String content;
@TableField(value = "stat")
private String stat;
@TableField(value = "create_time")
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss")
private Date createTime;
}
定义websockethandler 通过继承TextWebSocketHandler的方式完成。因为这里只做简单示例,仅做了对普通文本内容消息通知。不支持图片、表情消息的推送处理。如果需要集成这些功能,需要继承AbstractWebSocketHandler 并重写handleTextMessage、handleBinaryMessage、handlePongMessage 三个方法。
/**
* @author 苏犇
* @create time 2019/12/4 21:15
*/
@Slf4j
public class MyWebSocketHandler extends TextWebSocketHandler {
private static final ConcurrentMap<String, WebSocketSession> USERS;
private static final String WEBSOCKT_USER = "userId";
static {
USERS = new ConcurrentHashMap<>();
}
public static Set<String> getLoginUsers() {
return USERS.keySet();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("新的客服端连接 。。。 session id:{}", session.getId());
String userId = (String) session.getAttributes().get(WEBSOCKT_USER);
USERS.putIfAbsent(userId, session);
log.info("当前在线人数为:{}", USERS.size());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msgStr = message.getPayload();
JSONObject json = JSON.parseObject(msgStr);
Message msg = json.toJavaObject(Message.class);
String userId = msg.getToUserId();
if (StringUtils.isNoneEmpty(userId)) {
this.sendMessageToUser(userId, message);
} else
this.sendMessageToUsers(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("发生异常", exception);
if (session.isOpen()) {
session.close();
}
WebSocketSession execSession = USERS.remove(session.getId());
log.error("异常产生的session id:{}", execSession.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("有socket连接关闭,session id:{}", session.getId());
log.info("reason: {}", status.getReason());
String userId = (String) session.getAttributes().get(WEBSOCKT_USER);
USERS.remove(userId);
log.info("剩余在线人数为:{}", USERS.size());
}
public static synchronized void sendMessageToUser(String userId, TextMessage message) {
USERS.forEach((key, value) -> {
if (key.equals(userId)) {
try {
value.sendMessage(message);
} catch (IOException e) {
log.error("exception happened on send socket message");
}
}
});
}
public static synchronized void sendMessageToUsers(TextMessage message) {
USERS.forEach((key, value) -> {
try {
value.sendMessage(message);
} catch (IOException e) {
log.error("exception happened on send socket message");
}
});
}
}
编写一个controller控制器实现简单登录逻辑。用户名和密码不为空则登录成功。
@Controller
public class MyController {
@Resource
private MessageService messageService;
@PostMapping("/login.do")
public String login(String userName, String password, HttpServletRequest request, Model model) {
if (StringUtils.isNotBlank(userName) && StringUtils.isNotBlank(password)) {
request.getSession().setAttribute("userId", userName);
return "redirect:/toIndex";
} else {
model.addAttribute("errorMsg", "login fail");
return "login";
}
}
@GetMapping("/toIndex")
public ModelAndView toIndex() {
return new ModelAndView("index");
}
@PostMapping(value = "/submsg.do", produces = "application/json")
@ResponseBody
public Map subMsg(@RequestBody Message message) {
Map map = new HashMap();
try {
message.setCreateTime(new Date());
message.setStat("N");
messageService.saveOne(message);
map.put("result", true);
} catch (Exception e) {
map.put("result", false);
map.put("msg", e.getMessage());
}
return map;
}
}
业务层代码
@Service
public class MessageService {
@Resource
private MessageDAO messageDAO;
@Transactional(rollbackFor = Throwable.class)
public void saveOne(Message message) {
messageDAO.insert(message);
}
}
编写一个定时器任务,时间每间隔5秒钟查询一次数据库。当发现存在未推送的消息时,检查被推送消息的用户是否已登录。如果已登录则通过WebSocketHandler发送消息到前端。
@Component
@Slf4j
public class MessagePushTask {
@Resource
private MessageDAO messageDAO;
@Scheduled(cron = "0/5 * * * * ? ")
public void sendMessage() {
System.err.println("task start ... ");
QueryWrapper<Message> condition = new QueryWrapper<>();
condition.eq("stat", "N");
List<Message> messages = messageDAO.selectList(condition);
Set<String> loginUsers = MyWebSocketHandler.getLoginUsers();
for (Message message : messages) {
if (loginUsers.contains(message.getToUserId())) {
TextMessage msg = new TextMessage(JSON.toJSONString(message));
MyWebSocketHandler.sendMessageToUser(message.getToUserId(), msg);
log.info("message send ... : {}", JSON.toJSONStringWithDateFormat(message, "yyyy-MM-dd hh:mm:ss"));
message.setStat("Y");
messageDAO.update(message, null);
}
}
}
}
前端页面的编写:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="border: 1px solid black; width: 500px; height: 200px;" id="message">
</div>
<textarea style="border: 1px solid aqua; width: 500px; height: 200px;" id="userinput">
</textarea>
<button id="send">发送</button>
<div>
<p>当前选择的好友是:<i id="fuid"></i></p>
<select name="friends" id="fri">
<option value="001">001</option>
<option value="002">002</option>
</select>
<button onclick="choose()">确认</button>
</div>
</body>
<script type="text/javascript" src="/js/jquery.min.js"></script>
<script type="text/javascript">
var fuid;
function choose() {
fuid = $("#fri").val();
$("#fuid").text(fuid);
}
$(function () {
var socket = null;
if ('WebSocket' in window) {
console.log("浏览器支持websocket");
socket = new WebSocket("ws://localhost:8080/websocket");
var fromUserId = '${Session.userId!""}';
$("#send").on('click', function () {
var content = $("#userinput").val();
var msg = {"fromUserId": fromUserId, "content": content, "toUserId": fuid}
socket.send(JSON.stringify(msg));
$("#message").append("<p align='right'>" + "我: " + content + "</p>")
$("#userinput").val("");
})
//连接打开事件
socket.onopen = function () {
console.log("Socket 已打开");
/* var msg = {"fromUserId": fromUserId, "content": "测试群发消息"}
var msgStr = JSON.stringify(msg);
socket.send(msgStr);*/
};
//收到消息事件
socket.onmessage = function (msg) {
var msgStr = msg.data;
var obj = JSON.parse(msgStr);
console.log(obj);
$("#message").append("<p>" + "收到来自" + obj.fromUserId + "的消息:" + obj.content + "</p>")
};
//连接关闭事件
socket.onclose = function () {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function () {
alert("Socket发生了错误");
}
//窗口关闭时,关闭连接
window.unload = function () {
socket.close();
};
} else
alert("您的浏览器不支持websocket");
})
</script>
</html>
启动服务器查看效果,可以看到,后台已经开始每隔5秒钟查询一次db了。
浏览器访问http://localhost:8080/submsg,简单填写如下表单,点击提交,成功返回success。
这时我们查看数据库,发现表中已经插入刚才我们输入的数据了。 Stat = N 表示此消息尚未推送
再看看后台控制台,可以看到已经可以查询到这一条数据了。但由于此被推送的对象尚未登录,所以这条消息并没有被推送。
此时,我们使用userId=jack登录。浏览器地址栏输入http://localhost:8080/login
登录成功后,我们看到收到了来自tom的消息:消息测试。正是我们刚刚创建的那一条消息。
再看看数据库,可以看到stat字段的值已经被改成了Y,已发送状态。
二、使用RabbitMQ做为消息中间件,不再使用定时任务定期查表的方式获取数据。而改为消息消费者从消息队列Broker中提取数据。后监听被推送的用户是否已登录,如果已登录则确认签收消息,否则则拒收消息,并返回消息至队列中。
首先创建一个rabbitmq的spring配置文件。(题外话:这里使用xml而不适用java config的方式是因为我个人没用过javaconfig 配置过rabbitmq,仅仅在springboot 整合rabbitmq的时候接触过。不过这就是另一个故事了。)
<?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:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<rabbit:connection-factory host="localhost" port="5672" username="itsu" password="itsu"
virtual-host="/app" id="connectionFactory"/>
<rabbit:topic-exchange name="boot-message" auto-declare="true" durable="false" auto-delete="false">
<rabbit:bindings>
<rabbit:binding pattern="socket.msg.#" queue="msg-queue"/>
</rabbit:bindings>
</rabbit:topic-exchange>
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" exchange="boot-message"
confirm-callback="bootMsgConfirm" return-callback="bootMsgReturn" mandatory="true"/>
<rabbit:admin id="rabbitAdmin" connection-factory="connectionFactory"/>
<rabbit:queue name="msg-queue" auto-declare="true" auto-delete="false" durable="false" exclusive="false"/>
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" concurrency="1" type="direct"
receive-timeout="1000" max-concurrency="5">
<rabbit:listener ref="bootMsgConfirmListener" queues="msg-queue"/>
</rabbit:listener-container>
<!-- <bean id="messageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter" />-->
<!-- <bean id="bootMsgConcumer" class="com.itsu.compoment.rabbitmq.BootMsgConcumer"/>-->
<bean id="bootMsgConfirmListener" class="com.itsu.compoment.rabbitmq.BootMsgConfirmListener"></bean>
</beans>
定义消息confirm & return 监听,仅做简单打印输出。
@Component
@Slf4j
public class BootMsgConfirm implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("correlationData:{}", correlationData);
if (!ack) {
log.warn(cause);
}
}
}
@Component
@Slf4j
public class BootMsgReturn implements RabbitTemplate.ReturnCallback {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("replyCode:{}; replayText:{}; exchange:{} ; routintKey:{}", replyCode, replyText, exchange, routingKey);
}
}
定义消费者消息监听器,根据被推送用户是否登录来判断是否需要签收消息或者退还消息。
@Slf4j
public class BootMsgConfirmListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
log.info("收到消息:{}", JSON.toJSONString(message));
bytes = message.getBody();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
com.itsu.compoment.socket.Message msg = (com.itsu.compoment.socket.Message) ois.readObject();
Set<String> loginUsers = MyWebSocketHandler.getLoginUsers();
if (loginUsers.contains(msg.getToUserId())) {
TextMessage tms = new TextMessage(JSON.toJSONStringWithDateFormat(msg,"yyyy-MM-dd hh:mm:ss"));
MyWebSocketHandler.sendMessageToUser(msg.getToUserId(),tms);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("用户已上线,确认签收消息");
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
log.info("用户尚未上线,退还消息至rabbitmq");
}
}
}
在messageService中添加生产方法sandToQueue()发送数据到消息Broker ,这里可以看成是自己定义的boot-message交换机。
@Service
public class MessageService {
@Resource
private MessageDAO messageDAO;
@Resource
private RabbitTemplate rabbitTemplate;
@Transactional(rollbackFor = Throwable.class)
public void saveOne(Message message) {
messageDAO.insert(message);
}
public void sendToQueue(Message message) {
rabbitTemplate.convertAndSend("socket.msg.push", message);
}
}
启动服务器看看效果如何;
先在浏览器中访问http://localhost:15672,进入RabbitMQ Management后台界面。可以看到交换机和队列已经完成了绑定。
我们尝试着写入如下一条数据。提交成功。
再次查看后台,发现后台爆出了大量日志。这是因为目前kobe还没登录,消息被退回。之后又被发送到消费者。。。
使用kobe用户登录。
可以看到,马上就收到了刚刚创建的那一条消息。并且后台也不再打出消息退回的日志了,而是打出了确认签收的日志。
到此,已经完成了全部功能。其实这个命题并不是我一开始脑子里想到的,而是我之前在工作闲暇时想着做一个web聊天室,于是乎去看了几篇有关websocket的博文,然后就撸起袖子写了一个web聊天室。当时写到一半我突然想到了实时消息推送的功能,然后一想这玩意用websocket不是正好,于是便构思了一下,做了这么一个简单项目。