異步實現服務器推送消息(聊天功能示例)

 

優勢:異步推送消息只要客戶端發送異步請求就能夠,不依賴客戶端版本,不存在瀏覽器兼容問題。 javascript

 

1、 主要講解技術點,異步實現服務器推送消息html

2、 項目示例,聊天會話功能,主要邏輯以下:前端

    由Logan向 Charles 發送消息,若是Charles在線,則直接發送,不然存儲爲離線消息。java

    Charles 登陸後向服務端發請求獲取消息,首先查詢離線消息,若是有消息直接返回。沒有消息則等待。jquery

 

    因爲長時間沒有消息推送,等待會超時,因此設置超時異常通知,超時則返回空內容到客戶端,由客戶端再次發送獲取消息請求,解決超時問題。web

 

建議先複製項目到本地工程,邊測試邊理解。spring

 

項目示例以下:數據庫

1.   新建Maven項目 async-pushapache

 

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="width: 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="width: 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登陸的瀏覽器會自動收到回覆,以下所示

 

雙方消息顯示內容和時間徹底一直,角色互換。

 

功能正常運行

 

 

.

相關文章
相關標籤/搜索