SpringMVC整合WebSocket + RabbitMQ 实现消息实时推送功能

 

在日常的开发过程中我们经常遇到的一个场景就是消息提醒。传统的做法是通过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&amp;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不是正好,于是便构思了一下,做了这么一个简单项目。

  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值