zoukankan      html  css  js  c++  java
  • 异步实现服务器推送消息(聊天功能示例)

    优点:异步推送消息只要客户端发送异步请求就可以,不依赖客户端版本,不存在浏览器兼容问题。 

    一、 主要讲解技术点,异步实现服务器推送消息

    二、 项目示例,聊天会话功能,主要逻辑如下:

        由Logan向 Charles 发送消息,如果Charles在线,则直接发送,否则存储为离线消息。

        Charles 登录后向服务端发请求获取消息,首先查询离线消息,如果有消息直接返回。没有消息则等待。

        由于长时间没有消息推送,等待会超时,所以设置超时异常通知,超时则返回空内容到客户端,由客户端再次发送获取消息请求,解决超时问题。

    建议先复制项目到本地工程,边测试边理解。

    项目示例如下:

    1.   新建Maven项目 async-push

    2.   pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.java</groupId>
        <artifactId>async-push</artifactId>
        <version>1.0.0</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.5.RELEASE</version>
        </parent>
    
    
        <dependencies>
    
            <!-- Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
                <version>2.0.0.RELEASE</version>
            </dependency>
    
    
            <!-- 热部署 -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>springloaded</artifactId>
                <version>1.2.8.RELEASE</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>provided</scope>
            </dependency>
    
        </dependencies>
    
        <build>
            <finalName>${project.artifactId}</finalName>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>

    3.   AsyncPushStarter.java

    package com.java;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * 主启动类
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @SpringBootApplication
    public class AsyncPushStarter {
    
        public static void main(String[] args) {
            SpringApplication.run(AsyncPushStarter.class, args);
        }
    
    }

    4.   SendMessageVo.java

    package com.java.vo;
    
    /**
     * 发送消息封装体
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    public class SendMessageVo {
    
        /**
         * 发送目标ID
         */
        private String targetId;
    
        /**
         * 发送消息内容
         */
        private String content;
    
        public String getTargetId() {
            return targetId;
        }
    
        public void setTargetId(String targetId) {
            this.targetId = targetId;
        }
    
        public String getContent() {
            return content;
        }
    
        public void setContent(String content) {
            this.content = content;
        }
    
        @Override
        public String toString() {
            return "SendMessageVo [targetId=" + targetId + ", content=" + content + "]";
        }
    
    }

    5.   PushMessageVo.java

    package com.java.vo;
    
    import java.util.Date;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    /**
     * 推送消息封装体
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    public class PushMessageVo {
    
        /**
         * 发送人ID,即消息来源
         */
        private String srcId;
    
        /**
         * 发送消息内容
         */
        private String content;
    
        /**
         * 发送时间
         */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private Date sendTime;
    
        public String getSrcId() {
            return srcId;
        }
    
        public void setSrcId(String srcId) {
            this.srcId = srcId;
        }
    
        public String getContent() {
            return content;
        }
    
        public void setContent(String content) {
            this.content = content;
        }
    
        public Date getSendTime() {
            return sendTime;
        }
    
        public void setSendTime(Date sendTime) {
            this.sendTime = sendTime;
        }
    
        @Override
        public String toString() {
            return "PushMessageVo [srcId=" + srcId + ", content=" + content + ", sendTime=" + sendTime + "]";
        }
    
    }

    6.   MessagePool.java

    package com.java.pool;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.async.DeferredResult;
    
    import com.java.vo.PushMessageVo;
    
    /**
     * 消息池,存放所有消息
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @Component
    public class MessagePool {
    
        private Map<String, DeferredResult<List<PushMessageVo>>> messagePool = new HashMap<>();
    
        public void put(String targetId, DeferredResult<List<PushMessageVo>> result) {
            messagePool.put(targetId, result);
        }
    
        public DeferredResult<List<PushMessageVo>> get(String targetId) {
            return messagePool.get(targetId);
        }
    
    }

    7.   OfflineMessagePool.java

    package com.java.pool;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.stereotype.Component;
    
    import com.java.vo.PushMessageVo;
    
    /**
     * 离线消息池
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @Component
    public class OfflineMessagePool {
    
        private Map<String, List<PushMessageVo>> offlineMessagePool = new HashMap<>();
    
        /**
         * 增加一条待发送消息
         * 
         * @param targetId 发送目标ID
         * @param message 推送消息体
         */
        public void add(String targetId, PushMessageVo message) {
            List<PushMessageVo> list = offlineMessagePool.get(targetId);
            if (null == list) {
                list = new ArrayList<>();
                offlineMessagePool.put(targetId, list);
            }
            list.add(message);
        }
    
        /**
         * 获取所有待发送消息
         * 
         * @param targetId 发送目标ID
         * @return 发送目标对应的所有待发送消息
         */
        public List<PushMessageVo> get(String targetId) {
            List<PushMessageVo> list = offlineMessagePool.get(targetId);
    
            // 如果存在,则移除后返回
            if (null != list) {
                offlineMessagePool.remove(targetId);
            }
    
            return list;
        }
    
    }

    8.   MessageController.java

    package com.java.controller;
    
    import java.security.Principal;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.context.request.async.DeferredResult;
    
    import com.java.pool.MessagePool;
    import com.java.pool.OfflineMessagePool;
    import com.java.vo.PushMessageVo;
    import com.java.vo.SendMessageVo;
    
    /**
     * 发送接收消息接口类
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @RestController
    public class MessageController {
    
        private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        @Autowired
        private MessagePool messagePool;
    
        @Autowired
        private OfflineMessagePool offlineMessagePool;
    
        @PostMapping("/sentMessage")
        public Map<String, Object> sentMessage(Principal principal, SendMessageVo sendMessage) {
            PushMessageVo pushMessage = new PushMessageVo();
            pushMessage.setSrcId(principal.getName());
            pushMessage.setContent(sendMessage.getContent());
            pushMessage.setSendTime(new Date());
    
            System.out.println(sendMessage);
            System.out.println(pushMessage);
    
            DeferredResult<List<PushMessageVo>> deferredResult = messagePool.get(sendMessage.getTargetId());
    
            // 如果未上线,存到离线消息池中
            if (null == deferredResult) {
                offlineMessagePool.add(sendMessage.getTargetId(), pushMessage);
            }
    
            // 直接推送消息给目标ID
            else {
    
                List<PushMessageVo> list = new ArrayList<>();
                list.add(pushMessage);
                deferredResult.setResult(list);
            }
    
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("sendTime", format.format(pushMessage.getSendTime()));
            return result;
        }
    
        @GetMapping("/getMessage")
        public DeferredResult<List<PushMessageVo>> getMessage(Principal principal) {
            DeferredResult<List<PushMessageVo>> result = new DeferredResult<>();
    
            // 先取出未推送的离线消息
            List<PushMessageVo> list = offlineMessagePool.get(principal.getName());
    
            // 如果有离线消息,直接返回
            if (null != list) {
                result.setResult(list);
            }
    
            // 否则等待接收新消息
            else {
                messagePool.put(principal.getName(), result);
            }
    
            return result;
    
        }
    
    }

     

    9.   ControllerExceptionHandler.java

    package com.java.advice;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
    
    import com.java.vo.PushMessageVo;
    
    /**
     * 捕获异步超时异常,并进行处理
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @ControllerAdvice
    public class ControllerExceptionHandler {
    
        private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
    
        @ResponseBody
        @ExceptionHandler(AsyncRequestTimeoutException.class)
        public List<PushMessageVo> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
            logger.info("处理异步超时异常");
    
            // 异步超时返回一个空集合,由前端继续发请求
            List<PushMessageVo> list = new ArrayList<>();
            return list;
        }
    
    }

     

    下面是安全登录相关配置

    10.   ApplicationContextConfig.java

    package com.java.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * 配置文件类
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @Configuration
    public class ApplicationContextConfig {
    
        /**
         * 配置密码编码器,Spring Security 5.X必须配置,否则登录时报空指针异常
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }

    11.   LoginConfig.java

    package com.java.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    /**
     * 登录相关配置
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @Configuration
    public class LoginConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.authorizeRequests()
    
                    // 设置不需要授权的请求
                    .antMatchers("/js/*", "/login.html").permitAll()
    
                    // 其它任何请求都需要验证权限
                    .anyRequest().authenticated()
    
                    // 设置自定义表单登录页面
                    .and().formLogin().loginPage("/login.html")
    
                    // 设置登录验证请求地址为自定义登录页配置action ("/login/form")
                    .loginProcessingUrl("/login/form")
    
                    // 设置默认登录成功跳转页面
                    .defaultSuccessUrl("/main.html")
    
                    // 暂时停用csrf,否则会影响验证
                    .and().csrf().disable();
        }
    
    }

    12.   SecurityUserDetailsService.java

    package com.java.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    /**
     * UserDetailsService实现类
     * 
     * @author Logan
     * @createDate 2019-02-17
     * @version 1.0.0
     *
     */
    @Component
    public class SecurityUserDetailsService implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            // 数据库存储密码为加密后的密文(明文为123456)
            String password = passwordEncoder.encode("123456");
    
            System.out.println("username: " + username);
            System.out.println("password: " + password);
    
            // 模拟查询数据库,获取属于Admin和Normal角色的用户
            User user = new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("Admin,Normal"));
    
            return user;
        }
    
    }

    13.     静态资源文件如下

    static/login.html

    static/main.html

    static/js/jquery-3.3.1.min.js

    14.   login.html

    <!DOCTYPE html>
    <html>
    
        <head>
            <title>登录</title>
            <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        </head>
    
        <body>
    
            <!--登录框-->
            <div align="center">
                <h2>用户自定义登录页面</h2>
                <fieldset style=" 300px;">
                    <legend>登录框</legend>
                    <form action="/login/form" method="post">
                        <table>
                            <tr>
                                <th>用户名:</th>
                                <td><input name="username" value="Logan" /> </td>
                            </tr>
                            <tr>
                                <th>密码:</th>
                                <td><input type="password" name="password" value="123456" /> </td>
                            </tr>
                            <tr>
                                <th></th>
                                <td></td>
                            </tr>
                            <tr>
                                <td colspan="2" align="center"><button type="submit">登录</button></td>
                            </tr>
                        </table>
                    </form>
                </fieldset>
    
            </div>
    
        </body>
    
    </html>

    15.   main.html

    <!DOCTYPE html>
    <html>
    
        <head>
            <title>首页</title>
            <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
            <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
            <style>
                body,
                div {
                    margin: 0;
                    padding: 0;
                }
            </style>
            <script>
                $(function() {
                    getMessage();
                    $("#content").keydown(function(event) {
                        if(event.keyCode == 13) {
                            sendMessage();
                        }
                    });
                });
    
                function getMessage() {
                    $.get("/getMessage", function(data) {
                        for(var i = 0; i < data.length; i++) {
                            var msg = data[i];
    
                            /* 设置发送目标为消息来源的人,方便回复消息 */
                            $("#targetId").val(msg.srcId);
                            showMessage(msg.srcId, msg.sendTime, msg.content);
                        }
                        getMessage();
                    });
                }
    
                function sendMessage() {
                    var targetId = $("#targetId").val().trim();
                    if(!targetId) {
                        alert("未填写消息接收人!");
                        $("#targetId").focus();
                        return;
                    }
    
                    /*消息内容不做任何处理,只要不为空就发送*/
                    var content = $("#content").html();
                    if(!content) {
                        $("#content").focus();
                        return;
                    }
    
                    /*发送消息*/
                    $.post("/sentMessage", {
                        targetId: targetId,
                        content: content
                    }, function(data) {
                        if(data.success) {
                            $("#content").empty();
                            showMessage("", data.sendTime, content);
                        }
                    });
                }
    
                function showMessage(srcId, sendTime, content) {
                    var title = '<span style="color: green;">' + srcId + '&nbsp;&nbsp;' + sendTime + '</span>';
                    var content = '<div style="padding-left: 10px;">' + content + '</div>';
                    $("#showMessage").append(title).append(content).append("<br />");
    
                    /* 设置滚动条自动翻滚 */
                    $("#showMessage").scrollTop($("#showMessage")[0].scrollHeight);
                }
            </script>
        </head>
    
        <body>
            <div align="center">
    
                <div style="margin: 30px 0px;">
                    发送给:<input id="targetId" name="targetId" value="Charles" placeholder="消息接收人" />
                </div>
    
                <!--消息框-->
                <div style=" 600px;height: 500px;position: relative;">
    
                    <!--消息展示框-->
                    <div id="showMessage" style="border: cornflowerblue solid 2px;height: 300px;text-align: left;overflow: auto;">
    
                    </div>
    
                    <!--隔离条-->
                    <div style="height: 5px; background-color: darkgray;"></div>
    
                    <!--消息发送框-->
                    <div id="content" contenteditable="true" style="border: cornflowerblue solid 2px;height: 150px;text-align: left;">
    
                    </div>
    
                    <!--发送按钮-->
                    <div style="position: absolute;bottom: 0px; right: 10px;">
                        <button onclick="sendMessage()">发送</button>
                    </div>
                </div>
            </div>
    
        </body>
    
    </html>

    16.   js/jquery-3.3.1.min.js 可在官网下载

    https://code.jquery.com/jquery-3.3.1.min.js

    http://code.jquery.com/jquery-3.3.1.min.js

    17.   运行 AsyncPushStarter.java , 启动测试

     浏览器输入首页  http://localhost:8080/main.html

     地址栏自动跳转到登录页面,如下:

    输入如下信息:

    User:Logan

    Password:123456

     单击【登录】按钮,自动跳转到首页。

    输入信息,发送给 Charles

     换用其它浏览器,输入 http://localhost:8080/main.html

     自动跳转到登录页面,如下:

    输入如下信息

    User:Charles

    Password:123456

    用户名一定要是 Charles,否则收不到来自Logen的消息

     单击【登录】按钮,自动跳转到首页。

    自动接收来自Logan的离线消息。

    输入内容回复,在Logan登录的浏览器会自动收到回复,如下所示

    双方消息显示内容和时间完全一直,角色互换。

    功能正常运行

    .

  • 相关阅读:
    Oracle基础知识整理
    linux下yum安装redis以及使用
    mybatis 学习四 源码分析 mybatis如何执行的一条sql
    mybatis 学习三 mapper xml 配置信息
    mybatis 学习二 conf xml 配置信息
    mybatis 学习一 总体概述
    oracle sql 语句 示例
    jdbc 新认识
    eclipse tomcat 无法加载导入的web项目,There are no resources that can be added or removed from the server. .
    一些常用算法(持续更新)
  • 原文地址:https://www.cnblogs.com/jonban/p/10391339.html
Copyright © 2011-2022 走看看