博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
异步实现服务器推送消息(聊天功能示例)
阅读量:7077 次
发布时间:2019-06-28

本文共 14117 字,大约阅读时间需要 47 分钟。

 

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

 

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

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

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

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

 

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

 

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

 

项目示例如下:

1.   新建Maven项目 async-push

 

2.   pom.xml

4.0.0
com.java
async-push
1.0.0
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-oauth2
2.0.0.RELEASE
org.springframework
springloaded
1.2.8.RELEASE
provided
org.springframework.boot
spring-boot-devtools
provided
${project.artifactId}
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
UTF-8
org.springframework.boot
spring-boot-maven-plugin
repackage

 

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 * */@SpringBootApplicationpublic 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 * */@Componentpublic class MessagePool {    private Map
>> messagePool = new HashMap<>(); public void put(String targetId, DeferredResult
> result) { messagePool.put(targetId, result); } public DeferredResult
> 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 * */@Componentpublic class OfflineMessagePool {    private Map
> offlineMessagePool = new HashMap<>(); /** * 增加一条待发送消息 * * @param targetId 发送目标ID * @param message 推送消息体 */ public void add(String targetId, PushMessageVo message) { List
list = offlineMessagePool.get(targetId); if (null == list) { list = new ArrayList<>(); offlineMessagePool.put(targetId, list); } list.add(message); } /** * 获取所有待发送消息 * * @param targetId 发送目标ID * @return 发送目标对应的所有待发送消息 */ public List
get(String targetId) { List
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 * */@RestControllerpublic 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
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
> deferredResult = messagePool.get(sendMessage.getTargetId()); // 如果未上线,存到离线消息池中 if (null == deferredResult) { offlineMessagePool.add(sendMessage.getTargetId(), pushMessage); } // 直接推送消息给目标ID else { List
list = new ArrayList<>(); list.add(pushMessage); deferredResult.setResult(list); } Map
result = new HashMap<>(); result.put("success", true); result.put("sendTime", format.format(pushMessage.getSendTime())); return result; } @GetMapping("/getMessage") public DeferredResult
> getMessage(Principal principal) { DeferredResult
> result = new DeferredResult<>(); // 先取出未推送的离线消息 List
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 * */@ControllerAdvicepublic class ControllerExceptionHandler {    private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);    @ResponseBody    @ExceptionHandler(AsyncRequestTimeoutException.class)    public List
handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) { logger.info("处理异步超时异常"); // 异步超时返回一个空集合,由前端继续发请求 List
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 * */@Configurationpublic 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 * */@Configurationpublic 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 * */@Componentpublic 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

            登录        

用户自定义登录页面

登录框
用户名:
密码:

 

15.   main.html

            首页        
发送给:

 

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

 

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

 浏览器输入首页  

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

 

输入如下信息:

 

User:Logan

Password:123456

 

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

输入信息,发送给 Charles

 

 换用其它浏览器,输入 

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

输入如下信息

 

User:Charles

Password:123456

 

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

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

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

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

 

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

 

功能正常运行

 

 

.

转载于:https://www.cnblogs.com/jonban/p/10391339.html

你可能感兴趣的文章
第十八章 29创建可自动调节大小的string类字符串对象
查看>>
Latex多行公式的处理[转]
查看>>
操作符重载
查看>>
SQLPLUS工具简介
查看>>
AnDroidDraw+DroidDraw实现Android程序UI设计
查看>>
Multipatch 从点文件生成multipatch
查看>>
iOS模拟器,点击textfield为什么不弹出软键盘
查看>>
Jetty 7.6.8 和 8.1.8 发布
查看>>
程序员如何做出“不难看”的设计
查看>>
类苹果启动器 Cairo Dock 3.1.2 稳定版发布
查看>>
2012用户体验年会 奇虎360CEO兼首席体验官 周鸿祎主题演讲——简而未减
查看>>
Shipping your PyQt app for windows
查看>>
keil MDK编译器警告和错误详解
查看>>
javascript/jquery判断是否为undefined或是null!
查看>>
hdu3791(二叉搜索树)
查看>>
[C#]解决生成的缩略图模糊的问题
查看>>
jQuery常用标签详解
查看>>
学用MVC4做网站五:文章
查看>>
学习C++ -> 引用( References )
查看>>
分享:在Qt工程中加Boost
查看>>