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登录的浏览器会自动收到回复,如下所示

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

    功能正常运行

    .

  • 相关阅读:
    sqlserver 行转列 语文,数学,物理,化学
    除去字符串中不相临的重复的字符 aabcad 得 aabcd
    Mvc项目实例 MvcMusicStore 五
    c# 计算两日期的工作时间间隔(排除非工作日)及计算下一个工作时间点.
    html-----016---HTTP 状态消息
    html-----015---HTML ASCII 参考手册
    html-----014---统一资源定位器
    html-----013----实体字符/HTML URL 编码
    html-----012---颜色的改变
    html-----011--子窗体iframe
  • 原文地址:https://www.cnblogs.com/jonban/p/10391339.html
Copyright © 2011-2022 走看看